Skip to content

Commit ecfacc3

Browse files
bpo-44791: Fix substitution of ParamSpec in Concatenate with different parameter expressions (GH-27518)
* Substitution with a list of types returns now a tuple of types. * Substitution with Concatenate returns now a Concatenate with concatenated lists of arguments. * Substitution with Ellipsis is not supported.
1 parent 82bce54 commit ecfacc3

File tree

4 files changed

+65
-5
lines changed

4 files changed

+65
-5
lines changed

Lib/_collections_abc.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,10 @@ def __getitem__(self, item):
500500
if subparams:
501501
subargs = tuple(subst[x] for x in subparams)
502502
arg = arg[subargs]
503-
new_args.append(arg)
503+
if isinstance(arg, tuple):
504+
new_args.extend(arg)
505+
else:
506+
new_args.append(arg)
504507

505508
# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
506509
if not isinstance(new_args[0], list):

Lib/test/test_typing.py

+45-3
Original file line numberDiff line numberDiff line change
@@ -628,10 +628,31 @@ def test_paramspec(self):
628628
def test_concatenate(self):
629629
Callable = self.Callable
630630
fullname = f"{Callable.__module__}.Callable"
631+
T = TypeVar('T')
631632
P = ParamSpec('P')
632-
C1 = Callable[typing.Concatenate[int, P], int]
633-
self.assertEqual(repr(C1),
634-
f"{fullname}[typing.Concatenate[int, ~P], int]")
633+
P2 = ParamSpec('P2')
634+
C = Callable[Concatenate[int, P], T]
635+
self.assertEqual(repr(C),
636+
f"{fullname}[typing.Concatenate[int, ~P], ~T]")
637+
self.assertEqual(C[P2, int], Callable[Concatenate[int, P2], int])
638+
self.assertEqual(C[[str, float], int], Callable[[int, str, float], int])
639+
self.assertEqual(C[[], int], Callable[[int], int])
640+
self.assertEqual(C[Concatenate[str, P2], int],
641+
Callable[Concatenate[int, str, P2], int])
642+
with self.assertRaises(TypeError):
643+
C[..., int]
644+
645+
C = Callable[Concatenate[int, P], int]
646+
self.assertEqual(repr(C),
647+
f"{fullname}[typing.Concatenate[int, ~P], int]")
648+
self.assertEqual(C[P2], Callable[Concatenate[int, P2], int])
649+
self.assertEqual(C[[str, float]], Callable[[int, str, float], int])
650+
self.assertEqual(C[str, float], Callable[[int, str, float], int])
651+
self.assertEqual(C[[]], Callable[[int], int])
652+
self.assertEqual(C[Concatenate[str, P2]],
653+
Callable[Concatenate[int, str, P2], int])
654+
with self.assertRaises(TypeError):
655+
C[...]
635656

636657
def test_errors(self):
637658
Callable = self.Callable
@@ -5004,6 +5025,27 @@ def test_valid_uses(self):
50045025
self.assertEqual(C4.__args__, (Concatenate[int, T, P], T))
50055026
self.assertEqual(C4.__parameters__, (T, P))
50065027

5028+
def test_var_substitution(self):
5029+
T = TypeVar('T')
5030+
P = ParamSpec('P')
5031+
P2 = ParamSpec('P2')
5032+
C = Concatenate[T, P]
5033+
self.assertEqual(C[int, P2], Concatenate[int, P2])
5034+
self.assertEqual(C[int, [str, float]], (int, str, float))
5035+
self.assertEqual(C[int, []], (int,))
5036+
self.assertEqual(C[int, Concatenate[str, P2]],
5037+
Concatenate[int, str, P2])
5038+
with self.assertRaises(TypeError):
5039+
C[int, ...]
5040+
5041+
C = Concatenate[int, P]
5042+
self.assertEqual(C[P2], Concatenate[int, P2])
5043+
self.assertEqual(C[[str, float]], (int, str, float))
5044+
self.assertEqual(C[str, float], (int, str, float))
5045+
self.assertEqual(C[[]], (int,))
5046+
self.assertEqual(C[Concatenate[str, P2]], Concatenate[int, str, P2])
5047+
with self.assertRaises(TypeError):
5048+
C[...]
50075049

50085050
class TypeGuardTests(BaseTestCase):
50095051
def test_basics(self):

Lib/typing.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ def Concatenate(self, parameters):
604604
raise TypeError("The last parameter to Concatenate should be a "
605605
"ParamSpec variable.")
606606
msg = "Concatenate[arg, ...]: each arg must be a type."
607-
parameters = tuple(_type_check(p, msg) for p in parameters)
607+
parameters = (*(_type_check(p, msg) for p in parameters[:-1]), parameters[-1])
608608
return _ConcatenateGenericAlias(self, parameters)
609609

610610

@@ -1274,6 +1274,16 @@ def __init__(self, *args, **kwargs):
12741274
_typevar_types=(TypeVar, ParamSpec),
12751275
_paramspec_tvars=True)
12761276

1277+
def copy_with(self, params):
1278+
if isinstance(params[-1], (list, tuple)):
1279+
return (*params[:-1], *params[-1])
1280+
if isinstance(params[-1], _ConcatenateGenericAlias):
1281+
params = (*params[:-1], *params[-1].__args__)
1282+
elif not isinstance(params[-1], ParamSpec):
1283+
raise TypeError("The last parameter to Concatenate should be a "
1284+
"ParamSpec variable.")
1285+
return super().copy_with(params)
1286+
12771287

12781288
class Generic:
12791289
"""Abstract base class for generic types.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix substitution of :class:`~typing.ParamSpec` in
2+
:data:`~typing.Concatenate` with different parameter expressions.
3+
Substitution with a list of types returns now a tuple of types. Substitution
4+
with ``Concatenate`` returns now a ``Concatenate`` with concatenated lists
5+
of arguments.

0 commit comments

Comments
 (0)