Skip to content

Commit f81b228

Browse files
committed
Fix inheritance false positives with dataclasses/attrs (#12411)
Multiple inheritance from dataclasses and attrs classes works at runtime, so don't complain about `__match_args__` or `__attrs_attrs__` which tend to have incompatible types in subclasses. Fixes #12349. Fixes #12008. Fixes #12065.
1 parent 7e09c2a commit f81b228

File tree

8 files changed

+62
-19
lines changed

8 files changed

+62
-19
lines changed

mypy/checker.py

+9-12
Original file line numberDiff line numberDiff line change
@@ -2112,8 +2112,9 @@ class C(B, A[int]): ... # this is unsafe because...
21122112
self.msg.cant_override_final(name, base2.name, ctx)
21132113
if is_final_node(first.node):
21142114
self.check_if_final_var_override_writable(name, second.node, ctx)
2115-
# __slots__ and __deletable__ are special and the type can vary across class hierarchy.
2116-
if name in ('__slots__', '__deletable__'):
2115+
# Some attributes like __slots__ and __deletable__ are special, and the type can
2116+
# vary across class hierarchy.
2117+
if isinstance(second.node, Var) and second.node.allow_incompatible_override:
21172118
ok = True
21182119
if not ok:
21192120
self.msg.base_class_definitions_incompatible(name, base1, base2,
@@ -2475,16 +2476,12 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[
24752476
last_immediate_base = direct_bases[-1] if direct_bases else None
24762477

24772478
for base in lvalue_node.info.mro[1:]:
2478-
# Only check __slots__ against the 'object'
2479-
# If a base class defines a Tuple of 3 elements, a child of
2480-
# this class should not be allowed to define it as a Tuple of
2481-
# anything other than 3 elements. The exception to this rule
2482-
# is __slots__, where it is allowed for any child class to
2483-
# redefine it.
2484-
if lvalue_node.name == "__slots__" and base.fullname != "builtins.object":
2485-
continue
2486-
# We don't care about the type of "__deletable__".
2487-
if lvalue_node.name == "__deletable__":
2479+
# The type of "__slots__" and some other attributes usually doesn't need to
2480+
# be compatible with a base class. We'll still check the type of "__slots__"
2481+
# against "object" as an exception.
2482+
if (isinstance(lvalue_node, Var) and lvalue_node.allow_incompatible_override and
2483+
not (lvalue_node.name == "__slots__" and
2484+
base.fullname == "builtins.object")):
24882485
continue
24892486

24902487
if is_private(lvalue_node.name):

mypy/nodes.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ def deserialize(cls, data: JsonDict) -> 'Decorator':
852852
'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import',
853853
'is_classvar', 'is_abstract_var', 'is_final', 'final_unset_in_class', 'final_set_in_init',
854854
'explicit_self_type', 'is_ready', 'from_module_getattr',
855-
'has_explicit_value',
855+
'has_explicit_value', 'allow_incompatible_override',
856856
]
857857

858858

@@ -884,6 +884,7 @@ class Var(SymbolNode):
884884
'explicit_self_type',
885885
'from_module_getattr',
886886
'has_explicit_value',
887+
'allow_incompatible_override',
887888
)
888889

889890
def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None:
@@ -931,6 +932,8 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None:
931932
# Var can be created with an explicit value `a = 1` or without one `a: int`,
932933
# we need a way to tell which one is which.
933934
self.has_explicit_value = False
935+
# If True, subclasses can override this with an incompatible type.
936+
self.allow_incompatible_override = False
934937

935938
@property
936939
def name(self) -> str:

mypy/plugins/attrs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ def _add_attrs_magic_attribute(ctx: 'mypy.plugin.ClassDefContext',
744744
var.info = ctx.cls.info
745745
var.is_classvar = True
746746
var._fullname = f"{ctx.cls.fullname}.{MAGIC_ATTR_CLS_NAME}"
747+
var.allow_incompatible_override = True
747748
ctx.cls.info.names[MAGIC_ATTR_NAME] = SymbolTableNode(
748749
kind=MDEF,
749750
node=var,
@@ -778,7 +779,6 @@ def _add_match_args(ctx: 'mypy.plugin.ClassDefContext',
778779
cls=ctx.cls,
779780
name='__match_args__',
780781
typ=match_args,
781-
final=True,
782782
)
783783

784784

mypy/plugins/common.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
FuncDef, PassStmt, RefExpr, SymbolTableNode, Var, JsonDict,
66
)
77
from mypy.plugin import CheckerPluginInterface, ClassDefContext, SemanticAnalyzerPluginInterface
8-
from mypy.semanal import set_callable_name
8+
from mypy.semanal import set_callable_name, ALLOW_INCOMPATIBLE_OVERRIDE
99
from mypy.types import (
1010
CallableType, Overloaded, Type, TypeVarType, deserialize_type, get_proper_type,
1111
)
@@ -163,6 +163,7 @@ def add_attribute_to_class(
163163
typ: Type,
164164
final: bool = False,
165165
no_serialize: bool = False,
166+
override_allow_incompatible: bool = False,
166167
) -> None:
167168
"""
168169
Adds a new attribute to a class definition.
@@ -180,6 +181,10 @@ def add_attribute_to_class(
180181
node = Var(name, typ)
181182
node.info = info
182183
node.is_final = final
184+
if name in ALLOW_INCOMPATIBLE_OVERRIDE:
185+
node.allow_incompatible_override = True
186+
else:
187+
node.allow_incompatible_override = override_allow_incompatible
183188
node._fullname = info.fullname + '.' + name
184189
info.names[name] = SymbolTableNode(
185190
MDEF,

mypy/plugins/dataclasses.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ def transform(self) -> None:
227227
literals: List[Type] = [LiteralType(attr.name, str_type)
228228
for attr in attributes if attr.is_in_init]
229229
match_args_type = TupleType(literals, ctx.api.named_type("builtins.tuple"))
230-
add_attribute_to_class(ctx.api, ctx.cls, "__match_args__", match_args_type, final=True)
230+
add_attribute_to_class(ctx.api, ctx.cls, "__match_args__", match_args_type)
231231

232232
self._add_dataclass_fields_magic_attribute()
233233

mypy/semanal.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@
153153
# available very early on.
154154
CORE_BUILTIN_CLASSES: Final = ["object", "bool", "function"]
155155

156+
# Subclasses can override these Var attributes with incompatible types. This can also be
157+
# set for individual attributes using 'allow_incompatible_override' of Var.
158+
ALLOW_INCOMPATIBLE_OVERRIDE: Final = ('__slots__', '__deletable__', '__match_args__')
159+
156160

157161
# Used for tracking incomplete references
158162
Tag: _TypeAlias = int
@@ -2910,18 +2914,20 @@ def make_name_lvalue_var(
29102914
self, lvalue: NameExpr, kind: int, inferred: bool, has_explicit_value: bool,
29112915
) -> Var:
29122916
"""Return a Var node for an lvalue that is a name expression."""
2913-
v = Var(lvalue.name)
2917+
name = lvalue.name
2918+
v = Var(name)
29142919
v.set_line(lvalue)
29152920
v.is_inferred = inferred
29162921
if kind == MDEF:
29172922
assert self.type is not None
29182923
v.info = self.type
29192924
v.is_initialized_in_class = True
2925+
v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_OVERRIDE
29202926
if kind != LDEF:
2921-
v._fullname = self.qualified_name(lvalue.name)
2927+
v._fullname = self.qualified_name(name)
29222928
else:
29232929
# fullanme should never stay None
2924-
v._fullname = lvalue.name
2930+
v._fullname = name
29252931
v.is_ready = False # Type not inferred yet
29262932
v.has_explicit_value = has_explicit_value
29272933
return v

test-data/unit/check-attr.test

+16
Original file line numberDiff line numberDiff line change
@@ -1539,3 +1539,19 @@ n: NoMatchArgs
15391539
reveal_type(n.__match_args__) # E: "NoMatchArgs" has no attribute "__match_args__" \
15401540
# N: Revealed type is "Any"
15411541
[builtins fixtures/attr.pyi]
1542+
1543+
[case testAttrsMultipleInheritance]
1544+
# flags: --python-version 3.10
1545+
import attr
1546+
1547+
@attr.s
1548+
class A:
1549+
x = attr.ib(type=int)
1550+
1551+
@attr.s
1552+
class B:
1553+
y = attr.ib(type=int)
1554+
1555+
class AB(A, B):
1556+
pass
1557+
[builtins fixtures/attr.pyi]

test-data/unit/check-dataclasses.test

+16
Original file line numberDiff line numberDiff line change
@@ -1536,3 +1536,19 @@ A(a=1, b=2)
15361536
A(1)
15371537
A(a="foo") # E: Argument "a" to "A" has incompatible type "str"; expected "int"
15381538
[builtins fixtures/dataclasses.pyi]
1539+
1540+
[case testDataclassesMultipleInheritanceWithNonDataclass]
1541+
# flags: --python-version 3.10
1542+
from dataclasses import dataclass
1543+
1544+
@dataclass
1545+
class A:
1546+
prop_a: str
1547+
1548+
@dataclass
1549+
class B:
1550+
prop_b: bool
1551+
1552+
class Derived(A, B):
1553+
pass
1554+
[builtins fixtures/dataclasses.pyi]

0 commit comments

Comments
 (0)