Skip to content

Commit cb7b96d

Browse files
authored
Add __replace__ for dataclasses in 3.13 (#17469)
Fixes #17471
1 parent 45afd73 commit cb7b96d

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

mypy/plugins/dataclasses.py

+15
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,9 @@ def transform(self) -> bool:
385385

386386
self._add_dataclass_fields_magic_attribute()
387387
self._add_internal_replace_method(attributes)
388+
if self._api.options.python_version >= (3, 13):
389+
self._add_dunder_replace(attributes)
390+
388391
if "__post_init__" in info.names:
389392
self._add_internal_post_init_method(attributes)
390393

@@ -395,6 +398,18 @@ def transform(self) -> bool:
395398

396399
return True
397400

401+
def _add_dunder_replace(self, attributes: list[DataclassAttribute]) -> None:
402+
"""Add a `__replace__` method to the class, which is used to replace attributes in the `copy` module."""
403+
args = [attr.to_argument(self._cls.info, of="replace") for attr in attributes]
404+
type_vars = [tv for tv in self._cls.type_vars]
405+
add_method_to_class(
406+
self._api,
407+
self._cls,
408+
"__replace__",
409+
args=args,
410+
return_type=Instance(self._cls.info, type_vars),
411+
)
412+
398413
def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) -> None:
399414
"""
400415
Stashes the signature of 'dataclasses.replace(...)' for this specific dataclass

test-data/unit/check-dataclasses.test

+34
Original file line numberDiff line numberDiff line change
@@ -2489,3 +2489,37 @@ class Base:
24892489
class Child(Base):
24902490
y: int
24912491
[builtins fixtures/dataclasses.pyi]
2492+
2493+
[case testDunderReplacePresent]
2494+
# flags: --python-version 3.13
2495+
from dataclasses import dataclass
2496+
2497+
@dataclass
2498+
class Coords:
2499+
x: int
2500+
y: int
2501+
2502+
2503+
replaced = Coords(2, 4).__replace__(x=2, y=5)
2504+
reveal_type(replaced) # N: Revealed type is "__main__.Coords"
2505+
2506+
replaced = Coords(2, 4).__replace__(x=2)
2507+
reveal_type(replaced) # N: Revealed type is "__main__.Coords"
2508+
2509+
Coords(2, 4).__replace__(x="asdf") # E: Argument "x" to "__replace__" of "Coords" has incompatible type "str"; expected "int"
2510+
Coords(2, 4).__replace__(23) # E: Too many positional arguments for "__replace__" of "Coords"
2511+
Coords(2, 4).__replace__(23, 25) # E: Too many positional arguments for "__replace__" of "Coords"
2512+
Coords(2, 4).__replace__(x=23, y=25, z=42) # E: Unexpected keyword argument "z" for "__replace__" of "Coords"
2513+
2514+
from typing import Generic, TypeVar
2515+
T = TypeVar('T')
2516+
2517+
@dataclass
2518+
class Gen(Generic[T]):
2519+
x: T
2520+
2521+
replaced_2 = Gen(2).__replace__(x=2)
2522+
reveal_type(replaced_2) # N: Revealed type is "__main__.Gen[builtins.int]"
2523+
Gen(2).__replace__(x="not an int") # E: Argument "x" to "__replace__" of "Gen" has incompatible type "str"; expected "int"
2524+
2525+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)