Skip to content

Commit 73607be

Browse files
bpo-41559: Implement PEP 612 - Add ParamSpec and Concatenate to typing (#23702)
1 parent cc3467a commit 73607be

File tree

6 files changed

+381
-75
lines changed

6 files changed

+381
-75
lines changed

Lib/_collections_abc.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ def __subclasshook__(cls, C):
416416
class _CallableGenericAlias(GenericAlias):
417417
""" Represent `Callable[argtypes, resulttype]`.
418418
419-
This sets ``__args__`` to a tuple containing the flattened``argtypes``
419+
This sets ``__args__`` to a tuple containing the flattened ``argtypes``
420420
followed by ``resulttype``.
421421
422422
Example: ``Callable[[int, str], float]`` sets ``__args__`` to
@@ -444,15 +444,15 @@ def __create_ga(cls, origin, args):
444444
return super().__new__(cls, origin, ga_args)
445445

446446
def __repr__(self):
447-
if len(self.__args__) == 2 and self.__args__[0] is Ellipsis:
447+
if _has_special_args(self.__args__):
448448
return super().__repr__()
449449
return (f'collections.abc.Callable'
450450
f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], '
451451
f'{_type_repr(self.__args__[-1])}]')
452452

453453
def __reduce__(self):
454454
args = self.__args__
455-
if not (len(args) == 2 and args[0] is Ellipsis):
455+
if not _has_special_args(args):
456456
args = list(args[:-1]), args[-1]
457457
return _CallableGenericAlias, (Callable, args)
458458

@@ -461,12 +461,28 @@ def __getitem__(self, item):
461461
# rather than the default types.GenericAlias object.
462462
ga = super().__getitem__(item)
463463
args = ga.__args__
464-
t_result = args[-1]
465-
t_args = args[:-1]
466-
args = (t_args, t_result)
464+
# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
465+
if not isinstance(ga.__args__[0], tuple):
466+
t_result = ga.__args__[-1]
467+
t_args = ga.__args__[:-1]
468+
args = (t_args, t_result)
467469
return _CallableGenericAlias(Callable, args)
468470

469471

472+
def _has_special_args(args):
473+
"""Checks if args[0] matches either ``...``, ``ParamSpec`` or
474+
``_ConcatenateGenericAlias`` from typing.py
475+
"""
476+
if len(args) != 2:
477+
return False
478+
obj = args[0]
479+
if obj is Ellipsis:
480+
return True
481+
obj = type(obj)
482+
names = ('ParamSpec', '_ConcatenateGenericAlias')
483+
return obj.__module__ == 'typing' and any(obj.__name__ == name for name in names)
484+
485+
470486
def _type_repr(obj):
471487
"""Return the repr() of an object, special-casing types (internal helper).
472488

Lib/test/test_genericalias.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,27 @@ def __call__(self):
369369
self.assertEqual(c1.__args__, c2.__args__)
370370
self.assertEqual(hash(c1.__args__), hash(c2.__args__))
371371

372+
with self.subTest("Testing ParamSpec uses"):
373+
P = typing.ParamSpec('P')
374+
C1 = Callable[P, T]
375+
# substitution
376+
self.assertEqual(C1[int, str], Callable[[int], str])
377+
self.assertEqual(C1[[int, str], str], Callable[[int, str], str])
378+
self.assertEqual(repr(C1).split(".")[-1], "Callable[~P, ~T]")
379+
self.assertEqual(repr(C1[int, str]).split(".")[-1], "Callable[[int], str]")
380+
381+
C2 = Callable[P, int]
382+
# special case in PEP 612 where
383+
# X[int, str, float] == X[[int, str, float]]
384+
self.assertEqual(C2[int, str, float], C2[[int, str, float]])
385+
self.assertEqual(repr(C2).split(".")[-1], "Callable[~P, int]")
386+
self.assertEqual(repr(C2[int, str]).split(".")[-1], "Callable[[int, str], int]")
387+
388+
with self.subTest("Testing Concatenate uses"):
389+
P = typing.ParamSpec('P')
390+
C1 = Callable[typing.Concatenate[int, P], int]
391+
self.assertEqual(repr(C1), "collections.abc.Callable"
392+
"[typing.Concatenate[int, ~P], int]")
372393

373394
if __name__ == "__main__":
374395
unittest.main()

Lib/test/test_typing.py

Lines changed: 106 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from typing import Pattern, Match
2626
from typing import Annotated, ForwardRef
2727
from typing import TypeAlias
28+
from typing import ParamSpec, Concatenate
2829
import abc
2930
import typing
3031
import weakref
@@ -1130,10 +1131,6 @@ class P(PR[int, T], Protocol[T]):
11301131
PR[int]
11311132
with self.assertRaises(TypeError):
11321133
P[int, str]
1133-
with self.assertRaises(TypeError):
1134-
PR[int, 1]
1135-
with self.assertRaises(TypeError):
1136-
PR[int, ClassVar]
11371134

11381135
class C(PR[int, T]): pass
11391136

@@ -1155,8 +1152,6 @@ class P(PR[int, str], Protocol):
11551152
self.assertIsSubclass(P, PR)
11561153
with self.assertRaises(TypeError):
11571154
PR[int]
1158-
with self.assertRaises(TypeError):
1159-
PR[int, 1]
11601155

11611156
class P1(Protocol, Generic[T]):
11621157
def bar(self, x: T) -> str: ...
@@ -1175,8 +1170,6 @@ def bar(self, x: str) -> str:
11751170
return x
11761171

11771172
self.assertIsInstance(Test(), PSub)
1178-
with self.assertRaises(TypeError):
1179-
PR[int, ClassVar]
11801173

11811174
def test_init_called(self):
11821175
T = TypeVar('T')
@@ -1746,8 +1739,6 @@ def test_extended_generic_rules_eq(self):
17461739
self.assertEqual(typing.Iterable[Tuple[T, T]][T], typing.Iterable[Tuple[T, T]])
17471740
with self.assertRaises(TypeError):
17481741
Tuple[T, int][()]
1749-
with self.assertRaises(TypeError):
1750-
Tuple[T, U][T, ...]
17511742

17521743
self.assertEqual(Union[T, int][int], int)
17531744
self.assertEqual(Union[T, U][int, Union[int, str]], Union[int, str])
@@ -1759,10 +1750,6 @@ class Derived(Base): ...
17591750

17601751
self.assertEqual(Callable[[T], T][KT], Callable[[KT], KT])
17611752
self.assertEqual(Callable[..., List[T]][int], Callable[..., List[int]])
1762-
with self.assertRaises(TypeError):
1763-
Callable[[T], U][..., int]
1764-
with self.assertRaises(TypeError):
1765-
Callable[[T], U][[], int]
17661753

17671754
def test_extended_generic_rules_repr(self):
17681755
T = TypeVar('T')
@@ -4243,6 +4230,111 @@ def test_cannot_subscript(self):
42434230
TypeAlias[int]
42444231

42454232

4233+
class ParamSpecTests(BaseTestCase):
4234+
4235+
def test_basic_plain(self):
4236+
P = ParamSpec('P')
4237+
self.assertEqual(P, P)
4238+
self.assertIsInstance(P, ParamSpec)
4239+
4240+
def test_valid_uses(self):
4241+
P = ParamSpec('P')
4242+
T = TypeVar('T')
4243+
C1 = Callable[P, int]
4244+
self.assertEqual(C1.__args__, (P, int))
4245+
self.assertEqual(C1.__parameters__, (P,))
4246+
C2 = Callable[P, T]
4247+
self.assertEqual(C2.__args__, (P, T))
4248+
self.assertEqual(C2.__parameters__, (P, T))
4249+
# Test collections.abc.Callable too.
4250+
C3 = collections.abc.Callable[P, int]
4251+
self.assertEqual(C3.__args__, (P, int))
4252+
self.assertEqual(C3.__parameters__, (P,))
4253+
C4 = collections.abc.Callable[P, T]
4254+
self.assertEqual(C4.__args__, (P, T))
4255+
self.assertEqual(C4.__parameters__, (P, T))
4256+
4257+
# ParamSpec instances should also have args and kwargs attributes.
4258+
self.assertIn('args', dir(P))
4259+
self.assertIn('kwargs', dir(P))
4260+
P.args
4261+
P.kwargs
4262+
4263+
def test_user_generics(self):
4264+
T = TypeVar("T")
4265+
P = ParamSpec("P")
4266+
P_2 = ParamSpec("P_2")
4267+
4268+
class X(Generic[T, P]):
4269+
f: Callable[P, int]
4270+
x: T
4271+
G1 = X[int, P_2]
4272+
self.assertEqual(G1.__args__, (int, P_2))
4273+
self.assertEqual(G1.__parameters__, (P_2,))
4274+
4275+
G2 = X[int, Concatenate[int, P_2]]
4276+
self.assertEqual(G2.__args__, (int, Concatenate[int, P_2]))
4277+
self.assertEqual(G2.__parameters__, (P_2,))
4278+
4279+
G3 = X[int, [int, bool]]
4280+
self.assertEqual(G3.__args__, (int, (int, bool)))
4281+
self.assertEqual(G3.__parameters__, ())
4282+
4283+
G4 = X[int, ...]
4284+
self.assertEqual(G4.__args__, (int, Ellipsis))
4285+
self.assertEqual(G4.__parameters__, ())
4286+
4287+
class Z(Generic[P]):
4288+
f: Callable[P, int]
4289+
4290+
G5 = Z[[int, str, bool]]
4291+
self.assertEqual(G5.__args__, ((int, str, bool),))
4292+
self.assertEqual(G5.__parameters__, ())
4293+
4294+
G6 = Z[int, str, bool]
4295+
self.assertEqual(G6.__args__, ((int, str, bool),))
4296+
self.assertEqual(G6.__parameters__, ())
4297+
4298+
# G5 and G6 should be equivalent according to the PEP
4299+
self.assertEqual(G5.__args__, G6.__args__)
4300+
self.assertEqual(G5.__origin__, G6.__origin__)
4301+
self.assertEqual(G5.__parameters__, G6.__parameters__)
4302+
self.assertEqual(G5, G6)
4303+
4304+
def test_var_substitution(self):
4305+
T = TypeVar("T")
4306+
P = ParamSpec("P")
4307+
C1 = Callable[P, T]
4308+
self.assertEqual(C1[int, str], Callable[[int], str])
4309+
self.assertEqual(C1[[int, str, dict], float], Callable[[int, str, dict], float])
4310+
4311+
4312+
class ConcatenateTests(BaseTestCase):
4313+
def test_basics(self):
4314+
P = ParamSpec('P')
4315+
class MyClass: ...
4316+
c = Concatenate[MyClass, P]
4317+
self.assertNotEqual(c, Concatenate)
4318+
4319+
def test_valid_uses(self):
4320+
P = ParamSpec('P')
4321+
T = TypeVar('T')
4322+
C1 = Callable[Concatenate[int, P], int]
4323+
self.assertEqual(C1.__args__, (Concatenate[int, P], int))
4324+
self.assertEqual(C1.__parameters__, (P,))
4325+
C2 = Callable[Concatenate[int, T, P], T]
4326+
self.assertEqual(C2.__args__, (Concatenate[int, T, P], T))
4327+
self.assertEqual(C2.__parameters__, (T, P))
4328+
4329+
# Test collections.abc.Callable too.
4330+
C3 = collections.abc.Callable[Concatenate[int, P], int]
4331+
self.assertEqual(C3.__args__, (Concatenate[int, P], int))
4332+
self.assertEqual(C3.__parameters__, (P,))
4333+
C4 = collections.abc.Callable[Concatenate[int, T, P], T]
4334+
self.assertEqual(C4.__args__, (Concatenate[int, T, P], T))
4335+
self.assertEqual(C4.__parameters__, (T, P))
4336+
4337+
42464338
class AllTests(BaseTestCase):
42474339
"""Tests for __all__."""
42484340

0 commit comments

Comments
 (0)