Skip to content

Add __replace__ for dataclasses #17469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think theoretically we could make this not error and make it return a Gen[str], but that seems tricky to get right in general (what if there are multiple attributes?). We can omit it for now and add it later if people ask for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I agree, I think for now it makes sense to keep like this to keep things simple, and we can revisit.


[builtins fixtures/tuple.pyi]
Loading