From 5ed1f0030f7c77800984bf714beb8a5a7db02e63 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 21 Mar 2022 15:26:35 +0000 Subject: [PATCH 1/7] Fix multiple inheritance false positives in dataclasses and attrs classes 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. --- mypy/checker.py | 5 +++-- mypy/plugins/attrs.py | 1 - mypy/plugins/dataclasses.py | 2 +- test-data/unit/check-attr.test | 16 ++++++++++++++++ test-data/unit/check-dataclasses.test | 16 ++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e0f2398b67fc..7239893594a6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2103,8 +2103,9 @@ class C(B, A[int]): ... # this is unsafe because... self.msg.cant_override_final(name, base2.name, ctx) if is_final_node(first.node): self.check_if_final_var_override_writable(name, second.node, ctx) - # __slots__ and __deletable__ are special and the type can vary across class hierarchy. - if name in ('__slots__', '__deletable__'): + # Some attributes like __slots__ and __deletable__ are special, and the type can + # vary across class hierarchy. + if name in ('__slots__', '__deletable__', '__match_args__', '__attrs_attrs__'): ok = True if not ok: self.msg.base_class_definitions_incompatible(name, base1, base2, diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index a82b7d9bdfee..83eb3b2a07af 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -778,7 +778,6 @@ def _add_match_args(ctx: 'mypy.plugin.ClassDefContext', cls=ctx.cls, name='__match_args__', typ=match_args, - final=True, ) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 091c627f5c1b..211f0a8dda83 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -227,7 +227,7 @@ def transform(self) -> None: literals: List[Type] = [LiteralType(attr.name, str_type) for attr in attributes if attr.is_in_init] match_args_type = TupleType(literals, ctx.api.named_type("builtins.tuple")) - add_attribute_to_class(ctx.api, ctx.cls, "__match_args__", match_args_type, final=True) + add_attribute_to_class(ctx.api, ctx.cls, "__match_args__", match_args_type) self._add_dataclass_fields_magic_attribute() diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 1fc811d93dc9..7905f1add6ca 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1539,3 +1539,19 @@ n: NoMatchArgs reveal_type(n.__match_args__) # E: "NoMatchArgs" has no attribute "__match_args__" \ # N: Revealed type is "Any" [builtins fixtures/attr.pyi] + +[case testAttrsMultipleInheritance] +# flags: --python-version 3.10 +import attr + +@attr.s +class A: + x = attr.ib(type=int) + +@attr.s +class B: + y = attr.ib(type=int) + +class AB(A, B): + pass +[builtins fixtures/attr.pyi] diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index eed329bb59c7..83e9d672bf30 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1536,3 +1536,19 @@ A(a=1, b=2) A(1) A(a="foo") # E: Argument "a" to "A" has incompatible type "str"; expected "int" [builtins fixtures/dataclasses.pyi] + +[case testDataclassMultipleInheritance] +# flags: --python-version 3.10 +from dataclasses import dataclass + +@dataclass +class A: + prop_a: str + +@dataclass +class B: + prop_b: bool + +class Derived(A, B): + pass +[builtins fixtures/dataclasses.pyi] From adb470b5ddea975d88dc9a5e8a2fc26eda9c35c3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 22 Mar 2022 13:37:40 +0000 Subject: [PATCH 2/7] Allow plugins to mark magic attributes --- mypy/checker.py | 3 ++- mypy/nodes.py | 5 ++++- mypy/plugins/attrs.py | 1 + test-data/unit/check-dataclasses.test | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7239893594a6..afcdf4defc60 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2105,7 +2105,8 @@ class C(B, A[int]): ... # this is unsafe because... self.check_if_final_var_override_writable(name, second.node, ctx) # Some attributes like __slots__ and __deletable__ are special, and the type can # vary across class hierarchy. - if name in ('__slots__', '__deletable__', '__match_args__', '__attrs_attrs__'): + if name in ('__slots__', '__deletable__', '__match_args__') or ( + isinstance(second.node, Var) and second.node.allow_incompatible_override): ok = True if not ok: self.msg.base_class_definitions_incompatible(name, base1, base2, diff --git a/mypy/nodes.py b/mypy/nodes.py index 7680641e659f..98402ab9b71b 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -853,7 +853,7 @@ def deserialize(cls, data: JsonDict) -> 'Decorator': 'is_classmethod', 'is_property', 'is_settable_property', 'is_suppressed_import', 'is_classvar', 'is_abstract_var', 'is_final', 'final_unset_in_class', 'final_set_in_init', 'explicit_self_type', 'is_ready', 'from_module_getattr', - 'has_explicit_value', + 'has_explicit_value', 'allow_incompatible_override', ] @@ -885,6 +885,7 @@ class Var(SymbolNode): 'explicit_self_type', 'from_module_getattr', 'has_explicit_value', + 'allow_incompatible_override', ) def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None: @@ -932,6 +933,8 @@ def __init__(self, name: str, type: 'Optional[mypy.types.Type]' = None) -> None: # Var can be created with an explicit value `a = 1` or without one `a: int`, # we need a way to tell which one is which. self.has_explicit_value = False + # If True, subclasses can override this with an incompatible type. + self.allow_incompatible_override = False @property def name(self) -> str: diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 83eb3b2a07af..9e4b80c905cf 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -744,6 +744,7 @@ def _add_attrs_magic_attribute(ctx: 'mypy.plugin.ClassDefContext', var.info = ctx.cls.info var.is_classvar = True var._fullname = f"{ctx.cls.fullname}.{MAGIC_ATTR_CLS_NAME}" + var.allow_incompatible_override = True ctx.cls.info.names[MAGIC_ATTR_NAME] = SymbolTableNode( kind=MDEF, node=var, diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index 83e9d672bf30..1773ac0eac8d 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1537,7 +1537,7 @@ A(1) A(a="foo") # E: Argument "a" to "A" has incompatible type "str"; expected "int" [builtins fixtures/dataclasses.pyi] -[case testDataclassMultipleInheritance] +[case testDataclassesMultipleInheritanceWithNonDataclass] # flags: --python-version 3.10 from dataclasses import dataclass From 7fde42ee937dd15ea6a347c03ded9701e19f00b4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 22 Mar 2022 13:57:11 +0000 Subject: [PATCH 3/7] Set inheritance flag in semanal --- mypy/checker.py | 3 +-- mypy/plugins/common.py | 7 ++++++- mypy/semanal.py | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index afcdf4defc60..bdef49ff36fd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2105,8 +2105,7 @@ class C(B, A[int]): ... # this is unsafe because... self.check_if_final_var_override_writable(name, second.node, ctx) # Some attributes like __slots__ and __deletable__ are special, and the type can # vary across class hierarchy. - if name in ('__slots__', '__deletable__', '__match_args__') or ( - isinstance(second.node, Var) and second.node.allow_incompatible_override): + if isinstance(second.node, Var) and second.node.allow_incompatible_override: ok = True if not ok: self.msg.base_class_definitions_incompatible(name, base1, base2, diff --git a/mypy/plugins/common.py b/mypy/plugins/common.py index 40ac03e30a50..f90e44f2b221 100644 --- a/mypy/plugins/common.py +++ b/mypy/plugins/common.py @@ -5,7 +5,7 @@ FuncDef, PassStmt, RefExpr, SymbolTableNode, Var, JsonDict, ) from mypy.plugin import CheckerPluginInterface, ClassDefContext, SemanticAnalyzerPluginInterface -from mypy.semanal import set_callable_name +from mypy.semanal import set_callable_name, ALLOW_INCOMPATIBLE_ATTRS from mypy.types import ( CallableType, Overloaded, Type, TypeVarType, deserialize_type, get_proper_type, ) @@ -163,6 +163,7 @@ def add_attribute_to_class( typ: Type, final: bool = False, no_serialize: bool = False, + override_allow_incompatible: bool = False, ) -> None: """ Adds a new attribute to a class definition. @@ -180,6 +181,10 @@ def add_attribute_to_class( node = Var(name, typ) node.info = info node.is_final = final + if name in ALLOW_INCOMPATIBLE_ATTRS: + node.allow_incompatible_override = True + else: + node.allow_incompatible_override = override_allow_incompatible node._fullname = info.fullname + '.' + name info.names[name] = SymbolTableNode( MDEF, diff --git a/mypy/semanal.py b/mypy/semanal.py index 60127961b60e..07932891abc9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -152,6 +152,9 @@ # available very early on. CORE_BUILTIN_CLASSES: Final = ["object", "bool", "function"] +# Subclasses can override these attributes with incompatible types. +ALLOW_INCOMPATIBLE_ATTRS: Final = ('__slots__', '__deletable__', '__match_args__') + # Used for tracking incomplete references Tag: _TypeAlias = int @@ -2915,6 +2918,7 @@ def make_name_lvalue_var( self, lvalue: NameExpr, kind: int, inferred: bool, has_explicit_value: bool, ) -> Var: """Return a Var node for an lvalue that is a name expression.""" + name = lvalue.name v = Var(lvalue.name) v.set_line(lvalue) v.is_inferred = inferred @@ -2922,6 +2926,7 @@ def make_name_lvalue_var( assert self.type is not None v.info = self.type v.is_initialized_in_class = True + v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_ATTRS if kind != LDEF: v._fullname = self.qualified_name(lvalue.name) else: From fb602a4fc6d42c1867bca723e1f3a3db5061aa56 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 22 Mar 2022 14:02:28 +0000 Subject: [PATCH 4/7] Clean up --- mypy/checker.py | 13 +++---------- mypy/plugins/common.py | 4 ++-- mypy/semanal.py | 6 +++--- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index bdef49ff36fd..e141baf9af0e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2461,16 +2461,9 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[ last_immediate_base = direct_bases[-1] if direct_bases else None for base in lvalue_node.info.mro[1:]: - # Only check __slots__ against the 'object' - # If a base class defines a Tuple of 3 elements, a child of - # this class should not be allowed to define it as a Tuple of - # anything other than 3 elements. The exception to this rule - # is __slots__, where it is allowed for any child class to - # redefine it. - if lvalue_node.name == "__slots__" and base.fullname != "builtins.object": - continue - # We don't care about the type of "__deletable__". - if lvalue_node.name == "__deletable__": + # The type of "__slots__" and some other attributes doesn't need to + # be compatible with a base class. + if isinstance(lvalue_node, Var) and lvalue_node.allow_incompatible_override: continue if is_private(lvalue_node.name): diff --git a/mypy/plugins/common.py b/mypy/plugins/common.py index f90e44f2b221..90db614404bd 100644 --- a/mypy/plugins/common.py +++ b/mypy/plugins/common.py @@ -5,7 +5,7 @@ FuncDef, PassStmt, RefExpr, SymbolTableNode, Var, JsonDict, ) from mypy.plugin import CheckerPluginInterface, ClassDefContext, SemanticAnalyzerPluginInterface -from mypy.semanal import set_callable_name, ALLOW_INCOMPATIBLE_ATTRS +from mypy.semanal import set_callable_name, ALLOW_INCOMPATIBLE_OVERRIDE from mypy.types import ( CallableType, Overloaded, Type, TypeVarType, deserialize_type, get_proper_type, ) @@ -181,7 +181,7 @@ def add_attribute_to_class( node = Var(name, typ) node.info = info node.is_final = final - if name in ALLOW_INCOMPATIBLE_ATTRS: + if name in ALLOW_INCOMPATIBLE_OVERRIDE: node.allow_incompatible_override = True else: node.allow_incompatible_override = override_allow_incompatible diff --git a/mypy/semanal.py b/mypy/semanal.py index 07932891abc9..f58f705c0627 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -152,8 +152,8 @@ # available very early on. CORE_BUILTIN_CLASSES: Final = ["object", "bool", "function"] -# Subclasses can override these attributes with incompatible types. -ALLOW_INCOMPATIBLE_ATTRS: Final = ('__slots__', '__deletable__', '__match_args__') +# Subclasses can override these Var attributes with incompatible types. +ALLOW_INCOMPATIBLE_OVERRIDE: Final = ('__slots__', '__deletable__', '__match_args__') # Used for tracking incomplete references @@ -2926,7 +2926,7 @@ def make_name_lvalue_var( assert self.type is not None v.info = self.type v.is_initialized_in_class = True - v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_ATTRS + v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_OVERRIDE if kind != LDEF: v._fullname = self.qualified_name(lvalue.name) else: From 3c13f6831f008afa3919443c15c7a403ba3ba45b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 22 Mar 2022 14:13:03 +0000 Subject: [PATCH 5/7] Fix __slots__ and update comment --- mypy/checker.py | 8 +++++--- mypy/semanal.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e141baf9af0e..0a0608937538 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2461,9 +2461,11 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[ last_immediate_base = direct_bases[-1] if direct_bases else None for base in lvalue_node.info.mro[1:]: - # The type of "__slots__" and some other attributes doesn't need to - # be compatible with a base class. - if isinstance(lvalue_node, Var) and lvalue_node.allow_incompatible_override: + # The type of "__slots__" and some other attributes usually doesn't need to + # be compatible with a base class. We'll still check the type of "__slots__" + # against "object" as an exception. + if (isinstance(lvalue_node, Var) and lvalue_node.allow_incompatible_override and + not (lvalue_node.name == "__slots__" and base.fullname == "builtins.object")): continue if is_private(lvalue_node.name): diff --git a/mypy/semanal.py b/mypy/semanal.py index f58f705c0627..9bcfb5672dd5 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -152,7 +152,8 @@ # available very early on. CORE_BUILTIN_CLASSES: Final = ["object", "bool", "function"] -# Subclasses can override these Var attributes with incompatible types. +# Subclasses can override these Var attributes with incompatible types. This can also be +# set for individual attributes using 'allow_incompatible_override' of Var. ALLOW_INCOMPATIBLE_OVERRIDE: Final = ('__slots__', '__deletable__', '__match_args__') From a7e2c56114b928000a480c76c5724a6c77e2a9db Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 22 Mar 2022 14:15:11 +0000 Subject: [PATCH 6/7] Fix lint --- mypy/checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0a0608937538..56bcfab1797c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2465,7 +2465,8 @@ def check_compatibility_all_supers(self, lvalue: RefExpr, lvalue_type: Optional[ # be compatible with a base class. We'll still check the type of "__slots__" # against "object" as an exception. if (isinstance(lvalue_node, Var) and lvalue_node.allow_incompatible_override and - not (lvalue_node.name == "__slots__" and base.fullname == "builtins.object")): + not (lvalue_node.name == "__slots__" and + base.fullname == "builtins.object")): continue if is_private(lvalue_node.name): From ff424439dbc61b802eeb4134a801a1c87500b476 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 22 Mar 2022 14:27:31 +0000 Subject: [PATCH 7/7] Minor cleanup --- mypy/semanal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 9bcfb5672dd5..f1122fb7e4bf 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2920,7 +2920,7 @@ def make_name_lvalue_var( ) -> Var: """Return a Var node for an lvalue that is a name expression.""" name = lvalue.name - v = Var(lvalue.name) + v = Var(name) v.set_line(lvalue) v.is_inferred = inferred if kind == MDEF: @@ -2929,10 +2929,10 @@ def make_name_lvalue_var( v.is_initialized_in_class = True v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_OVERRIDE if kind != LDEF: - v._fullname = self.qualified_name(lvalue.name) + v._fullname = self.qualified_name(name) else: # fullanme should never stay None - v._fullname = lvalue.name + v._fullname = name v.is_ready = False # Type not inferred yet v.has_explicit_value = has_explicit_value return v