Skip to content

Commit

Permalink
Add __replace__ for dataclasses in 3.13 (#17469)
Browse files Browse the repository at this point in the history
Fixes #17471
  • Loading branch information
max-muoto committed Jul 3, 2024
1 parent 45afd73 commit cb7b96d
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 0 deletions.
15 changes: 15 additions & 0 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down
34 changes: 34 additions & 0 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit cb7b96d

Please sign in to comment.