From d71e6c8fc68338b8c5474eb72a441ec60ab42e79 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 12:02:18 +0200 Subject: [PATCH 01/12] DRAFT assure attribute presence with GenericAlias creates more issues than it solves --- src/test_typing_extensions.py | 26 ++++++++++++++++++++++++++ src/typing_extensions.py | 10 +++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index acd762ee..5ee56926 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7247,6 +7247,32 @@ def test_getitem(self): self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) self.assertIs(get_origin(fully_subscripted), ListOrSetT) + def test_alias_attributes(self): + T = TypeVar('T') + T2 = TypeVar('T2') + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + subscripted = ListOrSetT[int] + still_generic = ListOrSetT[Iterable[T2]] + fully_subscripted = still_generic[float] + + with self.subTest(variable=subscripted): + self.assertEqual(subscripted.__module__, ListOrSetT.__module__) + self.assertEqual(subscripted.__name__, "ListOrSetT") + self.assertEqual(subscripted.__value__, Union[List[T], Set[T]]) + self.assertEqual(subscripted.__type_params__, (T,)) + with self.subTest(variable=still_generic): + self.assertEqual(still_generic.__module__, ListOrSetT.__module__) + self.assertEqual(still_generic.__name__, "ListOrSetT") + self.assertEqual(still_generic.__value__, Union[List[T], Set[T]]) + self.assertEqual(still_generic.__type_params__, (T,)) + with self.subTest(variable=fully_subscripted): + if sys.version_info[:2] == (3, 8): + self.skipTest("Cannot further proxy attributes with _GenericAlias") + self.assertEqual(fully_subscripted.__module__, ListOrSetT.__module__) + self.assertEqual(fully_subscripted.__name__, "ListOrSetT") + self.assertEqual(fully_subscripted.__value__, Union[List[T], Set[T]]) + self.assertEqual(fully_subscripted.__type_params__, (T,)) + def test_pickle(self): global Alias Alias = TypeAliasType("Alias", int) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1adc5823..14d17a50 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3533,7 +3533,15 @@ def __getitem__(self, parameters): ) for item in parameters ] - return typing._GenericAlias(self, tuple(parameters)) + if sys.version_info >= (3, 9): + # Using GenericAlias solves the attribute presence issue + # however creates a chain of other issues. + return typing.GenericAlias(self, parameters) + alias = typing._GenericAlias(self, tuple(parameters)) + alias.__value__ = self.__value__ + alias.__type_params__ = self.__type_params__ + alias.__name__ = self.__name__ # this is present on 3.11 + return alias def __reduce__(self): return self.__name__ From 9706ec40977ad3f8ddb14c506d2afa231abc348e Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 12:27:35 +0200 Subject: [PATCH 02/12] Working variant to add attributes for first alias --- src/test_typing_extensions.py | 20 +++++++++----------- src/typing_extensions.py | 7 ++----- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 5ee56926..b24e954e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7255,18 +7255,16 @@ def test_alias_attributes(self): still_generic = ListOrSetT[Iterable[T2]] fully_subscripted = still_generic[float] - with self.subTest(variable=subscripted): - self.assertEqual(subscripted.__module__, ListOrSetT.__module__) - self.assertEqual(subscripted.__name__, "ListOrSetT") - self.assertEqual(subscripted.__value__, Union[List[T], Set[T]]) - self.assertEqual(subscripted.__type_params__, (T,)) - with self.subTest(variable=still_generic): - self.assertEqual(still_generic.__module__, ListOrSetT.__module__) - self.assertEqual(still_generic.__name__, "ListOrSetT") - self.assertEqual(still_generic.__value__, Union[List[T], Set[T]]) - self.assertEqual(still_generic.__type_params__, (T,)) + self.assertEqual(subscripted.__module__, ListOrSetT.__module__) + self.assertEqual(subscripted.__name__, "ListOrSetT") + self.assertEqual(subscripted.__value__, Union[List[T], Set[T]]) + self.assertEqual(subscripted.__type_params__, (T,)) + self.assertEqual(still_generic.__module__, ListOrSetT.__module__) + self.assertEqual(still_generic.__name__, "ListOrSetT") + self.assertEqual(still_generic.__value__, Union[List[T], Set[T]]) + self.assertEqual(still_generic.__type_params__, (T,)) with self.subTest(variable=fully_subscripted): - if sys.version_info[:2] == (3, 8): + if sys.version_info[:2] < (3, 12): self.skipTest("Cannot further proxy attributes with _GenericAlias") self.assertEqual(fully_subscripted.__module__, ListOrSetT.__module__) self.assertEqual(fully_subscripted.__name__, "ListOrSetT") diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 14d17a50..32f44da4 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3533,14 +3533,11 @@ def __getitem__(self, parameters): ) for item in parameters ] - if sys.version_info >= (3, 9): - # Using GenericAlias solves the attribute presence issue - # however creates a chain of other issues. - return typing.GenericAlias(self, parameters) alias = typing._GenericAlias(self, tuple(parameters)) alias.__value__ = self.__value__ alias.__type_params__ = self.__type_params__ - alias.__name__ = self.__name__ # this is present on 3.11 + if sys.version_info <= (3, 11): + alias.__name__ = self.__name__ return alias def __reduce__(self): From 3e93412dc58ed869aac88d6ac02743fe44d88860 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 17:11:00 +0200 Subject: [PATCH 03/12] Use GenericAlias for TypeAliasType for 3.10+ --- src/test_typing_extensions.py | 2 +- src/typing_extensions.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b24e954e..c1789af9 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7264,7 +7264,7 @@ def test_alias_attributes(self): self.assertEqual(still_generic.__value__, Union[List[T], Set[T]]) self.assertEqual(still_generic.__type_params__, (T,)) with self.subTest(variable=fully_subscripted): - if sys.version_info[:2] < (3, 12): + if not TYPING_3_10_0: # needs to align with GenericAlias usage in __getitem__ else 3.12+ self.skipTest("Cannot further proxy attributes with _GenericAlias") self.assertEqual(fully_subscripted.__module__, ListOrSetT.__module__) self.assertEqual(fully_subscripted.__name__, "ListOrSetT") diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 32f44da4..fe3a5b67 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3533,6 +3533,8 @@ def __getitem__(self, parameters): ) for item in parameters ] + if sys.version_info >= (3, 10): + return typing.GenericAlias(self, tuple(parameters)) alias = typing._GenericAlias(self, tuple(parameters)) alias.__value__ = self.__value__ alias.__type_params__ = self.__type_params__ From c155ca7903fb3d51e5b6a9190ab5ac4063f39a6d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 17:39:36 +0200 Subject: [PATCH 04/12] Uses GenericAlias from _types --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index fe3a5b67..be394927 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3534,7 +3534,7 @@ def __getitem__(self, parameters): for item in parameters ] if sys.version_info >= (3, 10): - return typing.GenericAlias(self, tuple(parameters)) + return _types.GenericAlias(self, tuple(parameters)) alias = typing._GenericAlias(self, tuple(parameters)) alias.__value__ = self.__value__ alias.__type_params__ = self.__type_params__ From ff16ad02ea82722b70d2788913ec95ecdf05c545 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 19:09:39 +0200 Subject: [PATCH 05/12] GenericAlias should not be used with Python3.9 --- src/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index be394927..5259f6f8 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3527,6 +3527,7 @@ def __repr__(self) -> str: def __getitem__(self, parameters): if not isinstance(parameters, tuple): parameters = (parameters,) + # Using 3.9 here will create problems with Concatenate parameters = [ typing._type_check( item, f'Subscripting {self.__name__} requires a type.' @@ -3538,8 +3539,7 @@ def __getitem__(self, parameters): alias = typing._GenericAlias(self, tuple(parameters)) alias.__value__ = self.__value__ alias.__type_params__ = self.__type_params__ - if sys.version_info <= (3, 11): - alias.__name__ = self.__name__ + alias.__name__ = self.__name__ return alias def __reduce__(self): From f535afb3e1b6184abae5e5ef6deb40a9c84a5368 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 25 Sep 2024 19:12:48 +0200 Subject: [PATCH 06/12] Using GenericAlias does not require check for non-types --- src/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5259f6f8..289e35a9 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3528,14 +3528,14 @@ def __getitem__(self, parameters): if not isinstance(parameters, tuple): parameters = (parameters,) # Using 3.9 here will create problems with Concatenate + if sys.version_info >= (3, 10): + return _types.GenericAlias(self, tuple(parameters)) parameters = [ typing._type_check( item, f'Subscripting {self.__name__} requires a type.' ) for item in parameters ] - if sys.version_info >= (3, 10): - return _types.GenericAlias(self, tuple(parameters)) alias = typing._GenericAlias(self, tuple(parameters)) alias.__value__ = self.__value__ alias.__type_params__ = self.__type_params__ From 8e69b75ef339ab253db165464414f068ccc7419a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 26 Sep 2024 09:42:28 -0700 Subject: [PATCH 07/12] Fix for doubly subscripted aliases too --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 19 +++++++++---------- src/typing_extensions.py | 17 ++++++++++------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c17c1ba..0d3cafa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ with `@typing_extensions.deprecated`. Patch by Sebastian Rittau. - Fix bug where `TypeAliasType` instances could be subscripted even where they were not generic. Patch by [Daraan](https://github.com/Daraan). +- 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. # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 13ff0c2c..05b3083f 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7251,25 +7251,24 @@ def test_alias_attributes(self): T = TypeVar('T') T2 = TypeVar('T2') ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) - subscripted = ListOrSetT[int] - still_generic = ListOrSetT[Iterable[T2]] - fully_subscripted = still_generic[float] + subscripted = ListOrSetT[int] self.assertEqual(subscripted.__module__, ListOrSetT.__module__) self.assertEqual(subscripted.__name__, "ListOrSetT") self.assertEqual(subscripted.__value__, Union[List[T], Set[T]]) self.assertEqual(subscripted.__type_params__, (T,)) + + still_generic = ListOrSetT[Iterable[T2]] self.assertEqual(still_generic.__module__, ListOrSetT.__module__) self.assertEqual(still_generic.__name__, "ListOrSetT") self.assertEqual(still_generic.__value__, Union[List[T], Set[T]]) self.assertEqual(still_generic.__type_params__, (T,)) - with self.subTest(variable=fully_subscripted): - if not TYPING_3_10_0: # needs to align with GenericAlias usage in __getitem__ else 3.12+ - self.skipTest("Cannot further proxy attributes with _GenericAlias") - self.assertEqual(fully_subscripted.__module__, ListOrSetT.__module__) - self.assertEqual(fully_subscripted.__name__, "ListOrSetT") - self.assertEqual(fully_subscripted.__value__, Union[List[T], Set[T]]) - self.assertEqual(fully_subscripted.__type_params__, (T,)) + + fully_subscripted = still_generic[float] + self.assertEqual(fully_subscripted.__module__, ListOrSetT.__module__) + self.assertEqual(fully_subscripted.__name__, "ListOrSetT") + self.assertEqual(fully_subscripted.__value__, Union[List[T], Set[T]]) + self.assertEqual(fully_subscripted.__type_params__, (T,)) def test_subscription_without_type_params(self): Simple = TypeAliasType("Simple", int) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ecb611f4..1faa3f68 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3452,6 +3452,13 @@ def _is_unionable(obj): TypeAliasType, )) + if sys.version_info < (3, 10): + class _TypeAliasGenericAlias(_typing_GenericAlias): + def __getattr__(self, attr): + if attr in {"__value__", "__type_params__", "__name__"}: + return getattr(self.__origin__, attr) + return super().__getattr__(attr) + class TypeAliasType: """Create named, parameterized type aliases. @@ -3532,17 +3539,13 @@ def __getitem__(self, parameters): # Using 3.9 here will create problems with Concatenate if sys.version_info >= (3, 10): return _types.GenericAlias(self, tuple(parameters)) - parameters = [ + parameters = tuple( typing._type_check( item, f'Subscripting {self.__name__} requires a type.' ) for item in parameters - ] - alias = typing._GenericAlias(self, tuple(parameters)) - alias.__value__ = self.__value__ - alias.__type_params__ = self.__type_params__ - alias.__name__ = self.__name__ - return alias + ) + return _TypeAliasGenericAlias(self, parameters) def __reduce__(self): return self.__name__ From 53df006b782c3eb15d5ae1f70fae485c76618a7b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 26 Sep 2024 09:44:51 -0700 Subject: [PATCH 08/12] fix typo --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1faa3f68..97699ebb 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3453,7 +3453,7 @@ def _is_unionable(obj): )) if sys.version_info < (3, 10): - class _TypeAliasGenericAlias(_typing_GenericAlias): + class _TypeAliasGenericAlias(typing._GenericAlias, _root=True): def __getattr__(self, attr): if attr in {"__value__", "__type_params__", "__name__"}: return getattr(self.__origin__, attr) From bad227d513509b7a0e8ea6d31e55b2bb8435efea Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 26 Sep 2024 09:50:39 -0700 Subject: [PATCH 09/12] fix py38 --- src/typing_extensions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 97699ebb..742692e9 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3459,6 +3459,12 @@ def __getattr__(self, attr): return getattr(self.__origin__, attr) return super().__getattr__(attr) + if sys.version_info < (3, 9): + def __getitem__(self, item): + result = super().__getitem__(item) + result.__class__ = type(self) + return result + class TypeAliasType: """Create named, parameterized type aliases. From c216119d1f2154e3375e68302f5002d858822e6e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 26 Sep 2024 09:52:19 -0700 Subject: [PATCH 10/12] remove unnecessary tuple cast --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 742692e9..befd32b1 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3544,7 +3544,7 @@ def __getitem__(self, parameters): parameters = (parameters,) # Using 3.9 here will create problems with Concatenate if sys.version_info >= (3, 10): - return _types.GenericAlias(self, tuple(parameters)) + return _types.GenericAlias(self, parameters) parameters = tuple( typing._type_check( item, f'Subscripting {self.__name__} requires a type.' From c4b415b1eeb7e4a1c45983789ca5e05ba9d94c06 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 26 Sep 2024 10:01:17 -0700 Subject: [PATCH 11/12] Emulate `types.GenericAlias` rather than `typing._GenericAlias` --- src/typing_extensions.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index befd32b1..04980662 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3453,11 +3453,28 @@ def _is_unionable(obj): )) if sys.version_info < (3, 10): + # Copied and pasted from https://github.com/python/cpython/blob/986a4e1b6fcae7fe7a1d0a26aea446107dd58dd2/Objects/genericaliasobject.c#L568-L582, + # so that we emulate the behaviour of `types.GenericAlias` on the latest versions of CPython + _ATTRIBUTE_DELEGATION_EXCLUSIONS = frozenset({ + "__class__", + "__bases__", + "__origin__", + "__args__", + "__unpacked__", + "__parameters__", + "__typing_unpacked_tuple_args__", + "__mro_entries__", + "__reduce_ex__", + "__reduce__", + "__copy__", + "__deepcopy__", + }) + class _TypeAliasGenericAlias(typing._GenericAlias, _root=True): def __getattr__(self, attr): - if attr in {"__value__", "__type_params__", "__name__"}: - return getattr(self.__origin__, attr) - return super().__getattr__(attr) + if attr in _ATTRIBUTE_DELEGATION_EXCLUSIONS: + return object.__getattr__(self, attr) + return getattr(self.__origin__, attr) if sys.version_info < (3, 9): def __getitem__(self, item): From c147b6fddfa3874a36ca83af892629c8c21ab27f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 26 Sep 2024 10:02:27 -0700 Subject: [PATCH 12/12] ruff --- src/typing_extensions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 04980662..13bd5442 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3454,7 +3454,8 @@ def _is_unionable(obj): if sys.version_info < (3, 10): # Copied and pasted from https://github.com/python/cpython/blob/986a4e1b6fcae7fe7a1d0a26aea446107dd58dd2/Objects/genericaliasobject.c#L568-L582, - # so that we emulate the behaviour of `types.GenericAlias` on the latest versions of CPython + # so that we emulate the behaviour of `types.GenericAlias` + # on the latest versions of CPython _ATTRIBUTE_DELEGATION_EXCLUSIONS = frozenset({ "__class__", "__bases__",