Skip to content

Commit 3f0ac21

Browse files
authored
Improve error handling for dataclass inheritance (#13531)
This pull request: 1. Fixes #8334. Overriding a dataclass attribute with a method or property now results in an error message, not a crash. (Overriding an attribute with a non-attribute at runtime will result in either inconsistent behavior or an exception, so I think unconditionally disallowing this is fine.) 2. Makes mypy report an error if you try subclassing a frozen dataclass with a non-frozen one or vice versa. Attempting to do this subclassing at runtime will raise a TypeError.
1 parent b06beab commit 3f0ac21

File tree

2 files changed

+106
-4
lines changed

2 files changed

+106
-4
lines changed

mypy/plugins/dataclasses.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,20 @@ def transform(self) -> bool:
244244
tvar_def=order_tvar_def,
245245
)
246246

247+
parent_decorator_arguments = []
248+
for parent in info.mro[1:-1]:
249+
parent_args = parent.metadata.get("dataclass")
250+
if parent_args:
251+
parent_decorator_arguments.append(parent_args)
252+
247253
if decorator_arguments["frozen"]:
254+
if any(not parent["frozen"] for parent in parent_decorator_arguments):
255+
ctx.api.fail("Cannot inherit frozen dataclass from a non-frozen one", info)
248256
self._propertize_callables(attributes, settable=False)
249257
self._freeze(attributes)
250258
else:
259+
if any(parent["frozen"] for parent in parent_decorator_arguments):
260+
ctx.api.fail("Cannot inherit non-frozen dataclass from a frozen one", info)
251261
self._propertize_callables(attributes)
252262

253263
if decorator_arguments["slots"]:
@@ -463,6 +473,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
463473
# copy() because we potentially modify all_attrs below and if this code requires debugging
464474
# we'll have unmodified attrs laying around.
465475
all_attrs = attrs.copy()
476+
known_super_attrs = set()
466477
for info in cls.info.mro[1:-1]:
467478
if "dataclass_tag" in info.metadata and "dataclass" not in info.metadata:
468479
# We haven't processed the base class yet. Need another pass.
@@ -484,6 +495,7 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
484495
with state.strict_optional_set(ctx.api.options.strict_optional):
485496
attr.expand_typevar_from_subtype(ctx.cls.info)
486497
known_attrs.add(name)
498+
known_super_attrs.add(name)
487499
super_attrs.append(attr)
488500
elif all_attrs:
489501
# How early in the attribute list an attribute appears is determined by the
@@ -498,6 +510,14 @@ def collect_attributes(self) -> list[DataclassAttribute] | None:
498510
all_attrs = super_attrs + all_attrs
499511
all_attrs.sort(key=lambda a: a.kw_only)
500512

513+
for known_super_attr_name in known_super_attrs:
514+
sym_node = cls.info.names.get(known_super_attr_name)
515+
if sym_node and sym_node.node and not isinstance(sym_node.node, Var):
516+
ctx.api.fail(
517+
"Dataclass attribute may only be overridden by another attribute",
518+
sym_node.node,
519+
)
520+
501521
# Ensure that arguments without a default don't follow
502522
# arguments that have a default.
503523
found_default = False
@@ -532,8 +552,8 @@ def _freeze(self, attributes: list[DataclassAttribute]) -> None:
532552
sym_node = info.names.get(attr.name)
533553
if sym_node is not None:
534554
var = sym_node.node
535-
assert isinstance(var, Var)
536-
var.is_property = True
555+
if isinstance(var, Var):
556+
var.is_property = True
537557
else:
538558
var = attr.to_var()
539559
var.info = info

test-data/unit/check-dataclasses.test

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,66 @@ reveal_type(C) # N: Revealed type is "def (some_int: builtins.int, some_str: bu
187187

188188
[builtins fixtures/dataclasses.pyi]
189189

190+
[case testDataclassIncompatibleOverrides]
191+
# flags: --python-version 3.7
192+
from dataclasses import dataclass
193+
194+
@dataclass
195+
class Base:
196+
foo: int
197+
198+
@dataclass
199+
class BadDerived1(Base):
200+
def foo(self) -> int: # E: Dataclass attribute may only be overridden by another attribute \
201+
# E: Signature of "foo" incompatible with supertype "Base"
202+
return 1
203+
204+
@dataclass
205+
class BadDerived2(Base):
206+
@property # E: Dataclass attribute may only be overridden by another attribute
207+
def foo(self) -> int: # E: Cannot override writeable attribute with read-only property
208+
return 2
209+
210+
@dataclass
211+
class BadDerived3(Base):
212+
class foo: pass # E: Dataclass attribute may only be overridden by another attribute
213+
[builtins fixtures/dataclasses.pyi]
214+
215+
[case testDataclassMultipleInheritance]
216+
# flags: --python-version 3.7
217+
from dataclasses import dataclass
218+
219+
class Unrelated:
220+
foo: str
221+
222+
@dataclass
223+
class Base:
224+
bar: int
225+
226+
@dataclass
227+
class Derived(Base, Unrelated):
228+
pass
229+
230+
d = Derived(3)
231+
reveal_type(d.foo) # N: Revealed type is "builtins.str"
232+
reveal_type(d.bar) # N: Revealed type is "builtins.int"
233+
[builtins fixtures/dataclasses.pyi]
234+
235+
[case testDataclassIncompatibleFrozenOverride]
236+
# flags: --python-version 3.7
237+
from dataclasses import dataclass
238+
239+
@dataclass(frozen=True)
240+
class Base:
241+
foo: int
242+
243+
@dataclass(frozen=True)
244+
class BadDerived(Base):
245+
@property # E: Dataclass attribute may only be overridden by another attribute
246+
def foo(self) -> int:
247+
return 3
248+
[builtins fixtures/dataclasses.pyi]
249+
190250
[case testDataclassesFreezing]
191251
# flags: --python-version 3.7
192252
from dataclasses import dataclass
@@ -200,6 +260,28 @@ john.name = 'Ben' # E: Property "name" defined in "Person" is read-only
200260

201261
[builtins fixtures/dataclasses.pyi]
202262

263+
[case testDataclassesInconsistentFreezing]
264+
# flags: --python-version 3.7
265+
from dataclasses import dataclass
266+
267+
@dataclass(frozen=True)
268+
class FrozenBase:
269+
pass
270+
271+
@dataclass
272+
class BadNormalDerived(FrozenBase): # E: Cannot inherit non-frozen dataclass from a frozen one
273+
pass
274+
275+
@dataclass
276+
class NormalBase:
277+
pass
278+
279+
@dataclass(frozen=True)
280+
class BadFrozenDerived(NormalBase): # E: Cannot inherit frozen dataclass from a non-frozen one
281+
pass
282+
283+
[builtins fixtures/dataclasses.pyi]
284+
203285
[case testDataclassesFields]
204286
# flags: --python-version 3.7
205287
from dataclasses import dataclass, field
@@ -1283,9 +1365,9 @@ from dataclasses import dataclass
12831365
class A:
12841366
foo: int
12851367

1286-
@dataclass
1368+
@dataclass(frozen=True)
12871369
class B(A):
1288-
@property
1370+
@property # E: Dataclass attribute may only be overridden by another attribute
12891371
def foo(self) -> int: pass
12901372

12911373
reveal_type(B) # N: Revealed type is "def (foo: builtins.int) -> __main__.B"

0 commit comments

Comments
 (0)