From 89db09029566cf3af04b540e33fe1ff9b32f8c8b Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Thu, 27 Jan 2022 05:01:24 -0800 Subject: [PATCH] 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. (cherry picked from commit ecfacc362dd7fef7715dcd94f2e2ca6c622ef115) Co-authored-by: Serhiy Storchaka --- Lib/_collections_abc.py | 5 +- Lib/test/test_typing.py | 48 +++++++++++++++++-- Lib/typing.py | 12 ++++- .../2021-07-31-23-18-50.bpo-44791.4jFdpO.rst | 5 ++ 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-07-31-23-18-50.bpo-44791.4jFdpO.rst diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 87a9cd2d46de99..97913c77721da9 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -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): diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d4068242da6da1..eb72b098bbf5cd 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -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 @@ -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): diff --git a/Lib/typing.py b/Lib/typing.py index 705331a9a89a07..aca3f7a7e82559 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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) @@ -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. diff --git a/Misc/NEWS.d/next/Library/2021-07-31-23-18-50.bpo-44791.4jFdpO.rst b/Misc/NEWS.d/next/Library/2021-07-31-23-18-50.bpo-44791.4jFdpO.rst new file mode 100644 index 00000000000000..8182aa4e5358aa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-07-31-23-18-50.bpo-44791.4jFdpO.rst @@ -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.