Skip to content

Commit 7a94183

Browse files
authored
Fix dataclass/protocol crash on joining types (#15629)
The root cause is hacky creation of incomplete symbols; instead switching to `add_method_to_class` which does the necessary housekeeping. Fixes #15618.
1 parent 2ebd51e commit 7a94183

File tree

4 files changed

+84
-87
lines changed

4 files changed

+84
-87
lines changed

Diff for: mypy/checker.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1200,9 +1200,10 @@ def check_func_def(
12001200
elif isinstance(arg_type, TypeVarType):
12011201
# Refuse covariant parameter type variables
12021202
# TODO: check recursively for inner type variables
1203-
if arg_type.variance == COVARIANT and defn.name not in (
1204-
"__init__",
1205-
"__new__",
1203+
if (
1204+
arg_type.variance == COVARIANT
1205+
and defn.name not in ("__init__", "__new__", "__post_init__")
1206+
and not is_private(defn.name) # private methods are not inherited
12061207
):
12071208
ctx: Context = arg_type
12081209
if ctx.line < 0:

Diff for: mypy/plugins/dataclasses.py

+55-82
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Final, Iterator
5+
from typing import TYPE_CHECKING, Final, Iterator, Literal
66

77
from mypy import errorcodes, message_registry
88
from mypy.expandtype import expand_type, expand_type_by_instance
@@ -86,7 +86,7 @@
8686
field_specifiers=("dataclasses.Field", "dataclasses.field"),
8787
)
8888
_INTERNAL_REPLACE_SYM_NAME: Final = "__mypy-replace"
89-
_INTERNAL_POST_INIT_SYM_NAME: Final = "__mypy-__post_init__"
89+
_INTERNAL_POST_INIT_SYM_NAME: Final = "__mypy-post_init"
9090

9191

9292
class DataclassAttribute:
@@ -118,14 +118,33 @@ def __init__(
118118
self.is_neither_frozen_nor_nonfrozen = is_neither_frozen_nor_nonfrozen
119119
self._api = api
120120

121-
def to_argument(self, current_info: TypeInfo) -> Argument:
122-
arg_kind = ARG_POS
123-
if self.kw_only and self.has_default:
124-
arg_kind = ARG_NAMED_OPT
125-
elif self.kw_only and not self.has_default:
126-
arg_kind = ARG_NAMED
127-
elif not self.kw_only and self.has_default:
128-
arg_kind = ARG_OPT
121+
def to_argument(
122+
self, current_info: TypeInfo, *, of: Literal["__init__", "replace", "__post_init__"]
123+
) -> Argument:
124+
if of == "__init__":
125+
arg_kind = ARG_POS
126+
if self.kw_only and self.has_default:
127+
arg_kind = ARG_NAMED_OPT
128+
elif self.kw_only and not self.has_default:
129+
arg_kind = ARG_NAMED
130+
elif not self.kw_only and self.has_default:
131+
arg_kind = ARG_OPT
132+
elif of == "replace":
133+
arg_kind = ARG_NAMED if self.is_init_var and not self.has_default else ARG_NAMED_OPT
134+
elif of == "__post_init__":
135+
# We always use `ARG_POS` without a default value, because it is practical.
136+
# Consider this case:
137+
#
138+
# @dataclass
139+
# class My:
140+
# y: dataclasses.InitVar[str] = 'a'
141+
# def __post_init__(self, y: str) -> None: ...
142+
#
143+
# We would be *required* to specify `y: str = ...` if default is added here.
144+
# But, most people won't care about adding default values to `__post_init__`,
145+
# because it is not designed to be called directly, and duplicating default values
146+
# for the sake of type-checking is unpleasant.
147+
arg_kind = ARG_POS
129148
return Argument(
130149
variable=self.to_var(current_info),
131150
type_annotation=self.expand_type(current_info),
@@ -236,7 +255,7 @@ def transform(self) -> bool:
236255
and attributes
237256
):
238257
args = [
239-
attr.to_argument(info)
258+
attr.to_argument(info, of="__init__")
240259
for attr in attributes
241260
if attr.is_in_init and not self._is_kw_only_type(attr.type)
242261
]
@@ -375,70 +394,26 @@ def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) ->
375394
Stashes the signature of 'dataclasses.replace(...)' for this specific dataclass
376395
to be used later whenever 'dataclasses.replace' is called for this dataclass.
377396
"""
378-
arg_types: list[Type] = []
379-
arg_kinds = []
380-
arg_names: list[str | None] = []
381-
382-
info = self._cls.info
383-
for attr in attributes:
384-
attr_type = attr.expand_type(info)
385-
assert attr_type is not None
386-
arg_types.append(attr_type)
387-
arg_kinds.append(
388-
ARG_NAMED if attr.is_init_var and not attr.has_default else ARG_NAMED_OPT
389-
)
390-
arg_names.append(attr.name)
391-
392-
signature = CallableType(
393-
arg_types=arg_types,
394-
arg_kinds=arg_kinds,
395-
arg_names=arg_names,
396-
ret_type=NoneType(),
397-
fallback=self._api.named_type("builtins.function"),
398-
)
399-
400-
info.names[_INTERNAL_REPLACE_SYM_NAME] = SymbolTableNode(
401-
kind=MDEF, node=FuncDef(typ=signature), plugin_generated=True
397+
add_method_to_class(
398+
self._api,
399+
self._cls,
400+
_INTERNAL_REPLACE_SYM_NAME,
401+
args=[attr.to_argument(self._cls.info, of="replace") for attr in attributes],
402+
return_type=NoneType(),
403+
is_staticmethod=True,
402404
)
403405

404406
def _add_internal_post_init_method(self, attributes: list[DataclassAttribute]) -> None:
405-
arg_types: list[Type] = [fill_typevars(self._cls.info)]
406-
arg_kinds = [ARG_POS]
407-
arg_names: list[str | None] = ["self"]
408-
409-
info = self._cls.info
410-
for attr in attributes:
411-
if not attr.is_init_var:
412-
continue
413-
attr_type = attr.expand_type(info)
414-
assert attr_type is not None
415-
arg_types.append(attr_type)
416-
# We always use `ARG_POS` without a default value, because it is practical.
417-
# Consider this case:
418-
#
419-
# @dataclass
420-
# class My:
421-
# y: dataclasses.InitVar[str] = 'a'
422-
# def __post_init__(self, y: str) -> None: ...
423-
#
424-
# We would be *required* to specify `y: str = ...` if default is added here.
425-
# But, most people won't care about adding default values to `__post_init__`,
426-
# because it is not designed to be called directly, and duplicating default values
427-
# for the sake of type-checking is unpleasant.
428-
arg_kinds.append(ARG_POS)
429-
arg_names.append(attr.name)
430-
431-
signature = CallableType(
432-
arg_types=arg_types,
433-
arg_kinds=arg_kinds,
434-
arg_names=arg_names,
435-
ret_type=NoneType(),
436-
fallback=self._api.named_type("builtins.function"),
437-
name="__post_init__",
438-
)
439-
440-
info.names[_INTERNAL_POST_INIT_SYM_NAME] = SymbolTableNode(
441-
kind=MDEF, node=FuncDef(typ=signature), plugin_generated=True
407+
add_method_to_class(
408+
self._api,
409+
self._cls,
410+
_INTERNAL_POST_INIT_SYM_NAME,
411+
args=[
412+
attr.to_argument(self._cls.info, of="__post_init__")
413+
for attr in attributes
414+
if attr.is_init_var
415+
],
416+
return_type=NoneType(),
442417
)
443418

444419
def add_slots(
@@ -1120,20 +1095,18 @@ def is_processed_dataclass(info: TypeInfo | None) -> bool:
11201095
def check_post_init(api: TypeChecker, defn: FuncItem, info: TypeInfo) -> None:
11211096
if defn.type is None:
11221097
return
1123-
1124-
ideal_sig = info.get_method(_INTERNAL_POST_INIT_SYM_NAME)
1125-
if ideal_sig is None or ideal_sig.type is None:
1126-
return
1127-
1128-
# We set it ourself, so it is always fine:
1129-
assert isinstance(ideal_sig.type, ProperType)
1130-
assert isinstance(ideal_sig.type, FunctionLike)
1131-
# Type of `FuncItem` is always `FunctionLike`:
11321098
assert isinstance(defn.type, FunctionLike)
11331099

1100+
ideal_sig_method = info.get_method(_INTERNAL_POST_INIT_SYM_NAME)
1101+
assert ideal_sig_method is not None and ideal_sig_method.type is not None
1102+
ideal_sig = ideal_sig_method.type
1103+
assert isinstance(ideal_sig, ProperType) # we set it ourselves
1104+
assert isinstance(ideal_sig, CallableType)
1105+
ideal_sig = ideal_sig.copy_modified(name="__post_init__")
1106+
11341107
api.check_override(
11351108
override=defn.type,
1136-
original=ideal_sig.type,
1109+
original=ideal_sig,
11371110
name="__post_init__",
11381111
name_in_super="__post_init__",
11391112
supertype="dataclass",

Diff for: test-data/unit/check-dataclasses.test

+23
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,17 @@ s: str = a.bar() # E: Incompatible types in assignment (expression has type "in
744744

745745
[builtins fixtures/dataclasses.pyi]
746746

747+
[case testDataclassGenericCovariant]
748+
from dataclasses import dataclass
749+
from typing import Generic, TypeVar
750+
751+
T_co = TypeVar("T_co", covariant=True)
752+
753+
@dataclass
754+
class MyDataclass(Generic[T_co]):
755+
a: T_co
756+
757+
[builtins fixtures/dataclasses.pyi]
747758

748759
[case testDataclassUntypedGenericInheritance]
749760
# flags: --python-version 3.7
@@ -2449,3 +2460,15 @@ class Test(Protocol):
24492460
def reset(self) -> None:
24502461
self.x = DEFAULT
24512462
[builtins fixtures/dataclasses.pyi]
2463+
2464+
[case testProtocolNoCrashOnJoining]
2465+
from dataclasses import dataclass
2466+
from typing import Protocol
2467+
2468+
@dataclass
2469+
class MyDataclass(Protocol): ...
2470+
2471+
a: MyDataclass
2472+
b = [a, a] # trigger joining the types
2473+
2474+
[builtins fixtures/dataclasses.pyi]

Diff for: test-data/unit/deps.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,7 @@ class B(A):
13881388
<m.A.(abstract)> -> <m.B.__init__>, m
13891389
<m.A.__dataclass_fields__> -> <m.B.__dataclass_fields__>
13901390
<m.A.__init__> -> <m.B.__init__>, m.B.__init__
1391-
<m.A.__mypy-replace> -> <m.B.__mypy-replace>
1391+
<m.A.__mypy-replace> -> <m.B.__mypy-replace>, m.B.__mypy-replace
13921392
<m.A.__new__> -> <m.B.__new__>
13931393
<m.A.x> -> <m.B.x>
13941394
<m.A.y> -> <m.B.y>
@@ -1420,7 +1420,7 @@ class B(A):
14201420
<m.A.__dataclass_fields__> -> <m.B.__dataclass_fields__>
14211421
<m.A.__init__> -> <m.B.__init__>, m.B.__init__
14221422
<m.A.__match_args__> -> <m.B.__match_args__>
1423-
<m.A.__mypy-replace> -> <m.B.__mypy-replace>
1423+
<m.A.__mypy-replace> -> <m.B.__mypy-replace>, m.B.__mypy-replace
14241424
<m.A.__new__> -> <m.B.__new__>
14251425
<m.A.x> -> <m.B.x>
14261426
<m.A.y> -> <m.B.y>

0 commit comments

Comments
 (0)