diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index dd2eceab217f..edfc6840fc37 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -385,6 +385,9 @@ def transform(self) -> bool: self._add_dataclass_fields_magic_attribute() self._add_internal_replace_method(attributes) + if self._api.options.python_version >= (3, 13): + self._add_dunder_replace(attributes) + if "__post_init__" in info.names: self._add_internal_post_init_method(attributes) @@ -395,6 +398,18 @@ def transform(self) -> bool: return True + def _add_dunder_replace(self, attributes: list[DataclassAttribute]) -> None: + """Add a `__replace__` method to the class, which is used to replace attributes in the `copy` module.""" + args = [attr.to_argument(self._cls.info, of="replace") for attr in attributes] + type_vars = [tv for tv in self._cls.type_vars] + add_method_to_class( + self._api, + self._cls, + "__replace__", + args=args, + return_type=Instance(self._cls.info, type_vars), + ) + def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) -> None: """ Stashes the signature of 'dataclasses.replace(...)' for this specific dataclass diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index f26ccd9a4854..0f726242b25b 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -2489,3 +2489,37 @@ class Base: class Child(Base): y: int [builtins fixtures/dataclasses.pyi] + +[case testDunderReplacePresent] +# flags: --python-version 3.13 +from dataclasses import dataclass + +@dataclass +class Coords: + x: int + y: int + + +replaced = Coords(2, 4).__replace__(x=2, y=5) +reveal_type(replaced) # N: Revealed type is "__main__.Coords" + +replaced = Coords(2, 4).__replace__(x=2) +reveal_type(replaced) # N: Revealed type is "__main__.Coords" + +Coords(2, 4).__replace__(x="asdf") # E: Argument "x" to "__replace__" of "Coords" has incompatible type "str"; expected "int" +Coords(2, 4).__replace__(23) # E: Too many positional arguments for "__replace__" of "Coords" +Coords(2, 4).__replace__(23, 25) # E: Too many positional arguments for "__replace__" of "Coords" +Coords(2, 4).__replace__(x=23, y=25, z=42) # E: Unexpected keyword argument "z" for "__replace__" of "Coords" + +from typing import Generic, TypeVar +T = TypeVar('T') + +@dataclass +class Gen(Generic[T]): + x: T + +replaced_2 = Gen(2).__replace__(x=2) +reveal_type(replaced_2) # N: Revealed type is "__main__.Gen[builtins.int]" +Gen(2).__replace__(x="not an int") # E: Argument "x" to "__replace__" of "Gen" has incompatible type "str"; expected "int" + +[builtins fixtures/tuple.pyi]