Skip to content
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

Add TypeAliasType #160

Merged
merged 10 commits into from
May 20, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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.

# Release 4.5.0 (February 14, 2023)

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
59 changes: 57 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 @@ -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()
79 changes: 79 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 @@ -2610,3 +2611,81 @@ 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)').

"""
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
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
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.

The PEP specifies that these attributes should be read-only, so shouldn't they be read-only properties here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I suppose we could do that, but it feels unnecessarily complicated.

Copy link
Member

Choose a reason for hiding this comment

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

I don't feel strongly, but it doesn't feel that complicated to me, and I think it would be nice to match CPython on 3.12+ here 🤷‍♂️


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)


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__

# 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]