Skip to content

Commit

Permalink
Fix inheritance false positives with dataclasses/attrs (#12411)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JukkaL committed Mar 23, 2022
1 parent 7e09c2a commit f81b228
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 19 deletions.
21 changes: 9 additions & 12 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 4 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -778,7 +779,6 @@ def _add_match_args(ctx: 'mypy.plugin.ClassDefContext',
cls=ctx.cls,
name='__match_args__',
typ=match_args,
final=True,
)


Expand Down
7 changes: 6 additions & 1 deletion mypy/plugins/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
12 changes: 9 additions & 3 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions test-data/unit/check-attr.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
16 changes: 16 additions & 0 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit f81b228

Please sign in to comment.