diff --git a/stdlib/@tests/test_cases/check_copy.py b/stdlib/@tests/test_cases/check_copy.py index 7ea25f029944..c9d4fa877e91 100644 --- a/stdlib/@tests/test_cases/check_copy.py +++ b/stdlib/@tests/test_cases/check_copy.py @@ -2,6 +2,7 @@ import copy import sys +from typing import Generic, TypeVar from typing_extensions import Self, assert_type @@ -19,3 +20,20 @@ def __replace__(self, val: int) -> Self: obj = ReplaceableClass(42) cpy = copy.replace(obj, val=23) assert_type(cpy, ReplaceableClass) + + +_T_co = TypeVar("_T_co", covariant=True) + + +class Box(Generic[_T_co]): + def __init__(self, value: _T_co, /) -> None: + self.value = value + + def __replace__(self, value: str) -> Box[str]: + return Box(value) + + +if sys.version_info >= (3, 13): + box1: Box[int] = Box(42) + box2 = copy.replace(box1, val="spam") + assert_type(box2, Box[str]) diff --git a/stdlib/copy.pyi b/stdlib/copy.pyi index 10d2f0ae3710..373899ea2635 100644 --- a/stdlib/copy.pyi +++ b/stdlib/copy.pyi @@ -1,16 +1,15 @@ import sys from typing import Any, Protocol, TypeVar, type_check_only -from typing_extensions import Self __all__ = ["Error", "copy", "deepcopy"] _T = TypeVar("_T") -_SR = TypeVar("_SR", bound=_SupportsReplace) +_RT_co = TypeVar("_RT_co", covariant=True) @type_check_only -class _SupportsReplace(Protocol): - # In reality doesn't support args, but there's no other great way to express this. - def __replace__(self, *args: Any, **kwargs: Any) -> Self: ... +class _SupportsReplace(Protocol[_RT_co]): + # In reality doesn't support args, but there's no great way to express this. + def __replace__(self, /, *_: Any, **changes: Any) -> _RT_co: ... # None in CPython but non-None in Jython PyStringMap: Any @@ -21,7 +20,8 @@ def copy(x: _T) -> _T: ... if sys.version_info >= (3, 13): __all__ += ["replace"] - def replace(obj: _SR, /, **changes: Any) -> _SR: ... + # The types accepted by `**changes` match those of `obj.__replace__`. + def replace(obj: _SupportsReplace[_RT_co], /, **changes: Any) -> _RT_co: ... class Error(Exception): ...