Skip to content

Commit

Permalink
bpo-44791: Fix substitution of ParamSpec in Concatenate with differen…
Browse files Browse the repository at this point in the history
…t 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.
(cherry picked from commit ecfacc3)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
  • Loading branch information
miss-islington and serhiy-storchaka authored Jan 27, 2022
1 parent 2572c67 commit 89db090
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 5 deletions.
5 changes: 4 additions & 1 deletion Lib/_collections_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,10 @@ def __getitem__(self, item):
if subparams:
subargs = tuple(subst[x] for x in subparams)
arg = arg[subargs]
new_args.append(arg)
if isinstance(arg, tuple):
new_args.extend(arg)
else:
new_args.append(arg)

# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
if not isinstance(new_args[0], list):
Expand Down
48 changes: 45 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,10 +621,31 @@ def test_paramspec(self):
def test_concatenate(self):
Callable = self.Callable
fullname = f"{Callable.__module__}.Callable"
T = TypeVar('T')
P = ParamSpec('P')
C1 = Callable[typing.Concatenate[int, P], int]
self.assertEqual(repr(C1),
f"{fullname}[typing.Concatenate[int, ~P], int]")
P2 = ParamSpec('P2')
C = Callable[Concatenate[int, P], T]
self.assertEqual(repr(C),
f"{fullname}[typing.Concatenate[int, ~P], ~T]")
self.assertEqual(C[P2, int], Callable[Concatenate[int, P2], int])
self.assertEqual(C[[str, float], int], Callable[[int, str, float], int])
self.assertEqual(C[[], int], Callable[[int], int])
self.assertEqual(C[Concatenate[str, P2], int],
Callable[Concatenate[int, str, P2], int])
with self.assertRaises(TypeError):
C[..., int]

C = Callable[Concatenate[int, P], int]
self.assertEqual(repr(C),
f"{fullname}[typing.Concatenate[int, ~P], int]")
self.assertEqual(C[P2], Callable[Concatenate[int, P2], int])
self.assertEqual(C[[str, float]], Callable[[int, str, float], int])
self.assertEqual(C[str, float], Callable[[int, str, float], int])
self.assertEqual(C[[]], Callable[[int], int])
self.assertEqual(C[Concatenate[str, P2]],
Callable[Concatenate[int, str, P2], int])
with self.assertRaises(TypeError):
C[...]

def test_errors(self):
Callable = self.Callable
Expand Down Expand Up @@ -4894,6 +4915,27 @@ def test_valid_uses(self):
self.assertEqual(C4.__args__, (Concatenate[int, T, P], T))
self.assertEqual(C4.__parameters__, (T, P))

def test_var_substitution(self):
T = TypeVar('T')
P = ParamSpec('P')
P2 = ParamSpec('P2')
C = Concatenate[T, P]
self.assertEqual(C[int, P2], Concatenate[int, P2])
self.assertEqual(C[int, [str, float]], (int, str, float))
self.assertEqual(C[int, []], (int,))
self.assertEqual(C[int, Concatenate[str, P2]],
Concatenate[int, str, P2])
with self.assertRaises(TypeError):
C[int, ...]

C = Concatenate[int, P]
self.assertEqual(C[P2], Concatenate[int, P2])
self.assertEqual(C[[str, float]], (int, str, float))
self.assertEqual(C[str, float], (int, str, float))
self.assertEqual(C[[]], (int,))
self.assertEqual(C[Concatenate[str, P2]], Concatenate[int, str, P2])
with self.assertRaises(TypeError):
C[...]

class TypeGuardTests(BaseTestCase):
def test_basics(self):
Expand Down
12 changes: 11 additions & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def Concatenate(self, parameters):
raise TypeError("The last parameter to Concatenate should be a "
"ParamSpec variable.")
msg = "Concatenate[arg, ...]: each arg must be a type."
parameters = tuple(_type_check(p, msg) for p in parameters)
parameters = (*(_type_check(p, msg) for p in parameters[:-1]), parameters[-1])
return _ConcatenateGenericAlias(self, parameters)


Expand Down Expand Up @@ -1274,6 +1274,16 @@ def __init__(self, *args, **kwargs):
_typevar_types=(TypeVar, ParamSpec),
_paramspec_tvars=True)

def copy_with(self, params):
if isinstance(params[-1], (list, tuple)):
return (*params[:-1], *params[-1])
if isinstance(params[-1], _ConcatenateGenericAlias):
params = (*params[:-1], *params[-1].__args__)
elif not isinstance(params[-1], ParamSpec):
raise TypeError("The last parameter to Concatenate should be a "
"ParamSpec variable.")
return super().copy_with(params)


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

0 comments on commit 89db090

Please sign in to comment.