[Python-checkins] bpo-41960: Add globalns and localns parameters to inspect.signature and Signature.from_callable (GH-22583)

isidentical webhook-mailer at python.org
Wed Dec 23 17:45:27 EST 2020


https://github.com/python/cpython/commit/eee1c7745ab4eb4f75153e71aaa2a62018b7625a
commit: eee1c7745ab4eb4f75153e71aaa2a62018b7625a
branch: master
author: Batuhan Taskaya <isidentical at gmail.com>
committer: isidentical <isidentical at gmail.com>
date: 2020-12-24T01:45:13+03:00
summary:

bpo-41960: Add globalns and localns parameters to inspect.signature and Signature.from_callable (GH-22583)

files:
A Misc/NEWS.d/next/Library/2020-10-06-23-59-20.bpo-41960.icQ7Xd.rst
M Doc/library/inspect.rst
M Doc/whatsnew/3.10.rst
M Lib/inspect.py
M Lib/test/test_inspect.py

diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index b53a9421fbca6..850d6018bab1f 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -556,7 +556,7 @@ The Signature object represents the call signature of a callable object and its
 return annotation.  To retrieve a Signature object, use the :func:`signature`
 function.
 
-.. function:: signature(callable, *, follow_wrapped=True)
+.. function:: signature(callable, *, follow_wrapped=True, globalns=None, localns=None)
 
    Return a :class:`Signature` object for the given ``callable``::
 
@@ -581,6 +581,9 @@ function.
    Raises :exc:`ValueError` if no signature can be provided, and
    :exc:`TypeError` if that type of object is not supported.
 
+   ``globalns`` and ``localns`` are passed into
+   :func:`typing.get_type_hints` when resolving the annotations.
+
    A slash(/) in the signature of a function denotes that the parameters prior
    to it are positional-only. For more info, see
    :ref:`the FAQ entry on positional-only parameters <faq-positional-only-arguments>`.
@@ -590,12 +593,21 @@ function.
       ``callable`` specifically (``callable.__wrapped__`` will not be used to
       unwrap decorated callables.)
 
+   .. versionadded:: 3.10
+      ``globalns`` and ``localns`` parameters.
+
    .. note::
 
       Some callables may not be introspectable in certain implementations of
       Python.  For example, in CPython, some built-in functions defined in
       C provide no metadata about their arguments.
 
+   .. note::
+
+      Will first try to resolve the annotations, but when it fails and
+      encounters with an error while that operation, the annotations will be
+      returned unchanged (as strings).
+
 
 .. class:: Signature(parameters=None, *, return_annotation=Signature.empty)
 
@@ -668,11 +680,12 @@ function.
          >>> str(new_sig)
          "(a, b) -> 'new return anno'"
 
-   .. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True)
+   .. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, globalns=None, localns=None)
 
        Return a :class:`Signature` (or its subclass) object for a given callable
        ``obj``.  Pass ``follow_wrapped=False`` to get a signature of ``obj``
-       without unwrapping its ``__wrapped__`` chain.
+       without unwrapping its ``__wrapped__`` chain. ``globalns`` and
+       ``localns`` will be used as the namespaces when resolving annotations.
 
        This method simplifies subclassing of :class:`Signature`::
 
@@ -683,6 +696,9 @@ function.
 
        .. versionadded:: 3.5
 
+       .. versionadded:: 3.10
+          ``globalns`` and ``localns`` parameters.
+
 
 .. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty)
 
diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst
index a6f9b0b1754d2..b5fb1e9a629c1 100644
--- a/Doc/whatsnew/3.10.rst
+++ b/Doc/whatsnew/3.10.rst
@@ -238,6 +238,11 @@ inspect
 When a module does not define ``__loader__``, fall back to ``__spec__.loader``.
 (Contributed by Brett Cannon in :issue:`42133`.)
 
+Added *globalns* and *localns* parameters in :func:`~inspect.signature` and
+:meth:`inspect.Signature.from_callable` to retrieve the annotations in given
+local and global namespaces.
+(Contributed by Batuhan Taskaya in :issue:`41960`.)
+
 linecache
 ---------
 
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 9150ac104dcb7..70c5ef7bb6471 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -2137,9 +2137,9 @@ def p(name_node, default_node, default=empty):
 
     return cls(parameters, return_annotation=cls.empty)
 
-def _get_type_hints(func):
+def _get_type_hints(func, **kwargs):
     try:
-        return typing.get_type_hints(func)
+        return typing.get_type_hints(func, **kwargs)
     except Exception:
         # First, try to use the get_type_hints to resolve
         # annotations. But for keeping the behavior intact
