From 4c23aff065fb28aba789a211937a2af974842110 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 31 May 2019 00:10:07 +0100 Subject: bpo-29262: Add get_origin() and get_args() introspection helpers to typing (GH-13685) This is an old feature request that appears from time to time. After a year of experimenting with various introspection capabilities in `typing_inspect` on PyPI, I propose to add these two most commonly used functions: `get_origin()` and `get_args()`. These are essentially thin public wrappers around private APIs: `__origin__` and `__args__`. As discussed in the issue and on the typing tracker, exposing some public helpers instead of `__origin__` and `__args__` directly will give us more flexibility if we will decide to update the internal representation, while still maintaining backwards compatibility. The implementation is very simple an is essentially a copy from `typing_inspect` with one exception: `ClassVar` was special-cased in `typing_inspect`, but I think this special-casing doesn't really help and only makes things more complicated. --- Doc/library/typing.rst | 19 ++++++++++ Lib/test/test_typing.py | 37 +++++++++++++++++++ Lib/typing.py | 42 ++++++++++++++++++++++ .../2019-05-30-21-25-14.bpo-29262.LdIzun.rst | 1 + 4 files changed, 99 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2019-05-30-21-25-14.bpo-29262.LdIzun.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 709580a..2575a99 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1021,6 +1021,25 @@ The module defines the following classes, functions and decorators: a dictionary constructed by merging all the ``__annotations__`` along ``C.__mro__`` in reverse order. +.. function:: get_origin(typ) +.. function:: get_args(typ) + + Provide basic introspection for generic types and special typing forms. + + For a typing object of the form ``X[Y, Z, ...]`` these functions return + ``X`` and ``(Y, Z, ...)``. If ``X`` is a generic alias for a builtin or + :mod:`collections` class, it gets normalized to the original class. + For unsupported objects return ``None`` and ``()`` correspondingly. + Examples:: + + assert get_origin(Dict[str, int]) is dict + assert get_args(Dict[int, str]) == (int, str) + + assert get_origin(Union[int, str]) is Union + assert get_args(Union[int, str]) == (int, str) + + .. versionadded:: 3.8 + .. decorator:: overload The ``@overload`` decorator allows describing functions and methods diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f9c18c8..a65d639 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -15,6 +15,7 @@ from typing import Callable from typing import Generic, ClassVar, Final, final, Protocol from typing import cast, runtime_checkable from typing import get_type_hints +from typing import get_origin, get_args from typing import no_type_check, no_type_check_decorator from typing import Type from typing import NewType @@ -2735,6 +2736,42 @@ class GetTypeHintTests(BaseTestCase): self.assertEqual(gth(G), {'lst': ClassVar[List[T]]}) +class GetUtilitiesTestCase(TestCase): + def test_get_origin(self): + T = TypeVar('T') + class C(Generic[T]): pass + self.assertIs(get_origin(C[int]), C) + self.assertIs(get_origin(C[T]), C) + self.assertIs(get_origin(int), None) + self.assertIs(get_origin(ClassVar[int]), ClassVar) + self.assertIs(get_origin(Union[int, str]), Union) + self.assertIs(get_origin(Literal[42, 43]), Literal) + self.assertIs(get_origin(Final[List[int]]), Final) + self.assertIs(get_origin(Generic), Generic) + self.assertIs(get_origin(Generic[T]), Generic) + self.assertIs(get_origin(List[Tuple[T, T]][int]), list) + + def test_get_args(self): + T = TypeVar('T') + class C(Generic[T]): pass + self.assertEqual(get_args(C[int]), (int,)) + self.assertEqual(get_args(C[T]), (T,)) + self.assertEqual(get_args(int), ()) + self.assertEqual(get_args(ClassVar[int]), (int,)) + self.assertEqual(get_args(Union[int, str]), (int, str)) + self.assertEqual(get_args(Literal[42, 43]), (42, 43)) + self.assertEqual(get_args(Final[List[int]]), (List[int],)) + self.assertEqual(get_args(Union[int, Tuple[T, int]][str]), + (int, Tuple[str, int])) + self.assertEqual(get_args(typing.Dict[int, Tuple[T, T]][Optional[int]]), + (int, Tuple[Optional[int], Optional[int]])) + self.assertEqual(get_args(Callable[[], T][int]), ([], int,)) + self.assertEqual(get_args(Union[int, Callable[[Tuple[T, ...]], str]]), + (int, Callable[[Tuple[T, ...]], str])) + self.assertEqual(get_args(Tuple[int, ...]), (int, ...)) + self.assertEqual(get_args(Tuple[()]), ((),)) + + class CollectionsAbcTests(BaseTestCase): def test_hashable(self): diff --git a/Lib/typing.py b/Lib/typing.py index 3b4e9df..16ccfad 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -99,6 +99,8 @@ __all__ = [ 'AnyStr', 'cast', 'final', + 'get_args', + 'get_origin', 'get_type_hints', 'NewType', 'no_type_check', @@ -1253,6 +1255,46 @@ def get_type_hints(obj, globalns=None, localns=None): return hints +def get_origin(tp): + """Get the unsubscripted version of a type. + + This supports generic types, Callable, Tuple, Union, Literal, Final and ClassVar. + Return None for unsupported types. Examples:: + + get_origin(Literal[42]) is Literal + get_origin(int) is None + get_origin(ClassVar[int]) is ClassVar + get_origin(Generic) is Generic + get_origin(Generic[T]) is Generic + get_origin(Union[T, int]) is Union + get_origin(List[Tuple[T, T]][int]) == list + """ + if isinstance(tp, _GenericAlias): + return tp.__origin__ + if tp is Generic: + return Generic + return None + + +def get_args(tp): + """Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples:: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + if isinstance(tp, _GenericAlias): + res = tp.__args__ + if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + return () + + def no_type_check(arg): """Decorator to indicate that annotations are not type hints. diff --git a/Misc/NEWS.d/next/Library/2019-05-30-21-25-14.bpo-29262.LdIzun.rst b/Misc/NEWS.d/next/Library/2019-05-30-21-25-14.bpo-29262.LdIzun.rst new file mode 100644 index 0000000..e1154ef --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-30-21-25-14.bpo-29262.LdIzun.rst @@ -0,0 +1 @@ +Add ``get_origin()`` and ``get_args()`` introspection helpers to ``typing`` module. \ No newline at end of file -- cgit v0.12