From 5b556b821b355fa89ce1992b0ae42aca38e55ff5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 14 May 2023 15:45:21 -0700 Subject: [PATCH 1/9] Add TypeAliasType --- CHANGELOG.md | 4 +- README.md | 1 + src/test_typing_extensions.py | 59 ++++++++++++++++++++++++++++- src/typing_extensions.py | 70 +++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc5abe74..0cc06410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ using the new release, and vice versa. Most users are unlikely to be affected by this change. Patch by Alex Waygood. - Backport the ability to define `__init__` methods on Protocol classes, a - change made in Python 3.11 (originally implemented in + change made in Python 3.11 (originally implemented in https://github.com/python/cpython/pull/31628 by Adrian Garcia Badaracco). Patch by Alex Waygood. - Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python @@ -73,6 +73,8 @@ - Backport the implementation of `NewType` from 3.10 (where it is implemented as a class rather than a function). This allows user-defined `NewType`s to be pickled. Patch by Alex Waygood. +- Add `typing_extensions.TypeAliasType`, a backport of `typing.TypeAliasType` + from PEP 695. Patch by Jelle Zijlstra. # Release 4.5.0 (February 14, 2023) diff --git a/README.md b/README.md index 11434d18..9c9a5b97 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ This module currently contains the following: - In the standard library since Python 3.12 - `override` (equivalent to `typing.override`; see [PEP 698](https://peps.python.org/pep-0698/)) + - `TypeAliasType` (equivalent to `typing.TypeAliasType`; see [PEP 695](https://peps.python.org/pep-0698/)) - `Buffer` (equivalent to `collections.abc.Buffer`; see [PEP 688](https://peps.python.org/pep-0688/)) - `get_original_bases` (equivalent to [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 469c31b6..c02defa5 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -21,7 +21,7 @@ import typing from typing import TypeVar, Optional, Union, AnyStr from typing import T, KT, VT # Not in __all__. -from typing import Tuple, List, Dict, Iterable, Iterator, Callable +from typing import Tuple, List, Set, Dict, Iterable, Iterator, Callable from typing import Generic from typing import no_type_check import warnings @@ -35,7 +35,7 @@ from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple -from typing_extensions import override, deprecated, Buffer +from typing_extensions import override, deprecated, Buffer, TypeAliasType from _typed_dict_test_helper import Foo, FooGeneric # Flags used to mark tests that only apply after a specific @@ -4440,5 +4440,60 @@ class GenericTypedDict(TypedDict, Generic[T]): ) +class TypeAliasTypeTests(BaseTestCase): + def test_attributes(self): + Simple = TypeAliasType("Simple", int) + self.assertEqual(Simple.__name__, "Simple") + self.assertIs(Simple.__value__, int) + self.assertEqual(Simple.__type_params__, ()) + self.assertEqual(Simple.__parameters__, ()) + + T = TypeVar("T") + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + self.assertEqual(ListOrSetT.__name__, "ListOrSetT") + self.assertEqual(ListOrSetT.__value__, Union[List[T], Set[T]]) + self.assertEqual(ListOrSetT.__type_params__, (T,)) + self.assertEqual(ListOrSetT.__parameters__, (T,)) + + Ts = TypeVarTuple("Ts") + Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,)) + self.assertEqual(Variadic.__name__, "Variadic") + self.assertEqual(Variadic.__value__, Tuple[int, Unpack[Ts]]) + self.assertEqual(Variadic.__type_params__, (Ts,)) + self.assertEqual(Variadic.__parameters__, (Unpack[Ts],)) + + def test_or(self): + Alias = TypeAliasType("Alias", int) + if sys.version_info >= (3, 10): + self.assertEqual(Alias | "Ref", Union[Alias, typing.ForwardRef("Ref")]) + else: + with self.assertRaises(TypeError): + Alias | "Ref" + + def test_getitem(self): + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + subscripted = ListOrSetT[int] + self.assertEqual(get_args(subscripted), (int,)) + self.assertIs(get_origin(subscripted), ListOrSetT) + with self.assertRaises(TypeError): + subscripted[str] + + still_generic = ListOrSetT[Iterable[T]] + self.assertEqual(get_args(still_generic), (Iterable[T],)) + self.assertIs(get_origin(still_generic), ListOrSetT) + fully_subscripted = still_generic[float] + self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) + self.assertIs(get_origin(fully_subscripted), ListOrSetT) + + def test_pickle(self): + global Alias + Alias = TypeAliasType("Alias", int) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + pickled = pickle.dumps(Alias, proto) + unpickled = pickle.loads(pickled) + self.assertIs(unpickled, Alias) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index dd12cfb8..406eb943 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -79,6 +79,7 @@ 'runtime_checkable', 'Text', 'TypeAlias', + 'TypeAliasType', 'TypeGuard', 'TYPE_CHECKING', 'Never', @@ -2610,3 +2611,72 @@ def __or__(self, other): def __ror__(self, other): return typing.Union[other, self] + + +if hasattr(typing, "TypeAliasType"): + TypeAliasType = typing.TypeAliasType +else: + class TypeAliasType: + """Create named, parameterized type aliases. + + This provides a backport of the new `type` statement in Python 3.12: + + type ListOrSet[T] = list[T] | set[T] + + is equivalent to: + + T = TypeVar("T") + ListOrSet = TypeAliasType("ListOrSet", list[T] | set[T], type_params=(T,)) + + The name ListOrSet can then be used as an alias for the type it refers to. + + The type_params argument should contain all the type parameters used + in the value of the type alias. If the alias is not generic, this + argument is omitted. + + Static type checkers should only support type aliases declared using + TypeAliasType that follow these rules: + + - The first argument (the name) must be a string literal. + - The TypeAliasType instance must be immediately assigned to a variable + of the same name. (For example, 'X = TypeAliasType("Y", int)' is invalid, + as is 'X, Y = TypeAliasType("X", int), TypeAliasType("Y", int)'). + + """ + def __init__(self, name: str, value, *, type_params=()): + if not isinstance(name, str): + raise TypeError("TypeAliasType name must be a string") + self.__name__ = name + self.__value__ = value + self.__type_params__ = type_params + + parameters = [] + for type_param in type_params: + if isinstance(type_param, TypeVarTuple): + parameters.extend(type_param) + else: + parameters.append(type_param) + self.__parameters__ = tuple(parameters) + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + def __repr__(self) -> str: + return self.__name__ + + def __getitem__(self, parameters): + if not isinstance(parameters, tuple): + parameters = (parameters,) + parameters = [typing._type_check(item, f'Subscripting {self.__name__} requires a type.') + for item in parameters] + return typing._GenericAlias(self, tuple(parameters)) + + def __reduce__(self): + return self.__name__ + + if sys.version_info >= (3, 10): + def __or__(self, right): + return typing.Union[self, right] + + def __ror__(self, left): + return typing.Union[left, self] From d11a723f874d438a047d08dd50109168b31e8c3c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 14 May 2023 15:49:09 -0700 Subject: [PATCH 2/9] CI fixes --- src/typing_extensions.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 406eb943..cda98d2e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2667,13 +2667,22 @@ def __repr__(self) -> str: def __getitem__(self, parameters): if not isinstance(parameters, tuple): parameters = (parameters,) - parameters = [typing._type_check(item, f'Subscripting {self.__name__} requires a type.') - for item in parameters] + parameters = [ + typing._type_check( + item, f'Subscripting {self.__name__} requires a type.' + ) + for item in parameters + ] return typing._GenericAlias(self, tuple(parameters)) def __reduce__(self): return self.__name__ + # The presence of this method convinces typing._type_check + # that TypeAliasTypes are types. + def __call__(self): + raise TypeError("Type alias is not callable") + if sys.version_info >= (3, 10): def __or__(self, right): return typing.Union[self, right] From cbdfbfa7e2bbe7ea919ae4baa102988cfd1dd1ac Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 15 May 2023 05:59:20 -0700 Subject: [PATCH 3/9] Update README.md Co-authored-by: Sebastian Rittau --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c9a5b97..44c2571a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ This module currently contains the following: - In the standard library since Python 3.12 - `override` (equivalent to `typing.override`; see [PEP 698](https://peps.python.org/pep-0698/)) - - `TypeAliasType` (equivalent to `typing.TypeAliasType`; see [PEP 695](https://peps.python.org/pep-0698/)) + - `TypeAliasType` (equivalent to `typing.TypeAliasType`; see [PEP 695](https://peps.python.org/pep-0695/)) - `Buffer` (equivalent to `collections.abc.Buffer`; see [PEP 688](https://peps.python.org/pep-0688/)) - `get_original_bases` (equivalent to [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases) From fdd32d4d3b7ee897ab7888d52952b6f68eb9701b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 15 May 2023 06:47:37 -0700 Subject: [PATCH 4/9] No subclassing --- src/test_typing_extensions.py | 5 +++++ src/typing_extensions.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index c02defa5..b447c884 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4494,6 +4494,11 @@ def test_pickle(self): unpickled = pickle.loads(pickled) self.assertIs(unpickled, Alias) + def test_no_instance_subclassing(self): + with self.assertRaises(TypeError): + class MyAlias(TypeAliasType): + pass + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index cda98d2e..1027b039 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2678,6 +2678,11 @@ def __getitem__(self, parameters): def __reduce__(self): return self.__name__ + def __init_subclass__(cls, *args, **kwargs): + raise TypeError( + "type 'typing_extensions.TypeAliasType' is not an acceptable base type" + ) + # The presence of this method convinces typing._type_check # that TypeAliasTypes are types. def __call__(self): From 5c5267ad205bdca96220328104dbea385d8a78d8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 19 May 2023 15:24:49 -0700 Subject: [PATCH 5/9] Update src/typing_extensions.py Co-authored-by: Alex Waygood --- src/typing_extensions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1027b039..a9b6036e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2643,6 +2643,10 @@ class TypeAliasType: as is 'X, Y = TypeAliasType("X", int), TypeAliasType("Y", int)'). """ + __slots__ = ( + "__name__", "__value__", "__type_params__", "__parameters__", "__module__" + ) + def __init__(self, name: str, value, *, type_params=()): if not isinstance(name, str): raise TypeError("TypeAliasType name must be a string") From 6f0f4f44308dc7a8fed553229289e5e9c9c0a133 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 19 May 2023 17:16:31 -0700 Subject: [PATCH 6/9] fix --- src/test_typing_extensions.py | 15 ++++++++++++++- src/typing_extensions.py | 12 ++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b0806a55..8d22eda1 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4573,7 +4573,20 @@ def test_attributes(self): self.assertEqual(Variadic.__name__, "Variadic") self.assertEqual(Variadic.__value__, Tuple[int, Unpack[Ts]]) self.assertEqual(Variadic.__type_params__, (Ts,)) - self.assertEqual(Variadic.__parameters__, (Unpack[Ts],)) + self.assertEqual(Variadic.__parameters__, tuple(iter(Ts))) + + def test_immutable(self): + Simple = TypeAliasType("Simple", int) + with self.assertRaisesRegex(TypeError, "cannot be modified"): + Simple.__name__ = "NewName" + with self.assertRaisesRegex(TypeError, "cannot be modified"): + Simple.__value__ = str + with self.assertRaisesRegex(TypeError, "cannot be modified"): + Simple.__type_params__ = (T,) + with self.assertRaisesRegex(TypeError, "cannot be modified"): + Simple.__parameters__ = (T,) + with self.assertRaisesRegex(TypeError, "cannot be modified"): + Simple.some_attribute = "not allowed" def test_or(self): Alias = TypeAliasType("Alias", int) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index a754566c..c0e8baae 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -7,6 +7,7 @@ import sys import types as _types import typing +from typing import Any import warnings @@ -2679,14 +2680,10 @@ class TypeAliasType: as is 'X, Y = TypeAliasType("X", int), TypeAliasType("Y", int)'). """ - __slots__ = ( - "__name__", "__value__", "__type_params__", "__parameters__", "__module__" - ) def __init__(self, name: str, value, *, type_params=()): if not isinstance(name, str): raise TypeError("TypeAliasType name must be a string") - self.__name__ = name self.__value__ = value self.__type_params__ = type_params @@ -2700,6 +2697,13 @@ def __init__(self, name: str, value, *, type_params=()): def_mod = _caller() if def_mod != 'typing_extensions': self.__module__ = def_mod + # Setting this attribute closes the TypeAliasType from further modification + self.__name__ = name + + def __setattr__(self, __name: str, __value: Any) -> None: + if hasattr(self, "__name__"): + raise TypeError("TypeAliasType cannot be modified") + super().__setattr__(__name, __value) def __repr__(self) -> str: return self.__name__ From facb227c3a475b0e18ba3470e688ad9463577703 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 19 May 2023 17:16:50 -0700 Subject: [PATCH 7/9] object not Any --- src/typing_extensions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c0e8baae..4b35728e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -7,7 +7,6 @@ import sys import types as _types import typing -from typing import Any import warnings @@ -2700,7 +2699,7 @@ def __init__(self, name: str, value, *, type_params=()): # Setting this attribute closes the TypeAliasType from further modification self.__name__ = name - def __setattr__(self, __name: str, __value: Any) -> None: + def __setattr__(self, __name: str, __value: object) -> None: if hasattr(self, "__name__"): raise TypeError("TypeAliasType cannot be modified") super().__setattr__(__name, __value) From a7d36fa482d6273fd13ece849a4fe7e0ea62d7be Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 20 May 2023 06:08:40 -0700 Subject: [PATCH 8/9] Different error, add __delattr__ --- src/test_typing_extensions.py | 14 +++++++++----- src/typing_extensions.py | 5 ++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8d22eda1..dadf6e3c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4577,16 +4577,20 @@ def test_attributes(self): def test_immutable(self): Simple = TypeAliasType("Simple", int) - with self.assertRaisesRegex(TypeError, "cannot be modified"): + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): Simple.__name__ = "NewName" - with self.assertRaisesRegex(TypeError, "cannot be modified"): + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): Simple.__value__ = str - with self.assertRaisesRegex(TypeError, "cannot be modified"): + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): Simple.__type_params__ = (T,) - with self.assertRaisesRegex(TypeError, "cannot be modified"): + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): Simple.__parameters__ = (T,) - with self.assertRaisesRegex(TypeError, "cannot be modified"): + with self.assertRaisesRegex(AttributeError, "Can't set attribute"): Simple.some_attribute = "not allowed" + with self.assertRaisesRegex(AttributeError, "Can't delete attribute"): + del Simple.__name__ + with self.assertRaisesRegex(AttributeError, "Can't delete attribute"): + del Simple.nonexistent_attribute def test_or(self): Alias = TypeAliasType("Alias", int) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4b35728e..01810f07 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2701,9 +2701,12 @@ def __init__(self, name: str, value, *, type_params=()): def __setattr__(self, __name: str, __value: object) -> None: if hasattr(self, "__name__"): - raise TypeError("TypeAliasType cannot be modified") + raise AttributeError(f"Can't set attribute {__name!r} on an instance of TypeAliasType") super().__setattr__(__name, __value) + def __delattr__(self, __name: str) -> None: + raise AttributeError(f"Can't delete attribute {__name!r} on an instance of TypeAliasType") + def __repr__(self) -> str: return self.__name__ From f4fe7f9edbc0d71c14496103cd62934f7d153369 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 20 May 2023 16:20:22 -0700 Subject: [PATCH 9/9] Update src/typing_extensions.py Co-authored-by: Alex Waygood --- src/typing_extensions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 01810f07..82e4ba76 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2701,11 +2701,15 @@ def __init__(self, name: str, value, *, type_params=()): def __setattr__(self, __name: str, __value: object) -> None: if hasattr(self, "__name__"): - raise AttributeError(f"Can't set attribute {__name!r} on an instance of TypeAliasType") + raise AttributeError( + f"Can't set attribute {__name!r} on an instance of TypeAliasType" + ) super().__setattr__(__name, __value) def __delattr__(self, __name: str) -> None: - raise AttributeError(f"Can't delete attribute {__name!r} on an instance of TypeAliasType") + raise AttributeError( + f"Can't delete attribute {__name!r} on an instance of TypeAliasType" + ) def __repr__(self) -> str: return self.__name__