@@ -2164,7 +2164,8 @@ def _signature_from_builtin(cls, func, skip_bound_arg=True):
     return _signature_fromstr(cls, func, s, skip_bound_arg)
 
 
-def _signature_from_function(cls, func, skip_bound_arg=True):
+def _signature_from_function(cls, func, skip_bound_arg=True,
+                             globalns=None, localns=None):
     """Private helper: constructs Signature for the given python function."""
 
     is_duck_function = False
@@ -2190,7 +2191,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True):
     positional = arg_names[:pos_count]
     keyword_only_count = func_code.co_kwonlyargcount
     keyword_only = arg_names[pos_count:pos_count + keyword_only_count]
-    annotations = _get_type_hints(func)
+    annotations = _get_type_hints(func, globalns=globalns, localns=localns)
 
     defaults = func.__defaults__
     kwdefaults = func.__kwdefaults__
@@ -2262,23 +2263,28 @@ def _signature_from_function(cls, func, skip_bound_arg=True):
 def _signature_from_callable(obj, *,
                              follow_wrapper_chains=True,
                              skip_bound_arg=True,
+                             globalns=None,
+                             localns=None,
                              sigcls):
 
     """Private helper function to get signature for arbitrary
     callable objects.
     """
 
+    _get_signature_of = functools.partial(_signature_from_callable,
+                                follow_wrapper_chains=follow_wrapper_chains,
+                                skip_bound_arg=skip_bound_arg,
+                                globalns=globalns,
+                                localns=localns,
+                                sigcls=sigcls)
+
     if not callable(obj):
         raise TypeError('{!r} is not a callable object'.format(obj))
 
     if isinstance(obj, types.MethodType):
         # In this case we skip the first parameter of the underlying
         # function (usually `self` or `cls`).
-        sig = _signature_from_callable(
-            obj.__func__,
-            follow_wrapper_chains=follow_wrapper_chains,
-            skip_bound_arg=skip_bound_arg,
-            sigcls=sigcls)
+        sig = _get_signature_of(obj.__func__)
 
         if skip_bound_arg:
             return _signature_bound_method(sig)
@@ -2292,11 +2298,7 @@ def _signature_from_callable(obj, *,
             # If the unwrapped object is a *method*, we might want to
             # skip its first parameter (self).
             # See test_signature_wrapped_bound_method for details.
-            return _signature_from_callable(
-                obj,
-                follow_wrapper_chains=follow_wrapper_chains,
-                skip_bound_arg=skip_bound_arg,
-                sigcls=sigcls)
+            return _get_signature_of(obj)
 
     try:
         sig = obj.__signature__
@@ -2323,11 +2325,7 @@ def _signature_from_callable(obj, *,
             # (usually `self`, or `cls`) will not be passed
             # automatically (as for boundmethods)
 
-            wrapped_sig = _signature_from_callable(
-                partialmethod.func,
-                follow_wrapper_chains=follow_wrapper_chains,
-                skip_bound_arg=skip_bound_arg,
-                sigcls=sigcls)
+            wrapped_sig = _get_signature_of(partialmethod.func)
 
             sig = _signature_get_partial(wrapped_sig, partialmethod, (None,))
             first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]
@@ -2346,18 +2344,15 @@ def _signature_from_callable(obj, *,
         # If it's a pure Python function, or an object that is duck type
         # of a Python function (Cython functions, for instance), then:
         return _signature_from_function(sigcls, obj,
-                                        skip_bound_arg=skip_bound_arg)
+                                        skip_bound_arg=skip_bound_arg,
+                                        globalns=globalns, localns=localns)
 
     if _signature_is_builtin(obj):
         return _signature_from_builtin(sigcls, obj,
                                        skip_bound_arg=skip_bound_arg)
 
     if isinstance(obj, functools.partial):
-        wrapped_sig = _signature_from_callable(
-            obj.func,
-            follow_wrapper_chains=follow_wrapper_chains,
-            skip_bound_arg=skip_bound_arg,
-            sigcls=sigcls)
+        wrapped_sig = _get_signature_of(obj.func)
         return _signature_get_partial(wrapped_sig, obj)
 
     sig = None
@@ -2368,29 +2363,17 @@ def _signature_from_callable(obj, *,
         # in its metaclass
         call = _signature_get_user_defined_method(type(obj), '__call__')
         if call is not None:
-            sig = _signature_from_callable(
-                call,
-                follow_wrapper_chains=follow_wrapper_chains,
-                skip_bound_arg=skip_bound_arg,
-                sigcls=sigcls)
+            sig = _get_signature_of(call)
         else:
             # Now we check if the 'obj' class has a '__new__' method
             new = _signature_get_user_defined_method(obj, '__new__')
             if new is not None:
-                sig = _signature_from_callable(
-                    new,
-                    follow_wrapper_chains=follow_wrapper_chains,
-                    skip_bound_arg=skip_bound_arg,
-                    sigcls=sigcls)
+                sig = _get_signature_of(new)
             else:
                 # Finally, we should have at least __init__ implemented
                 init = _signature_get_user_defined_method(obj, '__init__')
                 if init is not None:
-                    sig = _signature_from_callable(
-                        init,
-                        follow_wrapper_chains=follow_wrapper_chains,
-                        skip_bound_arg=skip_bound_arg,
-                        sigcls=sigcls)
+                    sig = _get_signature_of(init)
 
         if sig is None:
             # At this point we know, that `obj` is a class, with no user-
@@ -2436,11 +2419,7 @@ def _signature_from_callable(obj, *,
         call = _signature_get_user_defined_method(type(obj), '__call__')
         if call is not None:
             try:
-                sig = _signature_from_callable(
-                    call,
-                    follow_wrapper_chains=follow_wrapper_chains,
-                    skip_bound_arg=skip_bound_arg,
-                    sigcls=sigcls)
+                sig = _get_signature_of(call)
             except ValueError as ex:
                 msg = 'no signature found for {!r}'.format(obj)
                 raise ValueError(msg) from ex
@@ -2892,10 +2871,12 @@ def from_builtin(cls, func):
         return _signature_from_builtin(cls, func)
 
     @classmethod
-    def from_callable(cls, obj, *, follow_wrapped=True):
+    def from_callable(cls, obj, *,
+                      follow_wrapped=True, globalns=None, localns=None):
         """Constructs Signature for the given callable object."""
         return _signature_from_callable(obj, sigcls=cls,
-                                        follow_wrapper_chains=follow_wrapped)
+                                        follow_wrapper_chains=follow_wrapped,
+                                        globalns=globalns, localns=localns)
 
     @property
     def parameters(self):
@@ -3143,9 +3124,10 @@ def __str__(self):
         return rendered
 
 
-def signature(obj, *, follow_wrapped=True):
+def signature(obj, *, follow_wrapped=True, globalns=None, localns=None):
     """Get a signature object for the passed callable."""
-    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
+    return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
+                                   globalns=globalns, localns=localns)
 
 
 def _main():
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index c81d828b57ece..706fcbe3439b4 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -3250,6 +3250,26 @@ def test_signater_parameters_is_ordered(self):
         p2 = inspect.signature(lambda y, x: None).parameters
         self.assertNotEqual(p1, p2)
 
+    def test_signature_annotations_with_local_namespaces(self):
+        class Foo: ...
+        def func(foo: Foo) -> int: pass
+        def func2(foo: Foo, bar: Bar) -> int: pass
+
+        for signature_func in (inspect.signature, inspect.Signature.from_callable):
+            with self.subTest(signature_func = signature_func):
+                sig1 = signature_func(func)
+                self.assertEqual(sig1.return_annotation, 'int')
+                self.assertEqual(sig1.parameters['foo'].annotation, 'Foo')
+
+                sig2 = signature_func(func, localns=locals())
+                self.assertEqual(sig2.return_annotation, int)
+                self.assertEqual(sig2.parameters['foo'].annotation, Foo)
+
+                sig3 = signature_func(func2, globalns={'Bar': int}, localns=locals())
+                self.assertEqual(sig3.return_annotation, int)
+                self.assertEqual(sig3.parameters['foo'].annotation, Foo)
+                self.assertEqual(sig3.parameters['bar'].annotation, int)
+
 
 class TestParameterObject(unittest.TestCase):
     def test_signature_parameter_kinds(self):
diff --git a/Misc/NEWS.d/next/Library/2020-10-06-23-59-20.bpo-41960.icQ7Xd.rst b/Misc/NEWS.d/next/Library/2020-10-06-23-59-20.bpo-41960.icQ7Xd.rst
new file mode 100644
index 0000000000000..f7e71998dab9b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-10-06-23-59-20.bpo-41960.icQ7Xd.rst
@@ -0,0 +1,2 @@
+Add ``globalns`` and ``localns`` parameters to the :func:`inspect.signature`
+and :meth:`inspect.Signature.from_callable`.



More information about the Python-checkins mailing list