Skip to content

Commit d46b01c

Browse files
committed
wip
1 parent ebfea94 commit d46b01c

File tree

2 files changed

+66
-81
lines changed

2 files changed

+66
-81
lines changed

mypy/plugins/dataclasses.py

+54-81
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:
121+
def to_argument(
122+
self, current_info: TypeInfo, *, of: Literal["__init__", "replace", "__post_init__"]
123+
) -> Argument:
122124
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
125+
if of == "__init__":
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(
@@ -1113,20 +1088,18 @@ def is_processed_dataclass(info: TypeInfo | None) -> bool:
11131088
def check_post_init(api: TypeChecker, defn: FuncItem, info: TypeInfo) -> None:
11141089
if defn.type is None:
11151090
return
1116-
1117-
ideal_sig = info.get_method(_INTERNAL_POST_INIT_SYM_NAME)
1118-
if ideal_sig is None or ideal_sig.type is None:
1119-
return
1120-
1121-
# We set it ourself, so it is always fine:
1122-
assert isinstance(ideal_sig.type, ProperType)
1123-
assert isinstance(ideal_sig.type, FunctionLike)
1124-
# Type of `FuncItem` is always `FunctionLike`:
11251091
assert isinstance(defn.type, FunctionLike)
11261092

1093+
ideal_sig_method = info.get_method(_INTERNAL_POST_INIT_SYM_NAME)
1094+
assert ideal_sig_method is not None and ideal_sig_method.type is not None
1095+
ideal_sig = ideal_sig_method.type
1096+
assert isinstance(ideal_sig, ProperType) # we set it ourselves
1097+
assert isinstance(ideal_sig, CallableType)
1098+
ideal_sig = ideal_sig.copy_modified(name="__post_init__")
1099+
11271100
api.check_override(
11281101
override=defn.type,
1129-
original=ideal_sig.type,
1102+
original=ideal_sig,
11301103
name="__post_init__",
11311104
name_in_super="__post_init__",
11321105
supertype="dataclass",

test-data/unit/check-dataclasses.test

+12
Original file line numberDiff line numberDiff line change
@@ -2420,3 +2420,15 @@ class Test(Protocol):
24202420
def reset(self) -> None:
24212421
self.x = DEFAULT
24222422
[builtins fixtures/dataclasses.pyi]
2423+
2424+
[case testProtocolNoCrashOnJoining]
2425+
from dataclasses import dataclass
2426+
from typing import Protocol
2427+
2428+
@dataclass
2429+
class MyDataclass(Protocol): ...
2430+
2431+
a: MyDataclass
2432+
b = [a, a] # trigger joining the types
2433+
2434+
[builtins fixtures/dataclasses.pyi]

0 commit comments

Comments
 (0)