Repository: python/mypy

Issue:
Crash when overriding field as property/method on frozen derived class
I see this on python/mypy master (41b40aa4f) and Python 3.7.5.

Following on from #7535 (fixed recently by @msullivan), this triggers another crash (really just adding the `frozen=True` from my example on that bug report, though I also dropped the args to `field` since I realised I could):

```
from dataclasses import dataclass, field

@dataclass
class Base:
    foo: bool = field()

@dataclass(frozen=True)
class Derived(Base):
    @property
    def foo(self):
        pass
```

Traceback (seems slightly different from #6715 -- the assertion here is in method _freeze, not method collect_attributes):

```
$ mypy --show-traceback /home/me/minimal3.py
/home/me/minimal3.py:10: error: INTERNAL ERROR -- Please try using mypy master on Github:
https://mypy.rtfd.io/en/latest/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.770+dev.41b40aa4fc7cdeb32f5ee4fec97f5379ebbe795d
Traceback (most recent call last):
  File "/home/me/env/3/moving-target/bin/mypy", line 11, in <module>
    load_entry_point('mypy', 'console_scripts', 'mypy')()
  File "/home/me/dev/mypy/mypy/__main__.py", line 8, in console_entry
    main(None, sys.stdout, sys.stderr)
  File "/home/me/dev/mypy/mypy/main.py", line 89, in main
    res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
  File "/home/me/dev/mypy/mypy/build.py", line 166, in build
    sources, options, alt_lib_path, flush_errors, fscache, stdout, stderr, extra_plugins
  File "/home/me/dev/mypy/mypy/build.py", line 234, in _build
    graph = dispatch(sources, manager, stdout)
  File "/home/me/dev/mypy/mypy/build.py", line 2631, in dispatch
    process_graph(graph, manager)
  File "/home/me/dev/mypy/mypy/build.py", line 2938, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/home/me/dev/mypy/mypy/build.py", line 3031, in process_stale_scc
    mypy.semanal_main.semantic_analysis_for_scc(graph, scc, manager.errors)
  File "/home/me/dev/mypy/mypy/semanal_main.py", line 77, in semantic_analysis_for_scc
    process_top_levels(graph, scc, patches)
  File "/home/me/dev/mypy/mypy/semanal_main.py", line 202, in process_top_levels
    patches)
  File "/home/me/dev/mypy/mypy/semanal_main.py", line 330, in semantic_analyze_target
    active_type=active_type)
  File "/home/me/dev/mypy/mypy/semanal.py", line 363, in refresh_partial
    self.refresh_top_level(node)
  File "/home/me/dev/mypy/mypy/semanal.py", line 374, in refresh_top_level
    self.accept(d)
  File "/home/me/dev/mypy/mypy/semanal.py", line 4682, in accept
    node.accept(self)
  File "/home/me/dev/mypy/mypy/nodes.py", line 939, in accept
    return visitor.visit_class_def(self)
  File "/home/me/dev/mypy/mypy/semanal.py", line 1014, in visit_class_def
    self.analyze_class(defn)
  File "/home/me/dev/mypy/mypy/semanal.py", line 1091, in analyze_class
    self.analyze_class_body_common(defn)
  File "/home/me/dev/mypy/mypy/semanal.py", line 1100, in analyze_class_body_common
    self.apply_class_plugin_hooks(defn)
  File "/home/me/dev/mypy/mypy/semanal.py", line 1146, in apply_class_plugin_hooks
    hook(ClassDefContext(defn, decorator, self))
  File "/home/me/dev/mypy/mypy/plugins/dataclasses.py", line 369, in dataclass_class_maker_callback
    transformer.transform()
  File "/home/me/dev/mypy/mypy/plugins/dataclasses.py", line 182, in transform
    self._freeze(attributes)
  File "/home/me/dev/mypy/mypy/plugins/dataclasses.py", line 355, in _freeze
    assert isinstance(var, Var)
AssertionError: 
/home/me/minimal3.py:10: : note: use --pdb to drop into pdb
```



Discussion:
Is there any workaround that would make this less of a crash but more of a warning ?  warning that could therefore be ignore just like the others ones ?

It's so sad no being able to properly override inherited properties with `@property`, it's becomes very difficult to impose  immutability of the dataclasses without some cumbersome workaround via `__post_init__` and `object.setattr`
In #8976 there was a similar-looking crash when overriding a frozen attribute with a method:
```py
from dataclasses import dataclass


@dataclass(frozen=True)
class Foo:
    attr: int


@dataclass(frozen=True)
class Bar(Foo):
    def attr(self) -> int:
        return 1
```
Isn't this related to https://github.com/python/mypy/pull/13475  ? Well I guess not completely, as for a frozen dataclass, a read-only property should be enough

Resolving patch:
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -244,10 +244,20 @@ def transform(self) -> bool:
                     tvar_def=order_tvar_def,
                 )
 
+        parent_decorator_arguments = []
+        for parent in info.mro[1:-1]:
+            parent_args = parent.metadata.get("dataclass")
+            if parent_args:
+                parent_decorator_arguments.append(parent_args)
+
         if decorator_arguments["frozen"]:
+            if any(not parent["frozen"] for parent in parent_decorator_arguments):
+                ctx.api.fail("Cannot inherit frozen dataclass from a non-frozen one", info)
             self._propertize_callables(attributes, settable=False)
             self._freeze(attributes)
         else:
+            if any(parent["frozen"] for parent in parent_decorator_arguments):
+                ctx.api.fail("Cannot inherit non-frozen dataclass from a frozen one", info)
             self._propertize_callables(attributes)
 
         if decorator_arguments["slots"]:
@@ -446,6 +456,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
         # copy() because we potentially modify all_attrs below and if this code requires debugging
         # we'll have unmodified attrs laying around.
         all_attrs = attrs.copy()
+        known_super_attrs = set()
         for info in cls.info.mro[1:-1]:
             if "dataclass_tag" in info.metadata and "dataclass" not in info.metadata:
                 # We haven't processed the base class yet. Need another pass.
@@ -467,6 +478,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
                     with state.strict_optional_set(ctx.api.options.strict_optional):
                         attr.expand_typevar_from_subtype(ctx.cls.info)
                     known_attrs.add(name)
+                    known_super_attrs.add(name)
                     super_attrs.append(attr)
                 elif all_attrs:
                     # How early in the attribute list an attribute appears is determined by the
@@ -481,6 +493,14 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
             all_attrs = super_attrs + all_attrs
             all_attrs.sort(key=lambda a: a.kw_only)
 
+        for known_super_attr_name in known_super_attrs:
+            sym_node = cls.info.names.get(known_super_attr_name)
+            if sym_node and sym_node.node and not isinstance(sym_node.node, Var):
+                ctx.api.fail(
+                    "Dataclass attribute may only be overridden by another attribute",
+                    sym_node.node,
+                )
+
         # Ensure that arguments without a default don't follow
         # arguments that have a default.
         found_default = False
@@ -515,8 +535,8 @@ def _freeze(self, attributes: list[DataclassAttribute]) -> None:
             sym_node = info.names.get(attr.name)
             if sym_node is not None:
                 var = sym_node.node
-                assert isinstance(var, Var)
-                var.is_property = True
+                if isinstance(var, Var):
+                    var.is_property = True
             else:
                 var = attr.to_var()
                 var.info = info


Test patch:
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -187,6 +187,66 @@ reveal_type(C)  # N: Revealed type is "def (some_int: builtins.int, some_str: bu
 
 [builtins fixtures/dataclasses.pyi]
 
+[case testDataclassIncompatibleOverrides]
+# flags: --python-version 3.7
+from dataclasses import dataclass
+
+@dataclass
+class Base:
+    foo: int
+
+@dataclass
+class BadDerived1(Base):
+    def foo(self) -> int:  # E: Dataclass attribute may only be overridden by another attribute \
+                           # E: Signature of "foo" incompatible with supertype "Base"
+        return 1
+
+@dataclass
+class BadDerived2(Base):
+    @property              # E: Dataclass attribute may only be overridden by another attribute
+    def foo(self) -> int:  # E: Cannot override writeable attribute with read-only property
+        return 2
+
+@dataclass
+class BadDerived3(Base):
+    class foo: pass  # E: Dataclass attribute may only be overridden by another attribute
+[builtins fixtures/dataclasses.pyi]
+
+[case testDataclassMultipleInheritance]
+# flags: --python-version 3.7
+from dataclasses import dataclass
+
+class Unrelated:
+    foo: str
+
+@dataclass
+class Base:
+    bar: int
+
+@dataclass
+class Derived(Base, Unrelated):
+    pass
+
+d = Derived(3)
+reveal_type(d.foo)  # N: Revealed type is "builtins.str"
+reveal_type(d.bar)  # N: Revealed type is "builtins.int"
+[builtins fixtures/dataclasses.pyi]
+
+[case testDataclassIncompatibleFrozenOverride]
+# flags: --python-version 3.7
+from dataclasses import dataclass
+
+@dataclass(frozen=True)
+class Base:
+    foo: int
+
+@dataclass(frozen=True)
+class BadDerived(Base):
+    @property  # E: Dataclass attribute may only be overridden by another attribute
+    def foo(self) -> int:
+        return 3
+[builtins fixtures/dataclasses.pyi]
+
 [case testDataclassesFreezing]
 # flags: --python-version 3.7
 from dataclasses import dataclass
@@ -200,6 +260,28 @@ john.name = 'Ben'  # E: Property "name" defined in "Person" is read-only
 
 [builtins fixtures/dataclasses.pyi]
 
+[case testDataclassesInconsistentFreezing]
+# flags: --python-version 3.7
+from dataclasses import dataclass
+
+@dataclass(frozen=True)
+class FrozenBase:
+    pass
+
+@dataclass
+class BadNormalDerived(FrozenBase):  # E: Cannot inherit non-frozen dataclass from a frozen one
+    pass
+
+@dataclass
+class NormalBase:
+    pass
+
+@dataclass(frozen=True)
+class BadFrozenDerived(NormalBase):  # E: Cannot inherit frozen dataclass from a non-frozen one
+    pass
+
+[builtins fixtures/dataclasses.pyi]
+
 [case testDataclassesFields]
 # flags: --python-version 3.7
 from dataclasses import dataclass, field
@@ -1283,9 +1365,9 @@ from dataclasses import dataclass
 class A:
     foo: int
 
-@dataclass
+@dataclass(frozen=True)
 class B(A):
-    @property
+    @property  # E: Dataclass attribute may only be overridden by another attribute
     def foo(self) -> int: pass
 
 reveal_type(B)  # N: Revealed type is "def (foo: builtins.int) -> __main__.B"