diff --git a/stdlib/@tests/stubtest_allowlists/py313.txt b/stdlib/@tests/stubtest_allowlists/py313.txt index 12553bf684ee..f3df7cc2248c 100644 --- a/stdlib/@tests/stubtest_allowlists/py313.txt +++ b/stdlib/@tests/stubtest_allowlists/py313.txt @@ -2,6 +2,9 @@ # 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 0fcdcbb84d97..98045f819fd2 100644 --- a/stdlib/@tests/stubtest_allowlists/py314.txt +++ b/stdlib/@tests/stubtest_allowlists/py314.txt @@ -45,6 +45,8 @@ importlib.util.Loader.exec_module # See Lib/importlib/_abc.py. Might be defined # Pre-existing errors from Python 3.13 # ==================================== +# No way to express keyword-only ParamSpec +copy.replace # ======= # >= 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): ...