8000 bpo-41960: Add globalns and localns parameters to inspect.signature and Signature.from_callable by isidentical · Pull Request #22583 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``::

Expand All @@ -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>`.
Expand All @@ -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)

Expand Down Expand Up @@ -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`::

Expand All @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------

Expand Down
80 changes: 31 additions & 49 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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__
Expand Down Expand Up @@ -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)
Expand All @@ -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__
Expand All @@ -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]
Expand All @@ -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
Expand All @@ -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-
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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():
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``globalns`` and ``localns`` parameters to the :func:`inspect.signature`
and :meth:`inspect.Signature.from_callable`.
0