From 7b29a31ecc5688d88008ef1f97cf7464f1acf5b6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 31 Jul 2021 23:19:51 +0300 Subject: [PATCH 1/3] bpo-44791: Fix substitution of ParamSpec in Concatenate with different parameter expressions --- Doc/library/typing.rst | 3 +- Lib/test/test_typing.py | 45 +++++++++++++++++-- Lib/typing.py | 11 ++++- .../2021-07-31-23-18-50.bpo-44791.4jFdpO.rst | 6 +++ 4 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-07-31-23-18-50.bpo-44791.4jFdpO.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index e8d7c9ee01bd1f..ee74818c9501e7 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -688,7 +688,8 @@ These can be used as types in annotations using ``[]``, each having a unique syn callable. Usage is in the form ``Concatenate[Arg1Type, Arg2Type, ..., ParamSpecVariable]``. ``Concatenate`` is currently only valid when used as the first argument to a :data:`Callable`. - The last parameter to ``Concatenate`` must be a :class:`ParamSpec`. + The last parameter to ``Concatenate`` must be a :class:`ParamSpec` or + ellipsis (``...``). For example, to annotate a decorator ``with_lock`` which provides a :class:`threading.Lock` to the decorated function, ``Concatenate`` can be diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index fbdf634c5c3be8..2b27f4cb2b17b9 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -521,7 +521,6 @@ def test_ellipsis_in_generic(self): # Shouldn't crash; see https://github.com/python/typing/issues/259 typing.List[Callable[..., str]] - def test_basic(self): Callable = self.Callable alias = Callable[[int, str], float] @@ -598,10 +597,29 @@ 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[..., int], Callable[Concatenate[int, ...], int]) + self.assertEqual(C[Concatenate[str, P2], int], + Callable[Concatenate[int, str, P2], 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[...], Callable[Concatenate[int, ...], int]) + self.assertEqual(C[Concatenate[str, P2]], + Callable[Concatenate[int, str, P2], int]) def test_errors(self): Callable = self.Callable @@ -4674,6 +4692,25 @@ 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[int, ...]) + self.assertEqual(C[int, Concatenate[str, P2]], + Concatenate[int, str, P2]) + + 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[int, ...]) + self.assertEqual(C[Concatenate[str, P2]], Concatenate[int, str, P2]) class TypeGuardTests(BaseTestCase): def test_basics(self): diff --git a/Lib/typing.py b/Lib/typing.py index 702bb647269d0b..ee6a3f7d27c8a0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -591,11 +591,11 @@ def Concatenate(self, parameters): raise TypeError("Cannot take a Concatenate of no types.") if not isinstance(parameters, tuple): parameters = (parameters,) - if not isinstance(parameters[-1], ParamSpec): + if not(parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): 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) @@ -1247,6 +1247,13 @@ 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__) + 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..3d84c9bcc5e159 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-07-31-23-18-50.bpo-44791.4jFdpO.rst @@ -0,0 +1,6 @@ +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. Ellipsis is now accepted as the last argument of +``Concatenate``. From 54769b0f51c0951a05fd52d4ad9b5ca6e6d5d3b1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 26 Jan 2022 12:01:49 +0200 Subject: [PATCH 2/3] Fix substitution in collections.abc.Callable. --- Lib/_collections_abc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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): From 10674d3afe573c50f2ac604d7b1f94918e003de8 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 27 Jan 2022 13:42:20 +0200 Subject: [PATCH 3/3] Disallow Ellipsis as the last argument of Concatenate. --- Doc/library/typing.rst | 3 +-- Lib/test/test_typing.py | 12 ++++++++---- Lib/typing.py | 5 ++++- .../Library/2021-07-31-23-18-50.bpo-44791.4jFdpO.rst | 3 +-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 4e6f36dd162ecd..cdfd403a34ef91 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -725,8 +725,7 @@ These can be used as types in annotations using ``[]``, each having a unique syn callable. Usage is in the form ``Concatenate[Arg1Type, Arg2Type, ..., ParamSpecVariable]``. ``Concatenate`` is currently only valid when used as the first argument to a :data:`Callable`. - The last parameter to ``Concatenate`` must be a :class:`ParamSpec` or - ellipsis (``...``). + The last parameter to ``Concatenate`` must be a :class:`ParamSpec`. For example, to annotate a decorator ``with_lock`` which provides a :class:`threading.Lock` to the decorated function, ``Concatenate`` can be diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f99c34d594745b..327d755f4b92be 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -637,9 +637,10 @@ def test_concatenate(self): 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[..., int], Callable[Concatenate[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), @@ -648,9 +649,10 @@ def test_concatenate(self): 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[...], Callable[Concatenate[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 @@ -5017,17 +5019,19 @@ def test_var_substitution(self): 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[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[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 7a222eae587ca9..425cfedbe0c04a 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -600,7 +600,7 @@ def Concatenate(self, parameters): raise TypeError("Cannot take a Concatenate of no types.") if not isinstance(parameters, tuple): parameters = (parameters,) - if not(parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): + if not isinstance(parameters[-1], ParamSpec): raise TypeError("The last parameter to Concatenate should be a " "ParamSpec variable.") msg = "Concatenate[arg, ...]: each arg must be a type." @@ -1279,6 +1279,9 @@ def copy_with(self, params): 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) 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 index 3d84c9bcc5e159..8182aa4e5358aa 100644 --- 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 @@ -2,5 +2,4 @@ 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. Ellipsis is now accepted as the last argument of -``Concatenate``. +of arguments.