Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inheritance false positives with dataclasses/attrs #12411

Merged
merged 7 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 9 additions & 12 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 @@ -2460,16 +2461,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 @@ -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',
]


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
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 @@ -152,6 +152,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 @@ -2915,18 +2919,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]