From dd275c05ef6bd37d09417c5ef921884c1fce44c1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 8 Aug 2024 18:01:32 +0200 Subject: [PATCH] Concatenate supports Ellipsis in python3.9-3.10 --- src/test_typing_extensions.py | 76 +++++++++++++++++++++++++++++++++-- src/typing_extensions.py | 71 +++++++++++++++++++++++--------- 2 files changed, 125 insertions(+), 22 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 868e7938..39683e3d 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1681,12 +1681,16 @@ class C(Generic[T]): pass # In 3.9 and lower we use typing_extensions's hacky implementation # of ParamSpec, which gets incorrectly wrapped in a list self.assertIn(get_args(Callable[P, int]), [(P, int), ([P], int)]) - self.assertEqual(get_args(Callable[Concatenate[int, P], int]), - (Concatenate[int, P], int)) self.assertEqual(get_args(Required[int]), (int,)) self.assertEqual(get_args(NotRequired[int]), (int,)) self.assertEqual(get_args(Unpack[Ts]), (Ts,)) self.assertEqual(get_args(Unpack), ()) + self.assertEqual(get_args(Callable[Concatenate[int, P], int]), + (Concatenate[int, P], int)) + if sys.version_info >= (3, 9): + # Cannot construct Callable[Concatenate[int, ...] with non-types + self.assertEqual(get_args(Callable[Concatenate[int, ...], int]), + (Concatenate[int, ...], int)) class CollectionsAbcTests(BaseTestCase): @@ -5228,6 +5232,10 @@ class Y(Protocol[T, P]): self.assertEqual(G2.__args__, (int, Concatenate[int, P_2])) self.assertEqual(G2.__parameters__, (P_2,)) + G3 = klass[int, Concatenate[int, ...]] + self.assertEqual(G3.__args__, (int, Concatenate[int, ...])) + self.assertEqual(G3.__parameters__, ()) + # The following are some valid uses cases in PEP 612 that don't work: # These do not work in 3.9, _type_check blocks the list and ellipsis. # G3 = X[int, [int, bool]] @@ -5323,6 +5331,11 @@ class MyClass: ... c = Concatenate[MyClass, P] self.assertNotEqual(c, Concatenate) + # Test Ellipsis Concatenation + d = Concatenate[MyClass, ...] + self.assertNotEqual(d, c) + self.assertNotEqual(d, Concatenate) + def test_valid_uses(self): P = ParamSpec('P') T = TypeVar('T') @@ -5339,6 +5352,21 @@ def test_valid_uses(self): self.assertEqual(C3.__origin__, C4.__origin__) self.assertNotEqual(C3, C4) + # Callable with Ellipsis cannot be constructed in Python3.8 + if sys.version_info[:2] <= (3, 8): + return + + C5 = Callable[Concatenate[int, ...], int] + C6 = Callable[Concatenate[int, T, ...], T] + self.assertEqual(C5.__origin__, C6.__origin__) + self.assertNotEqual(C5, C6) + + # Test collections.abc.Callable too. + C7 = collections.abc.Callable[Concatenate[int, ...], int] + C8 = collections.abc.Callable[Concatenate[int, T, ...], T] + self.assertEqual(C7.__origin__, C8.__origin__) + self.assertNotEqual(C7, C8) + def test_invalid_uses(self): P = ParamSpec('P') T = TypeVar('T') @@ -5351,10 +5379,20 @@ def test_invalid_uses(self): with self.assertRaisesRegex( TypeError, - 'The last parameter to Concatenate should be a ParamSpec variable', + 'The last parameter to Concatenate should be a ParamSpec variable or ellipsis', ): Concatenate[P, T] + + if sys.version_info[:2] >= (3, 9): + # Cannot construct a Callable with Ellipsis in Python3.8 as args must be types + with self.assertRaisesRegex( + TypeError, + 'is not a generic class', + ): + Callable[Concatenate[int, ...], Any][T] + + if not TYPING_3_11_0: with self.assertRaisesRegex( TypeError, @@ -5362,14 +5400,37 @@ def test_invalid_uses(self): ): Concatenate[1, P] + with self.assertRaisesRegex( + TypeError, + 'each arg must be a type.', + ): + Concatenate[1, ..., P] + + @skipUnless(TYPING_3_11_0, "Cannot be backported to <=3.9" + "Cannot use typing._ConcatenateGenericAlias in 3.10") + def test_alias(self): + P = ParamSpec("P") + C1 = Callable[Concatenate[int, P], Any] + # Python <= 3.9 fails because parameters to generic types must be types. + # For Python 3.10 & typing._ConcatenateGenericAlias will + # as Ellipsis is not supported for ParamSpec + # Fallback to 3.10 & typing_extensions._ConcatenateGenericAlias not implemented + C1[...] + def test_basic_introspection(self): P = ParamSpec('P') C1 = Concatenate[int, P] C2 = Concatenate[int, T, P] + C3 = Concatenate[int, ...] + C4 = Concatenate[int, T, ...] self.assertEqual(C1.__origin__, Concatenate) self.assertEqual(C1.__args__, (int, P)) self.assertEqual(C2.__origin__, Concatenate) self.assertEqual(C2.__args__, (int, T, P)) + self.assertEqual(C3.__origin__, Concatenate) + self.assertEqual(C3.__args__, (int, Ellipsis)) + self.assertEqual(C4.__origin__, Concatenate) + self.assertEqual(C4.__args__, (int, T, Ellipsis)) def test_eq(self): P = ParamSpec('P') @@ -5380,6 +5441,13 @@ def test_eq(self): self.assertEqual(hash(C1), hash(C2)) self.assertNotEqual(C1, C3) + C4 = Concatenate[int, ...] + C5 = Concatenate[int, ...] + C6 = Concatenate[int, T, ...] + self.assertEqual(C4, C5) + self.assertEqual(hash(C4), hash(C5)) + self.assertNotEqual(C4, C6) + class TypeGuardTests(BaseTestCase): def test_basics(self): @@ -6050,7 +6118,7 @@ def test_typing_extensions_defers_when_possible(self): if sys.version_info < (3, 10, 1): exclude |= {"Literal"} if sys.version_info < (3, 11): - exclude |= {'final', 'Any', 'NewType', 'overload'} + exclude |= {'final', 'Any', 'NewType', 'overload', 'Concatenate'} if sys.version_info < (3, 12): exclude |= { 'SupportsAbs', 'SupportsBytes', diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 8046dae1..b321ccd4 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1795,28 +1795,63 @@ def __parameters__(self): return tuple( tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) ) +# 3.10+ +else: + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias +# 3.10 +if sys.version_info[:2] == (3, 10): + _ellipsis_dummy = ParamSpec('_ellipsis_dummy') -# 3.8-3.9 -@typing._tp_cache -def _concatenate_getitem(self, parameters): - if parameters == (): - raise TypeError("Cannot take a Concatenate of no types.") - if not isinstance(parameters, tuple): - parameters = (parameters,) - 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." - parameters = tuple(typing._type_check(p, msg) for p in parameters) - return _ConcatenateGenericAlias(self, parameters) - + @typing._tp_cache + def _concatenate_getitem(self, parameters): + if 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)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + msg = "Concatenate[arg, ...]: each arg must be a type." + parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]), + parameters[-1]) + if parameters[-1] is Ellipsis: + # Hack: Need ParamSpec as last parameter when passing to typing class in 3.10 + parameters = parameters[:-1] + (_ellipsis_dummy,) + concatenate = _ConcatenateGenericAlias(self, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) + # Remove dummy and replace with Ellipsis again + concatenate.__args__ = tuple(p if p is not _ellipsis_dummy else ... + for p in concatenate.__args__) + concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ + if p is not _ellipsis_dummy) + return concatenate + + return _ConcatenateGenericAlias(self, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) -# 3.10+ -if hasattr(typing, 'Concatenate'): +# 3.8-3.9 +else: + @typing._tp_cache + def _concatenate_getitem(self, parameters): + if 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)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + msg = "Concatenate[arg, ...]: each arg must be a type." + parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]), + parameters[-1]) + return _ConcatenateGenericAlias(self, parameters) + +# 3.11+; Concatenate does not accept ellipsis in 3.10 +if hasattr(typing, 'Concatenate') and sys.version_info[:2] >= (3, 11): Concatenate = typing.Concatenate - _ConcatenateGenericAlias = typing._ConcatenateGenericAlias -# 3.9 +# 3.9-3.10 elif sys.version_info[:2] >= (3, 9): @_ExtensionsSpecialForm def Concatenate(self, parameters):