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

This commit is contained in:
Batuhan Taskaya 2020-12-24 01:45:13 +03:00 committed by GitHub
parent 6b1ac809b9
commit eee1c7745a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 77 additions and 52 deletions

View File

@ -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` return annotation. To retrieve a Signature object, use the :func:`signature`
function. 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``:: Return a :class:`Signature` object for the given ``callable``::
@ -581,6 +581,9 @@ function.
Raises :exc:`ValueError` if no signature can be provided, and Raises :exc:`ValueError` if no signature can be provided, and
:exc:`TypeError` if that type of object is not supported. :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 A slash(/) in the signature of a function denotes that the parameters prior
to it are positional-only. For more info, see to it are positional-only. For more info, see
:ref:`the FAQ entry on positional-only parameters <faq-positional-only-arguments>`. :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 ``callable`` specifically (``callable.__wrapped__`` will not be used to
unwrap decorated callables.) unwrap decorated callables.)
.. versionadded:: 3.10
``globalns`` and ``localns`` parameters.
.. note:: .. note::
Some callables may not be introspectable in certain implementations of Some callables may not be introspectable in certain implementations of
Python. For example, in CPython, some built-in functions defined in Python. For example, in CPython, some built-in functions defined in
C provide no metadata about their arguments. 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) .. class:: Signature(parameters=None, *, return_annotation=Signature.empty)
@ -668,11 +680,12 @@ function.
>>> str(new_sig) >>> str(new_sig)
"(a, b) -> 'new return anno'" "(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 Return a :class:`Signature` (or its subclass) object for a given callable
``obj``. Pass ``follow_wrapped=False`` to get a signature of ``obj`` ``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`:: This method simplifies subclassing of :class:`Signature`::
@ -683,6 +696,9 @@ function.
.. versionadded:: 3.5 .. versionadded:: 3.5
.. versionadded:: 3.10
``globalns`` and ``localns`` parameters.
.. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty) .. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty)

View File

