diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 4a0a090facb8bb..cd3d4cc4dd683e 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -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. + 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) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 89694afdfa3fec..4bfec1dd179437 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -521,6 +521,15 @@ io built on debug mode `. (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. + :pypi:`inspect313` package has a backport of this feature. + (Contributed by Nikita Sobolev in :gh:`108901`.) + ipaddress --------- diff --git a/Lib/inspect.py b/Lib/inspect.py index 422c09a92ad141..c423843cfe05bd 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -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) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index b2265e44e0c79b..6efa88df02803b 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -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 diff --git a/Misc/NEWS.d/next/Library/2024-03-10-14-45-00.gh-issue-108901.vMlWej.rst b/Misc/NEWS.d/next/Library/2024-03-10-14-45-00.gh-issue-108901.vMlWej.rst new file mode 100644 index 00000000000000..0e19d2482b5876 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-03-10-14-45-00.gh-issue-108901.vMlWej.rst @@ -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.