From 7e0aac82fd30a3bff09116839adefb468b44a014 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 9 Aug 2024 11:03:44 +0200 Subject: [PATCH 01/15] Backport Ellipsis in Concatenate for 3.9-3.10 --- src/test_typing_extensions.py | 74 +++++++++++++++++++++++++++++++++-- src/typing_extensions.py | 72 +++++++++++++++++++++++++--------- 2 files changed, 123 insertions(+), 23 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 868e7938..07895514 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, 2): + # 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 3.8 and below 3.9.2 + if sys.version_info[:2] <= (3, 9, 2): + 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,18 @@ 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] + # Cannot construct a Callable with Ellipsis in 3.8 as args must be types + if sys.version_info[:2] >= (3, 9, 2): + 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 +5398,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 ... with 3.10 typing._ConcatenateGenericAlias") + 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 +5439,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 +6116,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..3cc027fb 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1795,28 +1795,62 @@ 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.2+ +if sys.version_info[:3] >= (3, 10, 2): + _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) - - -# 3.10+ -if hasattr(typing, 'Concatenate'): + @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.8-3.10.0 +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): From 73d3952a360263be0bb460e26e72feb327dd316f Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 12 Aug 2024 11:24:48 +0200 Subject: [PATCH 02/15] Extended ellipsis test, removed version_info slices --- src/test_typing_extensions.py | 20 ++++++++++---------- src/typing_extensions.py | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 07895514..b065864c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5384,7 +5384,7 @@ def test_invalid_uses(self): Concatenate[P, T] # Cannot construct a Callable with Ellipsis in 3.8 as args must be types - if sys.version_info[:2] >= (3, 9, 2): + if sys.version_info >= (3, 9, 2): with self.assertRaisesRegex( TypeError, 'is not a generic class', @@ -5404,16 +5404,16 @@ def test_invalid_uses(self): ): Concatenate[1, ..., P] - @skipUnless(TYPING_3_11_0, "Cannot be backported to <=3.9" - "Cannot use ... with 3.10 typing._ConcatenateGenericAlias") + @skipUnless(TYPING_3_11_0 or (3, 10, 0) <= sys.version_info < (3, 10, 2), + "Cannot be backported to <=3.9" + "Cannot use ... with 3.10.2+ typing._ConcatenateGenericAlias") 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[...] + P = ParamSpec('P') + X = Callable[Concatenate[int, P], Any] + + C1 = X[...] + self.assertEqual(C1.__parameters__, ()) + self.assertEqual(C1.__args__, (Concatenate[int, ...], Any)) def test_basic_introspection(self): P = ParamSpec('P') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 3cc027fb..e781a5c8 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1800,7 +1800,7 @@ def __parameters__(self): _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # 3.10.2+ -if sys.version_info[:3] >= (3, 10, 2): +if sys.version_info >= (3, 10, 2): _ellipsis_dummy = ParamSpec('_ellipsis_dummy') @typing._tp_cache @@ -1848,7 +1848,7 @@ def _concatenate_getitem(self, parameters): 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): +if hasattr(typing, 'Concatenate') and sys.version_info >= (3, 11): Concatenate = typing.Concatenate # 3.9-3.10 elif sys.version_info[:2] >= (3, 9): From 70d520535a7d49c94ab3a891d0cfb6823541201b Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 16:24:23 +0200 Subject: [PATCH 03/15] Properly skipped tests --- src/test_typing_extensions.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 4b9ce601..44a64fb8 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5384,17 +5384,18 @@ def test_valid_uses(self): self.assertEqual(C1.__origin__, C2.__origin__) self.assertNotEqual(C1, C2) - # Test collections.abc.Callable too. - if sys.version_info[:2] >= (3, 9): - C3 = collections.abc.Callable[Concatenate[int, P], int] - C4 = collections.abc.Callable[Concatenate[int, T, P], T] - self.assertEqual(C3.__origin__, C4.__origin__) - self.assertNotEqual(C3, C4) - - # Callable with Ellipsis cannot be constructed in 3.8 and below 3.9.2 - if sys.version_info[:2] <= (3, 9, 2): - return + @skipUnless(TYPING_3_9_0, "Needs PEP 585") + def test_collections_abc_callable(self): + P = ParamSpec('P') + T = TypeVar('T') + C3 = collections.abc.Callable[Concatenate[int, P], int] + C4 = collections.abc.Callable[Concatenate[int, T, P], T] + self.assertEqual(C3.__origin__, C4.__origin__) + self.assertNotEqual(C3, C4) + @skipUnless(sys.version_info >= (3, 9, 3), "Callable with Ellipsis cannot be constructed below 3.9.2") + def test_valid_uses_py39_plus(self): + T = TypeVar('T') C5 = Callable[Concatenate[int, ...], int] C6 = Callable[Concatenate[int, T, ...], T] self.assertEqual(C5.__origin__, C6.__origin__) From 8e4f0be2d7f3010b8ab57f719e741084b1313911 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 16:35:31 +0200 Subject: [PATCH 04/15] renamed function better reasoning --- src/test_typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 44a64fb8..f612426e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5446,8 +5446,8 @@ def test_invalid_uses(self): @skipUnless(TYPING_3_11_0 or (3, 10, 0) <= sys.version_info < (3, 10, 2), "Cannot be backported to <=3.9" - "Cannot use ... with 3.10.2+ typing._ConcatenateGenericAlias") - def test_alias(self): + "Cannot use ... with typing._ConcatenateGenericAlias after 3.10.2") + def test_alias_subscription_with_ellipsis(self): P = ParamSpec('P') X = Callable[Concatenate[int, P], Any] From 0991b4228e6b7055cd796f444ec489b5e443c13e Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 17:00:32 +0200 Subject: [PATCH 05/15] Properly skipped tests --- src/test_typing_extensions.py | 69 +++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f612426e..27eb5cdb 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1726,8 +1726,9 @@ class C(Generic[T]): pass self.assertEqual(get_args(Unpack), ()) self.assertEqual(get_args(Callable[Concatenate[int, P], int]), (Concatenate[int, P], int)) - if sys.version_info >= (3, 9, 2): - # Cannot construct Callable[Concatenate[int, ...] with non-types + with self.subTest("Concatenate[int, ...]"): + if sys.version_info < (3, 9, 2): + self.skipTest("arguments must be types before 3.9.2, i.e. no ...") self.assertEqual(get_args(Callable[Concatenate[int, ...], int]), (Concatenate[int, ...], int)) @@ -5384,24 +5385,25 @@ def test_valid_uses(self): self.assertEqual(C1.__origin__, C2.__origin__) self.assertNotEqual(C1, C2) - @skipUnless(TYPING_3_9_0, "Needs PEP 585") - def test_collections_abc_callable(self): + @skipUnless(TYPING_3_9_0, "Needs PEP 585; no backport for 3.8 typing") + def test_valid_uses_py39_plus(self): P = ParamSpec('P') T = TypeVar('T') - C3 = collections.abc.Callable[Concatenate[int, P], int] - C4 = collections.abc.Callable[Concatenate[int, T, P], T] - self.assertEqual(C3.__origin__, C4.__origin__) - self.assertNotEqual(C3, C4) - @skipUnless(sys.version_info >= (3, 9, 3), "Callable with Ellipsis cannot be constructed below 3.9.2") - def test_valid_uses_py39_plus(self): - T = TypeVar('T') - C5 = Callable[Concatenate[int, ...], int] - C6 = Callable[Concatenate[int, T, ...], T] + with self.subTest("typing.Callable with Ellipsis"): + if sys.version_info < (3, 9, 2): + self.skipTest("Must use types before 3.9.2") + C3 = Callable[Concatenate[int, ...], int] + C4 = Callable[Concatenate[int, T, ...], T] + self.assertEqual(C3.__origin__, C4.__origin__) + self.assertNotEqual(C3, C4) + + # Test collections.abc.Callable too. + C5 = collections.abc.Callable[Concatenate[int, P], int] + C6 = collections.abc.Callable[Concatenate[int, T, P], 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__) @@ -5423,26 +5425,29 @@ def test_invalid_uses(self): ): Concatenate[P, T] - # Cannot construct a Callable with Ellipsis in 3.8 as args must be types - if sys.version_info >= (3, 9, 2): - with self.assertRaisesRegex( - TypeError, - 'is not a generic class', - ): - Callable[Concatenate[int, ...], Any][T] + @skipIf(sys.version_info < (3, 9, 2), "Args must be types below 3.9.2") + def test_invalid_uses_py39_2_plus(self): + T = TypeVar('T') + with self.assertRaisesRegex( + TypeError, + 'is not a generic class', + ): + Callable[Concatenate[int, ...], Any][T] - if not TYPING_3_11_0: - with self.assertRaisesRegex( - TypeError, - 'each arg must be a type', - ): - Concatenate[1, P] + @skipIf(TYPING_3_11_0, "Args can be non-types in 3.11+") + def test_invalid_uses_before_3_11(self): + P = ParamSpec('P') + with self.assertRaisesRegex( + TypeError, + 'each arg must be a type', + ): + Concatenate[1, P] - with self.assertRaisesRegex( - TypeError, - 'each arg must be a type.', - ): - Concatenate[1, ..., P] + with self.assertRaisesRegex( + TypeError, + 'each arg must be a type.', + ): + Concatenate[1, ..., P] @skipUnless(TYPING_3_11_0 or (3, 10, 0) <= sys.version_info < (3, 10, 2), "Cannot be backported to <=3.9" From 3112a80d1c667a583a5ce6f1fd67958d8b2c2340 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 2 Oct 2024 10:06:57 +0200 Subject: [PATCH 06/15] Added info to changelog and docs --- CHANGELOG.md | 2 ++ doc/index.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3cafa1..7334969b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - Fix bug where a subscripted `TypeAliasType` instance did not have all attributes of the original `TypeAliasType` instance on older Python versions. Patch by [Daraan](https://github.com/Daraan) and Alex Waygood. +- Extended the Concatenate backport for Python 3.9 and 3.10 to now accept + ellipsis as an argument. Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/doc/index.rst b/doc/index.rst index 23a531c4..4fb6f3b4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -178,7 +178,7 @@ Special typing primitives See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10. The backport does not support certain operations involving ``...`` as - a parameter; see :issue:`48` and :issue:`110` for details. + a parameter; see :pr:`442`, :issue:`48` and :issue:`110` for details. .. data:: Final From 1eabcb6f45249eb9a8da73bf69f54958a5117993 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 2 Oct 2024 11:19:51 +0200 Subject: [PATCH 07/15] Full backport to 3.8 to support Concatenate[...] --- src/test_typing_extensions.py | 22 +++------ src/typing_extensions.py | 84 ++++++++++++++++------------------- 2 files changed, 46 insertions(+), 60 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 03d72892..6786c840 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1727,8 +1727,6 @@ class C(Generic[T]): pass self.assertEqual(get_args(Callable[Concatenate[int, P], int]), (Concatenate[int, P], int)) with self.subTest("Concatenate[int, ...]"): - if sys.version_info < (3, 9, 2): - self.skipTest("arguments must be types before 3.9.2, i.e. no ...") self.assertEqual(get_args(Callable[Concatenate[int, ...], int]), (Concatenate[int, ...], int)) @@ -5385,19 +5383,16 @@ def test_valid_uses(self): self.assertEqual(C1.__origin__, C2.__origin__) self.assertNotEqual(C1, C2) - @skipUnless(TYPING_3_9_0, "Needs PEP 585; no backport for 3.8 typing") - def test_valid_uses_py39_plus(self): - P = ParamSpec('P') - T = TypeVar('T') - with self.subTest("typing.Callable with Ellipsis"): - if sys.version_info < (3, 9, 2): - self.skipTest("Must use types before 3.9.2") C3 = Callable[Concatenate[int, ...], int] C4 = Callable[Concatenate[int, T, ...], T] self.assertEqual(C3.__origin__, C4.__origin__) self.assertNotEqual(C3, C4) + @skipUnless(TYPING_3_9_0, "Needs PEP 585") + def test_pep585_collections_callable(self): + P = ParamSpec('P') + T = TypeVar('T') # Test collections.abc.Callable too. C5 = collections.abc.Callable[Concatenate[int, P], int] C6 = collections.abc.Callable[Concatenate[int, T, P], T] @@ -5425,14 +5420,11 @@ def test_invalid_uses(self): ): Concatenate[P, T] - @skipIf(sys.version_info < (3, 9, 2), "Args must be types below 3.9.2") - def test_invalid_uses_py39_2_plus(self): - T = TypeVar('T') with self.assertRaisesRegex( TypeError, 'is not a generic class', ): - Callable[Concatenate[int, ...], Any][T] + Callable[Concatenate[int, ...], Any][Any] @skipIf(TYPING_3_11_0, "Args can be non-types in 3.11+") def test_invalid_uses_before_3_11(self): @@ -5450,7 +5442,7 @@ def test_invalid_uses_before_3_11(self): Concatenate[1, ..., P] @skipUnless(TYPING_3_11_0 or (3, 10, 0) <= sys.version_info < (3, 10, 2), - "Cannot be backported to <=3.9" + "Cannot be backported to <=3.9. See issue #48" "Cannot use ... with typing._ConcatenateGenericAlias after 3.10.2") def test_alias_subscription_with_ellipsis(self): P = ParamSpec('P') @@ -5458,7 +5450,7 @@ def test_alias_subscription_with_ellipsis(self): C1 = X[...] self.assertEqual(C1.__parameters__, ()) - self.assertEqual(C1.__args__, (Concatenate[int, ...], Any)) + self.assertEqual(get_args(C1), (Concatenate[int, ...], Any)) def test_basic_introspection(self): P = ParamSpec('P') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index df318dcf..bdea2007 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1799,53 +1799,47 @@ def __parameters__(self): else: _ConcatenateGenericAlias = typing._ConcatenateGenericAlias -# 3.10.2+ -if sys.version_info >= (3, 10, 2): - _ellipsis_dummy = ParamSpec('_ellipsis_dummy') - - @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, +# 3.8-3.9,2 +class _EllipsisDummyType: ... + +# 3.8-3.10 +def _create_concatenate_alias(origin, parameters): + if parameters[-1] is ... and sys.version_info < (3, 9, 2): + # Arguments must be types + parameters = parameters[:-1] + (_EllipsisDummyType,) + if sys.version_info >= (3, 10, 2): + concatenate = _ConcatenateGenericAlias(origin, parameters, _typevar_types=(TypeVar, ParamSpec), _paramspec_tvars=True) - -# 3.8-3.10.0 -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) + else: + concatenate = _ConcatenateGenericAlias(origin, parameters) + if parameters[-1] is not _EllipsisDummyType: + return concatenate + # Remove dummy again + concatenate.__args__ = tuple(p if p is not _EllipsisDummyType else ... + for p in concatenate.__args__) + if sys.version_info < (3, 10): + # backport needs __args__ adjustment only + return concatenate + concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ + if p is not _EllipsisDummyType) + return concatenate + + +# 3.8-3.10 +@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 _create_concatenate_alias(self, parameters) # 3.11+; Concatenate does not accept ellipsis in 3.10 if hasattr(typing, 'Concatenate') and sys.version_info >= (3, 11): From 3e6ec0aaf0a776be3572e08c1d96246df20d0f5f Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 2 Oct 2024 11:23:47 +0200 Subject: [PATCH 08/15] Updated changelog and docs --- CHANGELOG.md | 2 +- doc/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7334969b..61018d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Fix bug where a subscripted `TypeAliasType` instance did not have all attributes of the original `TypeAliasType` instance on older Python versions. Patch by [Daraan](https://github.com/Daraan) and Alex Waygood. -- Extended the Concatenate backport for Python 3.9 and 3.10 to now accept +- Extended the Concatenate backport for Python 3.8-3.10 to now accept ellipsis as an argument. Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/doc/index.rst b/doc/index.rst index 4fb6f3b4..e1eed353 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -178,7 +178,7 @@ Special typing primitives See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10. The backport does not support certain operations involving ``...`` as - a parameter; see :pr:`442`, :issue:`48` and :issue:`110` for details. + a parameter; see :issue:`48` and :pr:`442` for details. .. data:: Final From a77d93f99dc4b6135a7479d9a5fa9001cb49427d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 2 Oct 2024 11:32:50 +0200 Subject: [PATCH 09/15] minor comment and name update --- src/typing_extensions.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index d2599ff8..fa70877f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1799,30 +1799,30 @@ def __parameters__(self): else: _ConcatenateGenericAlias = typing._ConcatenateGenericAlias -# 3.8-3.9,2 -class _EllipsisDummyType: ... +# 3.8-3.9.2 +class _EllipsisDummy: ... # 3.8-3.10 def _create_concatenate_alias(origin, parameters): if parameters[-1] is ... and sys.version_info < (3, 9, 2): - # Arguments must be types - parameters = parameters[:-1] + (_EllipsisDummyType,) + # Hack: Arguments must be types, replace it with one. + parameters = parameters[:-1] + (_EllipsisDummy,) if sys.version_info >= (3, 10, 2): concatenate = _ConcatenateGenericAlias(origin, parameters, _typevar_types=(TypeVar, ParamSpec), _paramspec_tvars=True) else: concatenate = _ConcatenateGenericAlias(origin, parameters) - if parameters[-1] is not _EllipsisDummyType: + if parameters[-1] is not _EllipsisDummy: return concatenate # Remove dummy again - concatenate.__args__ = tuple(p if p is not _EllipsisDummyType else ... + concatenate.__args__ = tuple(p if p is not _EllipsisDummy else ... for p in concatenate.__args__) if sys.version_info < (3, 10): # backport needs __args__ adjustment only return concatenate concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ - if p is not _EllipsisDummyType) + if p is not _EllipsisDummy) return concatenate From 8cba5ab6ccbd7e1074dcd22ab708046c9947e83d Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 5 Oct 2024 13:47:01 +0200 Subject: [PATCH 10/15] Changed invalid use example to current behavior --- src/test_typing_extensions.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index d404a756..91518eae 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5426,20 +5426,14 @@ def test_invalid_uses(self): ): Callable[Concatenate[int, ...], Any][Any] - @skipIf(TYPING_3_11_0, "Args can be non-types in 3.11+") - def test_invalid_uses_before_3_11(self): + def test_invalid_use(self): + # Assure that `_type_check` is called. P = ParamSpec('P') with self.assertRaisesRegex( TypeError, - 'each arg must be a type', + "each arg must be a type", ): - Concatenate[1, P] - - with self.assertRaisesRegex( - TypeError, - 'each arg must be a type.', - ): - Concatenate[1, ..., P] + Concatenate[(str,), P] @skipUnless(TYPING_3_11_0 or (3, 10, 0) <= sys.version_info < (3, 10, 2), "Cannot be backported to <=3.9. See issue #48" From 65270c6bea9a9e9088329d5ee44d0ec030cd6163 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 5 Oct 2024 14:20:11 +0200 Subject: [PATCH 11/15] Changed ref to new PR --- doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index 9f2fbe6b..d321ce04 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -178,7 +178,7 @@ Special typing primitives See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10. The backport does not support certain operations involving ``...`` as - a parameter; see :issue:`48` and :pr:`442` for details. + a parameter; see :issue:`48` and :pr:`481` for details. .. data:: Final From 9fda1a1ff58809f76da36f3ef362e292d64b6496 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 21 Oct 2024 19:42:13 +0200 Subject: [PATCH 12/15] Unified typing.Callable and collections.abc.Callable tests --- src/test_typing_extensions.py | 45 +++++++++++++---------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 3fef7355..a1d259f6 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1726,8 +1726,7 @@ class C(Generic[T]): pass self.assertEqual(get_args(Unpack), ()) self.assertEqual(get_args(Callable[Concatenate[int, P], int]), (Concatenate[int, P], int)) - with self.subTest("Concatenate[int, ...]"): - self.assertEqual(get_args(Callable[Concatenate[int, ...], int]), + self.assertEqual(get_args(Callable[Concatenate[int, ...], int]), (Concatenate[int, ...], int)) @@ -5377,32 +5376,20 @@ class MyClass: ... def test_valid_uses(self): P = ParamSpec('P') T = TypeVar('T') - - C1 = Callable[Concatenate[int, P], int] - C2 = Callable[Concatenate[int, T, P], T] - self.assertEqual(C1.__origin__, C2.__origin__) - self.assertNotEqual(C1, C2) - - with self.subTest("typing.Callable with Ellipsis"): - C3 = Callable[Concatenate[int, ...], int] - C4 = Callable[Concatenate[int, T, ...], T] - self.assertEqual(C3.__origin__, C4.__origin__) - self.assertNotEqual(C3, C4) - - @skipUnless(TYPING_3_9_0, "Needs PEP 585") - def test_pep585_collections_callable(self): - P = ParamSpec('P') - T = TypeVar('T') - # Test collections.abc.Callable too. - C5 = collections.abc.Callable[Concatenate[int, P], int] - C6 = collections.abc.Callable[Concatenate[int, T, P], T] - self.assertEqual(C5.__origin__, C6.__origin__) - self.assertNotEqual(C5, C6) - - 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) + for callable_variant in (Callable, collections.abc.Callable): + with self.subTest(callable_variant=callable_variant): + if not TYPING_3_9_0 and callable_variant is collections.abc.Callable: + self.skipTest("Needs PEP 585") + + C1 = callable_variant[Concatenate[int, P], int] + C2 = callable_variant[Concatenate[int, T, P], T] + self.assertEqual(C1.__origin__, C2.__origin__) + self.assertNotEqual(C1, C2) + + C3 = callable_variant[Concatenate[int, ...], int] + C4 = callable_variant[Concatenate[int, T, ...], T] + self.assertEqual(C3.__origin__, C4.__origin__) + self.assertNotEqual(C3, C4) def test_invalid_uses(self): P = ParamSpec('P') @@ -5435,7 +5422,7 @@ def test_invalid_use(self): ): Concatenate[(str,), P] - @skipUnless(TYPING_3_11_0 or (3, 10, 0) <= sys.version_info < (3, 10, 2), + @skipUnless(TYPING_3_11_0, "Cannot be backported to <=3.9. See issue #48" "Cannot use ... with typing._ConcatenateGenericAlias after 3.10.2") def test_alias_subscription_with_ellipsis(self): From c092d7e180727bbeb11e4c6bfce71be847dea213 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 21 Oct 2024 19:43:56 +0200 Subject: [PATCH 13/15] fix whitespace --- src/test_typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a1d259f6..835fadbb 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5380,7 +5380,7 @@ def test_valid_uses(self): with self.subTest(callable_variant=callable_variant): if not TYPING_3_9_0 and callable_variant is collections.abc.Callable: self.skipTest("Needs PEP 585") - + C1 = callable_variant[Concatenate[int, P], int] C2 = callable_variant[Concatenate[int, T, P], T] self.assertEqual(C1.__origin__, C2.__origin__) From c5229f810184e1bd147e912481ffadf524c05212 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 22 Oct 2024 11:15:58 +0200 Subject: [PATCH 14/15] Minor changes - Move changelog entry up - code style --- CHANGELOG.md | 4 ++-- src/typing_extensions.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed92ede..f62d31d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,10 @@ - Backport to Python 3.10 the ability to substitute `...` in generic `Callable` aliases that have a `Concatenate` special form as their argument. Patch by [Daraan](https://github.com/Daraan). -- Fix error in subscription of `Unpack` aliases causing nested Unpacks - to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). - Extended the `Concatenate` backport for Python 3.8-3.10 to now accept `Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan). +- Fix error in subscription of `Unpack` aliases causing nested Unpacks + to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f3d4d3d1..c5e84b31 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1803,7 +1803,6 @@ def __parameters__(self): if sys.version_info < (3, 11): _typing_ConcatenateGenericAlias = _ConcatenateGenericAlias - class _ConcatenateGenericAlias(_typing_ConcatenateGenericAlias, _root=True): # needed for checks in collections.abc.Callable to accept this class __module__ = "typing" @@ -1822,11 +1821,12 @@ def copy_with(self, params): # 3.8-3.9.2 class _EllipsisDummy: ... + # 3.8-3.10 def _create_concatenate_alias(origin, parameters): if parameters[-1] is ... and sys.version_info < (3, 9, 2): # Hack: Arguments must be types, replace it with one. - parameters = parameters[:-1] + (_EllipsisDummy,) + parameters = (*parameters[:-1], _EllipsisDummy) if sys.version_info >= (3, 10, 2): concatenate = _ConcatenateGenericAlias(origin, parameters, _typevar_types=(TypeVar, ParamSpec), @@ -1861,6 +1861,7 @@ def _concatenate_getitem(self, parameters): parameters[-1]) return _create_concatenate_alias(self, parameters) + # 3.11+; Concatenate does not accept ellipsis in 3.10 if sys.version_info >= (3, 11): Concatenate = typing.Concatenate From c06a9c7a074c3eeef319858b6a1bcfa9a7d6eebf Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 22 Oct 2024 11:32:33 +0200 Subject: [PATCH 15/15] unified invalid tests and additional test case --- src/test_typing_extensions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index add98213..1b43f90f 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5407,13 +5407,19 @@ def test_invalid_uses(self): ): Concatenate[P, T] + # Test with tuple argument + with self.assertRaisesRegex( + TypeError, + "The last parameter to Concatenate should be a ParamSpec variable or ellipsis.", + ): + Concatenate[(P, T)] + with self.assertRaisesRegex( TypeError, 'is not a generic class', ): Callable[Concatenate[int, ...], Any][Any] - def test_invalid_use(self): # Assure that `_type_check` is called. P = ParamSpec('P') with self.assertRaisesRegex(