Skip to content

Commit eea32d0

Browse files
[copy] Type replace more strongly (#14819)
1 parent 6573de7 commit eea32d0

5 files changed

Lines changed: 17 additions & 8 deletions

File tree

stdlib/@tests/stubtest_allowlists/py313.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# =========================
22
# New errors in Python 3.13
33
# =========================
4-
4+
# No way to express keyword-only ParamSpec
5+
copy.replace
56
# ====================================
67
# Pre-existing errors from Python 3.12
78
# ====================================

stdlib/@tests/stubtest_allowlists/py314.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ threading.Condition.locked
2828
# Pre-existing errors from Python 3.13
2929
# ====================================
3030

31+
# No way to express keyword-only ParamSpec
32+
copy.replace
3133

3234
# =======
3335
# >= 3.12

stdlib/@tests/stubtest_allowlists/py315.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ types.MappingProxyType.get
127127

128128
typing_extensions.Protocol # Super-special typing primitive
129129

130+
copy.replace # Cannot have keyword-only ParamSpec
131+
130132

131133
# =============================================================
132134
# Allowlist entries that cannot or should not be fixed; >= 3.12

stdlib/@tests/test_cases/check_copy.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class ReplaceableClass:
1010
def __init__(self, val: int) -> None:
1111
self.val = val
1212

13-
def __replace__(self, val: int) -> Self:
13+
def __replace__(self, /, *, val: int) -> Self:
1414
cpy = copy.copy(self)
1515
cpy.val = val
1616
return cpy
@@ -29,11 +29,11 @@ class Box(Generic[_T_co]):
2929
def __init__(self, value: _T_co, /) -> None:
3030
self.value = value
3131

32-
def __replace__(self, value: str) -> Box[str]:
32+
def __replace__(self, /, *, value: str) -> Box[str]:
3333
return Box(value)
3434

3535

3636
if sys.version_info >= (3, 13):
3737
box1: Box[int] = Box(42)
38-
box2 = copy.replace(box1, val="spam")
38+
box2 = copy.replace(box1, value="spam")
3939
assert_type(box2, Box[str])

stdlib/copy.pyi

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import sys
22
from typing import Any, Protocol, TypeVar, type_check_only
3+
from typing_extensions import ParamSpec
34

45
__all__ = ["Error", "copy", "deepcopy"]
56

67
_T = TypeVar("_T")
78
_RT_co = TypeVar("_RT_co", covariant=True)
9+
_P = ParamSpec("_P")
810

911
@type_check_only
10-
class _SupportsReplace(Protocol[_RT_co]):
12+
class _SupportsReplace(Protocol[_P, _RT_co]):
1113
# In reality doesn't support args, but there's no great way to express this.
12-
def __replace__(self, /, *_: Any, **changes: Any) -> _RT_co: ...
14+
def __replace__(self, /, *_: _P.args, **changes: _P.kwargs) -> _RT_co: ...
1315

1416
# None in CPython but non-None in Jython
1517
PyStringMap: Any
@@ -25,8 +27,10 @@ else:
2527

2628
if sys.version_info >= (3, 13):
2729
__all__ += ["replace"]
28-
# The types accepted by `**changes` match those of `obj.__replace__`.
29-
def replace(obj: _SupportsReplace[_RT_co], /, **changes: Any) -> _RT_co: ...
30+
31+
def replace(
32+
obj: _SupportsReplace[_P, _RT_co], /, *_: _P.args, **changes: _P.kwargs # does not accept positional arguments at runtime
33+
) -> _RT_co: ...
3034

3135
class Error(Exception): ...
3236

0 commit comments

Comments
 (0)