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

mypy/checker.py

Lines changed: 4 additions & 3 deletions
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:

mypy/plugins/dataclasses.py

Lines changed: 55 additions & 82 deletions
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",

test-data/unit/check-dataclasses.test

Lines changed: 23 additions & 0 deletions
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]

test-data/unit/deps.test

Lines changed: 2 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy