Skip to content
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

gh-108901: Add bound_arg to Signature.from_callable() and signature() #116559

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
19 changes: 14 additions & 5 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
@@ -651,7 +651,7 @@ and its return annotation. To retrieve a :class:`!Signature` object,
use the :func:`!signature`
function.

.. function:: signature(callable, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
.. function:: signature(callable, *, follow_wrapped=True, bound_arg=False, globals=None, locals=None, eval_str=False)

Return a :class:`Signature` object for the given *callable*:

@@ -675,6 +675,12 @@ function.
Accepts a wide range of Python callables, from plain functions and classes to
:func:`functools.partial` objects.

If *follow_wrapped* is ``False`` *callable* will not be unwrapped
(``callable.__wrapped__`` will not be used to unwrap decorated callables).

If *bound_arg* is ``False``, remove ``self`` parameter
from the method signature.
Comment on lines +681 to +682
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to describe the non-default behaviour for boolean options (so folks know what they'll get if they request non-default behaviour):

Suggested change
If *bound_arg* is ``False``, remove ``self`` parameter
from the method signature.
If *bound_arg* is ``True``, report the (already bound) first parameter on bound
instance or class methods (usually ``self`` or ``cls``). This emulates the historical
behaviour of the deprecated :func:`getfullargspec` function.

(Describing both behaviours is also an option, but it seemed excessively verbose in this case)


For objects defined in modules using stringized annotations
(``from __future__ import annotations``), :func:`signature` will
attempt to automatically un-stringize the annotations using
@@ -696,13 +702,13 @@ function.

.. versionchanged:: 3.5
The *follow_wrapped* parameter was added.
Pass ``False`` to get a signature of
*callable* specifically (``callable.__wrapped__`` will not be used to
unwrap decorated callables.)

.. versionchanged:: 3.10
The *globals*, *locals*, and *eval_str* parameters were added.

.. versionchanged:: 3.13
The *bound_arg* parameter was added.

.. note::

Some callables may not be introspectable in certain implementations of
@@ -806,7 +812,7 @@ function.

.. versionadded:: 3.13

.. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False)
.. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, bound_arg=False, globals=None, locals=None, eval_str=False)

Return a :class:`Signature` (or its subclass) object for a given callable
*obj*.
@@ -827,6 +833,9 @@ function.
.. versionchanged:: 3.10
The *globals*, *locals*, and *eval_str* parameters were added.

.. versionchanged:: 3.13
The *bound_arg* parameter was added.


.. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty)

9 changes: 9 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
@@ -521,6 +521,15 @@ io
built on debug mode <debug-build>`.
(Contributed by Victor Stinner in :gh:`62948`.)

inspect
-------

* Add *bound_arg* parameter to :func:`inspect.Signature.from_callable`
and :func:`inspect.signature`: keep the ``self`` parameter
in the method signature if *bound_arg* is True.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reword here to match the updated main function docs (including mentioning the getfullargspec compatibility)

:pypi:`inspect313` package has a backport of this feature.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make the backport mention a seealso note in the module docs rather than only putting it here?

(Contributed by Nikita Sobolev in :gh:`108901`.)

ipaddress
---------

8 changes: 6 additions & 2 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
@@ -3085,10 +3085,12 @@ def __init__(self, parameters=None, *, return_annotation=_empty,

@classmethod
def from_callable(cls, obj, *,
follow_wrapped=True, globals=None, locals=None, eval_str=False):
follow_wrapped=True, bound_arg=False,
globals=None, locals=None, eval_str=False):
"""Constructs Signature for the given callable object."""
return _signature_from_callable(obj, sigcls=cls,
follow_wrapper_chains=follow_wrapped,
skip_bound_arg=not bound_arg,
globals=globals, locals=locals, eval_str=eval_str)

@property
@@ -3355,9 +3357,11 @@ def format(self, *, max_width=None):
return rendered


def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
def signature(obj, *, follow_wrapped=True, bound_arg=False,
globals=None, locals=None, eval_str=False):
"""Get a signature object for the passed callable."""
return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
bound_arg=bound_arg,
globals=globals, locals=locals, eval_str=eval_str)


89 changes: 89 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
@@ -4645,6 +4645,95 @@ class D2(D1):
self.assertEqual(inspect.signature(D2), inspect.signature(D1))


def check_bound_arg(self, *, bound_arg, follow_wrapped):
def decorator(func):
@functools.wraps(func) # set `__wrapper__` attribute
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

def func(self, arg: int) -> None: ...
@decorator
def decorated(self, arg: int) -> None: ...

class My:
def method(self, arg: int) -> None: ...
@decorator
def decorated(self, arg: int) -> None: ...
@classmethod
def cl(cls, arg2: str) -> None: ...
@staticmethod
def st(arg1: str) -> bool: ...

if bound_arg:
signatures = [
(My().method, '(self, arg: int) -> None'),
(My().decorated, (
'(self, arg: int) -> None'
if follow_wrapped else
'(*args, **kwargs) -> None'
)),
(My.cl, '(cls, arg2: str) -> None'),
(My().cl, '(cls, arg2: str) -> None'),
]
else:
signatures = [
(My().method, '(arg: int) -> None'),
(My().decorated, (
'(arg: int) -> None'
if follow_wrapped else
'(*args, **kwargs) -> None'
)),
(My.cl, '(arg2: str) -> None'),
(My().cl, '(arg2: str) -> None'),
]

common_signatures = [
(func, '(self, arg: int) -> None'),
(decorated, (
'(self, arg: int) -> None'
if follow_wrapped else
'(*args, **kwargs) -> None'
)),
(My.method, '(self, arg: int) -> None'),
(My.decorated, (
'(self, arg: int) -> None'
if follow_wrapped else
'(*args, **kwargs) -> None'
)),
(My.st, '(arg1: str) -> bool'),
(My().st, '(arg1: str) -> bool'),
]
signatures.extend(common_signatures)

for signature_func in (
inspect.signature,
inspect.Signature.from_callable,
):
for callable, text_sig in signatures:
with self.subTest(
callable=callable,
text_sig=text_sig,
signature_func=signature_func,
follow_wrapped=follow_wrapped,
bound_arg=bound_arg,
):
sig = signature_func(
callable,
follow_wrapped=follow_wrapped,
bound_arg=bound_arg,
)
self.assertEqual(str(sig), text_sig)

def test_signature_bound_arg_method(self):
for bound_arg in (True, False):
for follow_wrapped in (True, False):
self.check_bound_arg(
bound_arg=bound_arg,
follow_wrapped=follow_wrapped,
)


class TestParameterObject(unittest.TestCase):
def test_signature_parameter_kinds(self):
P = inspect.Parameter
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add *bound_arg* keyword-only parameter to
:func:`inspect.Signature.from_callable` and :func:`inspect.signature`.
If *bound_arg* is ``True``, keep ``self`` parameter in method a signature.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention the getfullargspec compatibility aspect here.