[Python-checkins] bpo-29262: Add get_origin() and get_args() introspection helpers to typing (GH-13685)

Ivan Levkivskyi webhook-mailer at python.org
Thu May 30 19:10:16 EDT 2019


https://github.com/python/cpython/commit/4c23aff065fb28aba789a211937a2af974842110
commit: 4c23aff065fb28aba789a211937a2af974842110
branch: master
author: Ivan Levkivskyi <levkivskyi at gmail.com>
committer: GitHub <noreply at github.com>
date: 2019-05-31T00:10:07+01:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2019-05-30-21-25-14.bpo-29262.LdIzun.rst
M Doc/library/typing.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 709580ad2159..2575a995817d 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 f9c18c84c8f9..a65d639fe9e1 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -15,6 +15,7 @@
 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 @@ def test_get_type_hints_ClassVar(self):
         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 3b4e9df0482e..16ccfad049f4 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -99,6 +99,8 @@
     '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 000000000000..e1154ef575a5
--- /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



More information about the Python-checkins mailing list