@ -238,6 +238,11 @@ inspect
When a module does not define ``__loader__``, fall back to ``__spec__.loader``. When a module does not define ``__loader__``, fall back to ``__spec__.loader``.
(Contributed by Brett Cannon in :issue:`42133`.) (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 linecache
--------- ---------

View File

@ -2137,9 +2137,9 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True):
return cls(parameters, return_annotation=cls.empty) return cls(parameters, return_annotation=cls.empty)
def _get_type_hints(func): def _get_type_hints(func, **kwargs):
try: try:
return typing.get_type_hints(func) return typing.get_type_hints(func, **kwargs)
except Exception: except Exception:
# First, try to use the get_type_hints to resolve # First, try to use the get_type_hints to resolve
# annotations. But for keeping the behavior intact # 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) 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.""" """Private helper: constructs Signature for the given python function."""
is_duck_function = False is_duck_function = False
@ -2190,7 +2191,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True):
positional = arg_names[:pos_count] positional = arg_names[:pos_count]
keyword_only_count = func_code.co_kwonlyargcount keyword_only_count = func_code.co_kwonlyargcount
keyword_only = arg_names[pos_count:pos_count + keyword_only_count] 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__ defaults = func.__defaults__
kwdefaults = func.__kwdefaults__ kwdefaults = func.__kwdefaults__
@ -2262,23 +2263,28 @@ def _signature_from_function(cls, func, skip_bound_arg=True):
def _signature_from_callable(obj, *, def _signature_from_callable(obj, *,
follow_wrapper_chains=True, follow_wrapper_chains=True,
skip_bound_arg=True, skip_bound_arg=True,
globalns=None,
localns=None,
sigcls): sigcls):
"""Private helper function to get signature for arbitrary """Private helper function to get signature for arbitrary
callable objects. 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): if not callable(obj):
raise TypeError('{!r} is not a callable object'.format(obj)) raise TypeError('{!r} is not a callable object'.format(obj))
if isinstance(obj, types.MethodType): if isinstance(obj, types.MethodType):
# In this case we skip the first parameter of the underlying # In this case we skip the first parameter of the underlying
# function (usually `self` or `cls`). # function (usually `self` or `cls`).
sig = _signature_from_callable( sig = _get_signature_of(obj.__func__)
obj.__func__,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
sigcls=sigcls)
if skip_bound_arg: if skip_bound_arg:
return _signature_bound_method(sig) 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 # If the unwrapped object is a *method*, we might want to
# skip its first parameter (self). # skip its first parameter (self).
# See test_signature_wrapped_bound_method for details. # See test_signature_wrapped_bound_method for details.
return _signature_from_callable( return _get_signature_of(obj)
obj,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
sigcls=sigcls)
try: try:
sig = obj.__signature__ sig = obj.__signature__
@ -2323,11 +2325,7 @@ def _signature_from_callable(obj, *,
# (usually `self`, or `cls`) will not be passed # (usually `self`, or `cls`) will not be passed
# automatically (as for boundmethods) # automatically (as for boundmethods)
wrapped_sig = _signature_from_callable( wrapped_sig = _get_signature_of(partialmethod.func)
partialmethod.func,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
sigcls=sigcls)
sig = _signature_get_partial(wrapped_sig, partialmethod, (None,)) sig = _signature_get_partial(wrapped_sig, partialmethod, (None,))
first_wrapped_param = tuple(wrapped_sig.parameters.values())[0] 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 # If it's a pure Python function, or an object that is duck type
# of a Python function (Cython functions, for instance), then: # of a Python function (Cython functions, for instance), then:
return _signature_from_function(sigcls, obj, 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): if _signature_is_builtin(obj):
return _signature_from_builtin(sigcls, obj, return _signature_from_builtin(sigcls, obj,
skip_bound_arg=skip_bound_arg) skip_bound_arg=skip_bound_arg)
if isinstance(obj, functools.partial): if isinstance(obj, functools.partial):
wrapped_sig = _signature_from_callable( wrapped_sig = _get_signature_of(obj.func)
obj.func,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
sigcls=sigcls)
return _signature_get_partial(wrapped_sig, obj) return _signature_get_partial(wrapped_sig, obj)
sig = None sig = None
@ -2368,29 +2363,17 @@ def _signature_from_callable(obj, *,
# in its metaclass # in its metaclass
call = _signature_get_user_defined_method(type(obj), '__call__') call = _signature_get_user_defined_method(type(obj), '__call__')
if call is not None: if call is not None:
sig = _signature_from_callable( sig = _get_signature_of(call)
call,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
sigcls=sigcls)
else: else:
# Now we check if the 'obj' class has a '__new__' method # Now we check if the 'obj' class has a '__new__' method
new = _signature_get_user_defined_method(obj, '__new__') new = _signature_get_user_defined_method(obj, '__new__')
if new is not None: if new is not None:
sig = _signature_from_callable( sig = _get_signature_of(new)
new,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
sigcls=sigcls)
else: else:
# Finally, we should have at least __init__ implemented # Finally, we should have at least __init__ implemented
init = _signature_get_user_defined_method(obj, '__init__') init = _signature_get_user_defined_method(obj, '__init__')
if init is not None: if init is not None:
sig = _signature_from_callable( sig = _get_signature_of(init)
init,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
sigcls=sigcls)
if sig is None: if sig is None:
# At this point we know, that `obj` is a class, with no user- # 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__') call = _signature_get_user_defined_method(type(obj), '__call__')
if call is not None: if call is not None:
try: try:
sig = _signature_from_callable( sig = _get_signature_of(call)
call,
follow_wrapper_chains=follow_wrapper_chains,
skip_bound_arg=skip_bound_arg,
sigcls=sigcls)
except ValueError as ex: except ValueError as ex:
msg = 'no signature found for {!r}'.format(obj) msg = 'no signature found for {!r}'.format(obj)
raise ValueError(msg) from ex raise ValueError(msg) from ex
@ -2892,10 +2871,12 @@ class Signature:
return _signature_from_builtin(cls, func) return _signature_from_builtin(cls, func)
@classmethod @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.""" """Constructs Signature for the given callable object."""
return _signature_from_callable(obj, sigcls=cls, return _signature_from_callable(obj, sigcls=cls,
follow_wrapper_chains=follow_wrapped) follow_wrapper_chains=follow_wrapped,
globalns=globalns, localns=localns)
@property @property
def parameters(self): def parameters(self):
@ -3143,9 +3124,10 @@ class Signature:
return rendered 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.""" """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(): def _main():

View File

@ -3250,6 +3250,26 @@ class TestSignatureObject(unittest.TestCase):
p2 = inspect.signature(lambda y, x: None).parameters p2 = inspect.signature(lambda y, x: None).parameters
self.assertNotEqual(p1, p2) 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): class TestParameterObject(unittest.TestCase):
def test_signature_parameter_kinds(self): def test_signature_parameter_kinds(self):

View File

@ -0,0 +1,2 @@
Add ``globalns`` and ``localns`` parameters to the :func:`inspect.signature`
and :meth:`inspect.Signature.from_callable`.