Skip to content

Add TypeAliasType #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
- Backport changes to the repr of `typing.Unpack` that were made in order to
implement [PEP 692](https://peps.python.org/pep-0692/) (backport of
https://github.com/python/cpython/pull/104048). Patch by Alex Waygood.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-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)
Expand Down
81 changes: 79 additions & 2 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -4553,5 +4553,82 @@ 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__, tuple(iter(Ts)))

def test_immutable(self):
Simple = TypeAliasType("Simple", int)
with self.assertRaisesRegex(AttributeError, "Can't set attribute"):
Simple.__name__ = "NewName"
with self.assertRaisesRegex(AttributeError, "Can't set attribute"):
Simple.__value__ = str
with self.assertRaisesRegex(AttributeError, "Can't set attribute"):
Simple.__type_params__ = (T,)
with self.assertRaisesRegex(AttributeError, "Can't set attribute"):
Simple.__parameters__ = (T,)
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)
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)

def test_no_instance_subclassing(self):
with self.assertRaises(TypeError):
class MyAlias(TypeAliasType):
pass
Comment on lines +4627 to +4630
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Might be worth adding a test like this to python/cpython#103764 as well; I couldn't spot one)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.



if __name__ == '__main__':
main()
98 changes: 98 additions & 0 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
'runtime_checkable',
'Text',
'TypeAlias',
'TypeAliasType',
'TypeGuard',
'TYPE_CHECKING',
'Never',
Expand Down Expand Up @@ -2646,3 +2647,100 @@ 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.__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
Comment on lines +2696 to +2698
Copy link
Member

@AlexWaygood AlexWaygood May 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In python/cpython#103764, it looks like __module__ is always "typing", no matter what module the type alias is defined in:

>>> type T = int | str
>>> T.__module__
'typing'

I like the behavior you have in this PR more, but it's more complicated to implement, and doesn't match the CPython PR currently :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Resolved by python/cpython#104550, for future reference)

# Setting this attribute closes the TypeAliasType from further modification
self.__name__ = name

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"
)
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__

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__

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):
raise TypeError("Type alias is not callable")

if sys.version_info >= (3, 10):
def __or__(self, right):
return typing.Union[self, right]

def __ror__(self, left):
return typing.Union[left, self]