diff --git a/stdlib/@tests/test_cases/check_copy.py b/stdlib/@tests/test_cases/check_copy.py new file mode 100644 index 000000000000..7ea25f029944 --- /dev/null +++ b/stdlib/@tests/test_cases/check_copy.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import copy +import sys +from typing_extensions import Self, assert_type + + +class ReplaceableClass: + def __init__(self, val: int) -> None: + self.val = val + + def __replace__(self, val: int) -> Self: + cpy = copy.copy(self) + cpy.val = val + return cpy + + +if sys.version_info >= (3, 13): + obj = ReplaceableClass(42) + cpy = copy.replace(obj, val=23) + assert_type(cpy, ReplaceableClass) diff --git a/stdlib/copy.pyi b/stdlib/copy.pyi index 8a2dcc508e5d..020ce6c31b58 100644 --- a/stdlib/copy.pyi +++ b/stdlib/copy.pyi @@ -1,8 +1,16 @@ -from typing import Any, TypeVar +import sys +from typing import Any, Protocol, TypeVar +from typing_extensions import ParamSpec, Self __all__ = ["Error", "copy", "deepcopy"] _T = TypeVar("_T") +_SR = TypeVar("_SR", bound=_SupportsReplace[Any]) +_P = ParamSpec("_P") + +class _SupportsReplace(Protocol[_P]): + # In reality doesn't support args, but there's no other great way to express this. + def __replace__(self, *args: _P.args, **kwargs: _P.kwargs) -> Self: ... # None in CPython but non-None in Jython PyStringMap: Any @@ -11,6 +19,10 @@ PyStringMap: Any def deepcopy(x: _T, memo: dict[int, Any] | None = None, _nil: Any = []) -> _T: ... def copy(x: _T) -> _T: ... +if sys.version_info >= (3, 13): + __all__ += ["replace"] + def replace(obj: _SR, /, **changes: Any) -> _SR: ... + class Error(Exception): ... error = Error