diff --git a/mypy/checker.py b/mypy/checker.py index 0a76b76a3f54..b3dea605b6a2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2112,8 +2112,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 isinstance(second.node, Var) and second.node.allow_incompatible_override: ok = True if not ok: self.msg.base_class_definitions_incompatible(name, base1, base2, @@ -2475,16 +2476,12 @@ 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 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/nodes.py b/mypy/nodes.py index f520280dce2d..fbd36b67cbc0 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -852,7 +852,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', ] @@ -884,6 +884,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: @@ -931,6 +932,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 a82b7d9bdfee..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, @@ -778,7 +779,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/common.py b/mypy/plugins/common.py index 40ac03e30a50..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 +from mypy.semanal import set_callable_name, ALLOW_INCOMPATIBLE_OVERRIDE 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_OVERRIDE: + 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/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/mypy/semanal.py b/mypy/semanal.py index 012c1af6f92a..30134d5b1003 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -153,6 +153,10 @@ # available very early on. CORE_BUILTIN_CLASSES: Final = ["object", "bool", "function"] +# 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__') + # Used for tracking incomplete references Tag: _TypeAlias = int @@ -2910,18 +2914,20 @@ 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.""" - v = Var(lvalue.name) + name = lvalue.name + v = Var(name) v.set_line(lvalue) v.is_inferred = inferred if kind == MDEF: assert self.type is not None v.info = self.type 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 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..1773ac0eac8d 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 testDataclassesMultipleInheritanceWithNonDataclass] +# 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]