Skip to content

Commit 89257b5

Browse files
committed
stash in a secret class-private
1 parent 7ed3741 commit 89257b5

File tree

2 files changed

+44
-26
lines changed

2 files changed

+44
-26
lines changed

mypy/plugins/dataclasses.py

+33-26
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Context,
2424
DataclassTransformSpec,
2525
Expression,
26+
FuncDef,
2627
JsonDict,
2728
NameExpr,
2829
Node,
@@ -73,6 +74,7 @@
7374
frozen_default=False,
7475
field_specifiers=("dataclasses.Field", "dataclasses.field"),
7576
)
77+
_INTERNAL_REPLACE_METHOD = "__mypy_replace"
7678

7779

7880
class DataclassAttribute:
@@ -325,6 +327,7 @@ def transform(self) -> bool:
325327
add_attribute_to_class(self._api, self._cls, "__match_args__", match_args_type)
326328

327329
self._add_dataclass_fields_magic_attribute()
330+
self._add_internal_replace_method(attributes)
328331

329332
info.metadata["dataclass"] = {
330333
"attributes": [attr.serialize() for attr in attributes],
@@ -333,6 +336,30 @@ def transform(self) -> bool:
333336

334337
return True
335338

339+
def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) -> None:
340+
arg_types = [Instance(self._cls.info, [])]
341+
arg_kinds = [ARG_POS]
342+
arg_names = [None]
343+
for attr in attributes:
344+
arg_types.append(attr.type)
345+
arg_names.append(attr.name)
346+
arg_kinds.append(
347+
ARG_NAMED_OPT if attr.has_default or not attr.is_init_var else ARG_NAMED
348+
)
349+
350+
signature = CallableType(
351+
arg_types=arg_types,
352+
arg_kinds=arg_kinds,
353+
arg_names=arg_names,
354+
ret_type=Instance(self._cls.info, []),
355+
fallback=self._api.named_type("builtins.function"),
356+
name=f"replace of {self._cls.info.name}",
357+
)
358+
359+
self._cls.info.names[_INTERNAL_REPLACE_METHOD] = SymbolTableNode(
360+
kind=MDEF, node=FuncDef(typ=signature), plugin_generated=True
361+
)
362+
336363
def add_slots(
337364
self, info: TypeInfo, attributes: list[DataclassAttribute], *, correct_version: bool
338365
) -> None:
@@ -797,36 +824,16 @@ def replace_function_sig_callback(ctx: FunctionSigContext) -> CallableType:
797824
obj_type = get_proper_type(obj_type)
798825
if not isinstance(obj_type, Instance):
799826
return ctx.default_signature
800-
inst_type_str = format_type_bare(obj_type)
801827

802-
metadata = obj_type.type.metadata
803-
dataclass = metadata.get("dataclass")
804-
if not dataclass:
828+
repl = obj_type.type.get_method(_INTERNAL_REPLACE_METHOD)
829+
if repl is None:
830+
inst_type_str = format_type_bare(obj_type)
805831
ctx.api.fail(
806832
f'Argument 1 to "replace" has incompatible type "{inst_type_str}"; expected a dataclass',
807833
ctx.context,
808834
)
809835
return ctx.default_signature
810836

811-
arg_names = [None]
812-
arg_kinds = [ARG_POS]
813-
arg_types = [obj_type]
814-
for attr in dataclass["attributes"]:
815-
if not attr["is_in_init"]:
816-
continue
817-
arg_names.append(attr["name"])
818-
arg_kinds.append(
819-
ARG_NAMED if not attr["has_default"] and attr["is_init_var"] else ARG_NAMED_OPT
820-
)
821-
arg_types.append(ctx.api.named_type(attr["type"]))
822-
823-
return ctx.default_signature.copy_modified(
824-
arg_names=arg_names,
825-
arg_kinds=arg_kinds,
826-
arg_types=arg_types,
827-
ret_type=obj_type,
828-
name=f"{ctx.default_signature.name} of {inst_type_str}",
829-
# prevent 'dataclasses.pyi:...: note: "replace" of "A" defined here' notes
830-
# since they are misleading: the definition is dynamic, not from a definition
831-
definition=None,
832-
)
837+
repl_type = repl.type
838+
assert isinstance(repl_type, CallableType)
839+
return repl_type

test-data/unit/check-dataclasses.test

+11
Original file line numberDiff line numberDiff line change
@@ -2024,3 +2024,14 @@ a2 = replace(a, q='42') # E: Argument "q" to "replace" of "A" has incompatible
20242024
reveal_type(a2) # N: Revealed type is "__main__.A"
20252025

20262026
[builtins fixtures/dataclasses.pyi]
2027+
[case testReplaceNotDataclass]
2028+
from dataclasses import replace
2029+
2030+
replace(5) # E: Argument 1 to "replace" has incompatible type "int"; expected a dataclass
2031+
2032+
class C:
2033+
pass
2034+
2035+
replace(C()) # E: Argument 1 to "replace" has incompatible type "C"; expected a dataclass
2036+
2037+
[builtins fixtures/dataclasses.pyi]

0 commit comments

Comments
 (0)