diff --git a/stdlib/@tests/stubtest_allowlists/py313.txt b/stdlib/@tests/stubtest_allowlists/py313.txt index 3929791d4ef4..a40b3dc2e55f 100644 --- a/stdlib/@tests/stubtest_allowlists/py313.txt +++ b/stdlib/@tests/stubtest_allowlists/py313.txt @@ -1,7 +1,8 @@ # ========================= # New errors in Python 3.13 # ========================= - +# No way to express keyword-only ParamSpec +copy.replace # ==================================== # Pre-existing errors from Python 3.12 # ==================================== diff --git a/stdlib/@tests/stubtest_allowlists/py314.txt b/stdlib/@tests/stubtest_allowlists/py314.txt index 64a6fa7d2752..526b96ee729b 100644 --- a/stdlib/@tests/stubtest_allowlists/py314.txt +++ b/stdlib/@tests/stubtest_allowlists/py314.txt @@ -35,6 +35,8 @@ multiprocessing.process.BaseProcess.__init__ # Pre-existing errors from Python 3.13 # ==================================== +# No way to express keyword-only ParamSpec +copy.replace # ======= # >= 3.12 diff --git a/stdlib/@tests/stubtest_allowlists/py315.txt b/stdlib/@tests/stubtest_allowlists/py315.txt index b225bfc4924a..bd323b2cdcbf 100644 --- a/stdlib/@tests/stubtest_allowlists/py315.txt +++ b/stdlib/@tests/stubtest_allowlists/py315.txt @@ -175,6 +175,8 @@ types.MappingProxyType.get typing_extensions.Protocol # Super-special typing primitive +copy.replace # Cannot have keyword-only ParamSpec + # ============================================================= # Allowlist entries that cannot or should not be fixed; >= 3.12 diff --git a/stdlib/@tests/test_cases/check_copy.py b/stdlib/@tests/test_cases/check_copy.py index c9d4fa877e91..59896851d737 100644 --- a/stdlib/@tests/test_cases/check_copy.py +++ b/stdlib/@tests/test_cases/check_copy.py @@ -10,7 +10,7 @@ class ReplaceableClass: def __init__(self, val: int) -> None: self.val = val - def __replace__(self, val: int) -> Self: + def __replace__(self, /, *, val: int) -> Self: cpy = copy.copy(self) cpy.val = val return cpy @@ -29,11 +29,11 @@ class Box(Generic[_T_co]): def __init__(self, value: _T_co, /) -> None: self.value = value - def __replace__(self, value: str) -> Box[str]: + 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") + box2 = copy.replace(box1, value="spam") assert_type(box2, Box[str]) diff --git a/stdlib/copy.pyi b/stdlib/copy.pyi index 373899ea2635..aec0d031f8b6 100644 --- a/stdlib/copy.pyi +++ b/stdlib/copy.pyi @@ -1,15 +1,17 @@ import sys from typing import Any, Protocol, TypeVar, type_check_only +from typing_extensions import ParamSpec __all__ = ["Error", "copy", "deepcopy"] _T = TypeVar("_T") _RT_co = TypeVar("_RT_co", covariant=True) +_P = ParamSpec("_P") @type_check_only -class _SupportsReplace(Protocol[_RT_co]): +class _SupportsReplace(Protocol[_P, _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: ... + def __replace__(self, /, *_: _P.args, **changes: _P.kwargs) -> _RT_co: ... # None in CPython but non-None in Jython PyStringMap: Any @@ -20,8 +22,10 @@ def copy(x: _T) -> _T: ... if sys.version_info >= (3, 13): __all__ += ["replace"] - # The types accepted by `**changes` match those of `obj.__replace__`. - def replace(obj: _SupportsReplace[_RT_co], /, **changes: Any) -> _RT_co: ... + + def replace( + obj: _SupportsReplace[_P, _RT_co], /, *_: _P.args, **changes: _P.kwargs # does not accept positional arguments at runtime + ) -> _RT_co: ... class Error(Exception): ...