Skip to content

gh-101293: Fix inspect.signature behavior on __call__ decorated with staticmethod and classmethod decorators. #102564

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

Closed
wants to merge 2 commits into from
Closed
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
35 changes: 33 additions & 2 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2052,6 +2052,25 @@ def _signature_get_user_defined_method(cls, method_name):
# callables, this check won't be necessary
return meth

def _signature_get_user_defined_method_with_descriptor(cls, method_name):
"""Private helper. Checks if ``cls`` has an attribute
named ``method_name`` and returns it with descriptor only if it is a
pure python function.
"""
try:
meth = getattr(cls, method_name)
except AttributeError:
return None, None

if isinstance(meth, _NonUserDefinedCallables):
# Once '__signature__' will be added to 'C'-level
# callables, this check won't be necessary
return None, None

descriptior = getattr_static(cls, method_name, default=None)

return meth, descriptior


def _signature_get_partial(wrapped_sig, partial, extra_args=()):
"""Private helper to calculate how 'wrapped_sig' signature will
Expand Down Expand Up @@ -2673,13 +2692,25 @@ def _signature_from_callable(obj, *,
# We also check that the 'obj' is not an instance of
# types.WrapperDescriptorType or types.MethodWrapperType to avoid
# infinite recursion (and even potential segfault)
call = _signature_get_user_defined_method(type(obj), '__call__')
call, descriptior = _signature_get_user_defined_method_with_descriptor(type(obj), '__call__')

if call is not None:
if descriptior is not None:
is_staticmethod = isinstance(descriptior, staticmethod)
is_classmethod = isinstance(descriptior, classmethod)
else:
is_staticmethod = False
is_classmethod = False


try:
sig = _get_signature_of(call)
sig = _get_signature_of(call, skip_bound_arg=not is_staticmethod)
except ValueError as ex:
msg = 'no signature found for {!r}'.format(obj)
raise ValueError(msg) from ex
else:
if is_staticmethod or is_classmethod:
return sig

if sig is not None:
# For classes and objects we skip the first parameter of their
Expand Down
38 changes: 38 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -3660,6 +3660,44 @@ class Wrapped:
with self.assertRaisesRegex(ValueError, 'wrapper loop'):
self.signature(Wrapped)

def test_signature_on_class_callable_objects(self):
class Foo:
@classmethod
def __call__(cls, a):
pass

self.assertEqual(self.signature(Foo()),
((('a', ..., ..., "positional_or_keyword"),),
...))

class Bar:
@classmethod
def __call__(cls):
pass

self.assertEqual(self.signature(Bar()),
((()),
...))

def test_signature_on_static_callable_objects(self):
class Foo:
@staticmethod
def __call__(a):
pass

self.assertEqual(self.signature(Foo()),
((('a', ..., ..., "positional_or_keyword"),),
...))

class Bar:
@staticmethod
def __call__():
pass

self.assertEqual(self.signature(Bar()),
((()),
...))

def test_signature_on_lambdas(self):
self.assertEqual(self.signature((lambda a=10: a)),
((('a', 10, ..., "positional_or_keyword"),),
Expand Down