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

Conversation

sobolevn
Copy link
Member

@sobolevn sobolevn commented Mar 10, 2024

Doc/library/inspect.rst Outdated Show resolved Hide resolved
Doc/whatsnew/3.13.rst Outdated Show resolved Hide resolved
Lib/test/test_inspect/test_inspect.py Outdated Show resolved Hide resolved
Lib/test/test_inspect/test_inspect.py Outdated Show resolved Hide resolved
Lib/test/test_inspect/test_inspect.py Outdated Show resolved Hide resolved
Lib/test/test_inspect/test_inspect.py Outdated Show resolved Hide resolved
@vstinner
Copy link
Member

By the way, skip_bound_arg parameter name is a little bit long. Maybe you can rename it to bound_arg.

@pitrou: Do you have an opinion on the parameter name? :-)

@sobolevn
Copy link
Member Author

sobolevn commented Mar 11, 2024

I would prefer to have skip_bound_arg as a name, because it has a history (from existing code in inspect.py):

cpython/Lib/inspect.py

Lines 1404 to 1413 in 4e5df20

# Re: `skip_bound_arg=False`
#
# There is a notable difference in behaviour between getfullargspec
# and Signature: the former always returns 'self' parameter for bound
# methods, whereas the Signature always shows the actual calling
# signature of the passed object.
#
# To simulate this behaviour, we "unbind" bound methods, to trick
# inspect.signature to always return their first parameter ("self",
# usually)

@vstinner
Copy link
Member

skip_bound_arg=False is a double negation, and I'm always confused by double negation :-( bound_arg=True is more straightfoward to me.

Previously, the argument was private, no?

@sobolevn
Copy link
Member Author

Yes, it was private. It only existed on protected _signature_from_callable helper.

Doc/library/inspect.rst Outdated Show resolved Hide resolved
@sobolevn sobolevn changed the title gh-108901: Add skip_bound_arg to Signature.from_callable() and signature() gh-108901: Add bound_arg to Signature.from_callable() and signature() Apr 24, 2024
@sobolevn
Copy link
Member Author

Done, now it is named bound_arg! 👍

@encukou
Copy link
Member

encukou commented Apr 25, 2024

What's the motivation for this change? I can't find the discussion behind it.

My worry is that it only works on some callables, and it's not very clear which ones those are -- or how to make a custom callable (e.g. one implemented in the C API) work like a method.

@sobolevn
Copy link
Member Author

What's the motivation for this change?

I want to deprecate getfullargspec, which works almost like signature(), but with this difference. See

cpython/Lib/inspect.py

Lines 1403 to 1423 in 2c45148

# Re: `skip_bound_arg=False`
#
# There is a notable difference in behaviour between getfullargspec
# and Signature: the former always returns 'self' parameter for bound
# methods, whereas the Signature always shows the actual calling
# signature of the passed object.
#
# To simulate this behaviour, we "unbind" bound methods, to trick
# inspect.signature to always return their first parameter ("self",
# usually)
# Re: `follow_wrapper_chains=False`
#
# getfullargspec() historically ignored __wrapped__ attributes,
# so we ensure that remains the case in 3.3
sig = _signature_from_callable(func,
follow_wrapper_chains=False,
skip_bound_arg=False,
sigcls=Signature,
eval_str=False)

My worry is that it only works on some callables, and it's not very clear which ones those are -- or how to make a custom callable (e.g. one implemented in the C API) work like a method.

It will work exactly like this older function.

@encukou
Copy link
Member

encukou commented Apr 25, 2024

Is this behaviour useful, or is it a bug that's kept in getfullargspec for backwards compatibility?

Comment on lines 681 to 682
If *bound_arg* is ``False``, remove ``self`` parameter
from the method signature.
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)


* 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)

* 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.
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?

@@ -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.

@vstinner
Copy link
Member

Is this behaviour useful, or is it a bug that's kept in getfullargspec for backwards compatibility?

IMO it's useful in general, not only for backward compatibility. Keep self or not should be configurable.

@ncoghlan
Copy link
Contributor

ncoghlan commented Apr 29, 2024

Is this behaviour useful, or is it a bug that's kept in getfullargspec for backwards compatibility?

I think it was primarily a quirk of the way getfullargspec was originally implemented (i.e. it followed bound methods down to the underlying function to get the signature details, and then just returned those details as is without accounting for the first parameter already being bound).

However, the suggested flag isn't hard to maintain, since the complexity is in the branch that corrects the underlying function signature to remove the already bound first parameter (the default behaviour). The "unwrap bound methods" branch is literally just return sig:

if skip_bound_arg:

I do think making the flag public is worth including as a step prior to full programmatic deprecation of getfullargspec. With the suggested docs change, I also think the bound_arg parameter name is a reasonable one.

(The one potential argument I see for a parameter name like unbind_methods over bound_arg, is that the flag specifically affects the handling of the bound arg in bound methods. It doesn't have any effect on the handling of the previously bound arguments in functools.partial and functools.partialmethod instances. However, I don't think that concern is significant enough to be worth changing the parameter name again)

@hugovk hugovk removed their request for review April 30, 2024 07:46
@encukou
Copy link
Member

encukou commented Apr 30, 2024

the suggested flag isn't hard to maintain

My worry is that while the flag isn't hard to maintain as an internal detail of how getfullargspec extracts information from Python functions/methods, signature should work for other callables, like instances of C extension types (e.g. ones with Py_TPFLAGS_METHOD_DESCRIPTOR), or Cython functions. We might not be able to support these things transparently, but there should be a clear way to make custom objects that work with the flag. Or a documented way, at least.

(Unfortunately I'll probably not have time to investigate this worry before beta 1. If the feature is waiting for my review, it won't make it.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants