From eee1c7745ab4eb4f75153e71aaa2a62018b7625a Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 24 Dec 2020 01:45:13 +0300 Subject: bpo-41960: Add globalns and localns parameters to inspect.signature and Signature.from_callable (GH-22583) --- Doc/library/inspect.rst | 22 +++++- Doc/whatsnew/3.10.rst | 5 ++ Lib/inspect.py | 80 +++++++++------------- Lib/test/test_inspect.py | 20 ++++++ .../2020-10-06-23-59-20.bpo-41960.icQ7Xd.rst | 2 + 5 files changed, 77 insertions(+), 52 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-10-06-23-59-20.bpo-41960.icQ7Xd.rst diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index b53a942..850d601 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 `. @@ -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 a6f9b0b..b5fb1e9 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 9150ac1..70c5ef7 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2137,9 +2137,9 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True): 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 @@ class Signature: 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 @@ class Signature: 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 c81d828..706fcbe 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3250,6 +3250,26 @@ class TestSignatureObject(unittest.TestCase): 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 0000000..f7e7199 --- /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`. -- cgit v0.12