Skip to content

Commit b5107a9

Browse files
authored
Fix crash in dataclass protocol with self attribute assignment (#15157)
Fix #15004 FWIW I don't think dataclass protocols make much sense, but we definitely should not crash. Also the root cause has nothing to do with dataclasses, the problem is that a self attribute assignment in a protocol created a new `Var` (after an original `Var` was created in class body), which is obviously wrong.
1 parent 9f69bea commit b5107a9

File tree

3 files changed

+37
-5
lines changed

3 files changed

+37
-5
lines changed

mypy/semanal.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -3647,7 +3647,7 @@ def analyze_lvalue(
36473647
has_explicit_value=has_explicit_value,
36483648
)
36493649
elif isinstance(lval, MemberExpr):
3650-
self.analyze_member_lvalue(lval, explicit_type, is_final)
3650+
self.analyze_member_lvalue(lval, explicit_type, is_final, has_explicit_value)
36513651
if explicit_type and not self.is_self_member_ref(lval):
36523652
self.fail("Type cannot be declared in assignment to non-self attribute", lval)
36533653
elif isinstance(lval, IndexExpr):
@@ -3824,7 +3824,9 @@ def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, explicit_type: bool = Fa
38243824
has_explicit_value=True,
38253825
)
38263826

3827-
def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool, is_final: bool) -> None:
3827+
def analyze_member_lvalue(
3828+
self, lval: MemberExpr, explicit_type: bool, is_final: bool, has_explicit_value: bool
3829+
) -> None:
38283830
"""Analyze lvalue that is a member expression.
38293831
38303832
Arguments:
@@ -3853,12 +3855,18 @@ def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool, is_final:
38533855
and explicit_type
38543856
):
38553857
self.attribute_already_defined(lval.name, lval, cur_node)
3856-
# If the attribute of self is not defined in superclasses, create a new Var, ...
3858+
if self.type.is_protocol and has_explicit_value and cur_node is not None:
3859+
# Make this variable non-abstract, it would be safer to do this only if we
3860+
# are inside __init__, but we do this always to preserve historical behaviour.
3861+
if isinstance(cur_node.node, Var):
3862+
cur_node.node.is_abstract_var = False
38573863
if (
3864+
# If the attribute of self is not defined, create a new Var, ...
38583865
node is None
3859-
or (isinstance(node.node, Var) and node.node.is_abstract_var)
3866+
# ... or if it is defined as abstract in a *superclass*.
3867+
or (cur_node is None and isinstance(node.node, Var) and node.node.is_abstract_var)
38603868
# ... also an explicit declaration on self also creates a new Var.
3861-
# Note that `explicit_type` might has been erased for bare `Final`,
3869+
# Note that `explicit_type` might have been erased for bare `Final`,
38623870
# so we also check if `is_final` is passed.
38633871
or (cur_node is None and (explicit_type or is_final))
38643872
):

test-data/unit/check-dataclasses.test

+13
Original file line numberDiff line numberDiff line change
@@ -2037,3 +2037,16 @@ Foo(
20372037
present_5=5,
20382038
)
20392039
[builtins fixtures/dataclasses.pyi]
2040+
2041+
[case testProtocolNoCrash]
2042+
from typing import Protocol, Union, ClassVar
2043+
from dataclasses import dataclass, field
2044+
2045+
DEFAULT = 0
2046+
2047+
@dataclass
2048+
class Test(Protocol):
2049+
x: int
2050+
def reset(self) -> None:
2051+
self.x = DEFAULT
2052+
[builtins fixtures/dataclasses.pyi]

test-data/unit/check-protocols.test

+11
Original file line numberDiff line numberDiff line change
@@ -4054,3 +4054,14 @@ class P(Protocol):
40544054

40554055
[file lib.py]
40564056
class C: ...
4057+
4058+
[case testAllowDefaultConstructorInProtocols]
4059+
from typing import Protocol
4060+
4061+
class P(Protocol):
4062+
x: int
4063+
def __init__(self, x: int) -> None:
4064+
self.x = x
4065+
4066+
class C(P): ...
4067+
C(0) # OK

0 commit comments

Comments
 (0)