Skip to content

Commit

Permalink
pythongh-101293: Fix support of custom callables and types in inspect…
Browse files Browse the repository at this point in the history
….Signature.from_callable() (pythonGH-115530)

Support callables with the __call__() method and types with
__new__() and __init__() methods set to class methods, static
methods, bound methods, partial functions, and other types of
methods and descriptors.

Add tests for numerous types of callables and descriptors.
  • Loading branch information
serhiy-storchaka authored and adorilson committed Mar 25, 2024
1 parent b8b144b commit 974f589
Show file tree
Hide file tree
Showing 3 changed files with 438 additions and 89 deletions.
161 changes: 74 additions & 87 deletions Lib/inspect.py
Expand Up @@ -2039,15 +2039,17 @@ def _signature_get_user_defined_method(cls, method_name):
named ``method_name`` and returns it only if it is a
pure python function.
"""
try:
meth = getattr(cls, method_name)
except AttributeError:
return
if method_name == '__new__':
meth = getattr(cls, method_name, None)
else:
if not isinstance(meth, _NonUserDefinedCallables):
# Once '__signature__' will be added to 'C'-level
# callables, this check won't be necessary
return meth
meth = getattr_static(cls, method_name, None)
if meth is None or isinstance(meth, _NonUserDefinedCallables):
# Once '__signature__' will be added to 'C'-level
# callables, this check won't be necessary
return None
if method_name != '__new__':
meth = _descriptor_get(meth, cls)
return meth


def _signature_get_partial(wrapped_sig, partial, extra_args=()):
Expand Down Expand Up @@ -2492,6 +2494,15 @@ def _signature_from_function(cls, func, skip_bound_arg=True,
__validate_parameters__=is_duck_function)


def _descriptor_get(descriptor, obj):
if isclass(descriptor):
return descriptor
get = getattr(type(descriptor), '__get__', _sentinel)
if get is _sentinel:
return descriptor
return get(descriptor, obj, type(obj))


def _signature_from_callable(obj, *,
follow_wrapper_chains=True,
skip_bound_arg=True,
Expand Down Expand Up @@ -2600,96 +2611,72 @@ def _signature_from_callable(obj, *,
wrapped_sig = _get_signature_of(obj.func)
return _signature_get_partial(wrapped_sig, obj)

sig = None
if isinstance(obj, type):
# obj is a class or a metaclass

# First, let's see if it has an overloaded __call__ defined
# in its metaclass
call = _signature_get_user_defined_method(type(obj), '__call__')
if call is not None:
sig = _get_signature_of(call)
else:
factory_method = None
new = _signature_get_user_defined_method(obj, '__new__')
init = _signature_get_user_defined_method(obj, '__init__')

# Go through the MRO and see if any class has user-defined
# pure Python __new__ or __init__ method
for base in obj.__mro__:
# Now we check if the 'obj' class has an own '__new__' method
if new is not None and '__new__' in base.__dict__:
factory_method = new
break
# or an own '__init__' method
elif init is not None and '__init__' in base.__dict__:
factory_method = init
break
return _get_signature_of(call)

if factory_method is not None:
sig = _get_signature_of(factory_method)

if sig is None:
# At this point we know, that `obj` is a class, with no user-
# defined '__init__', '__new__', or class-level '__call__'

for base in obj.__mro__[:-1]:
# Since '__text_signature__' is implemented as a
# descriptor that extracts text signature from the
# class docstring, if 'obj' is derived from a builtin
# class, its own '__text_signature__' may be 'None'.
# Therefore, we go through the MRO (except the last
# class in there, which is 'object') to find the first
# class with non-empty text signature.
try:
text_sig = base.__text_signature__
except AttributeError:
pass
else:
if text_sig:
# If 'base' class has a __text_signature__ attribute:
# return a signature based on it
return _signature_fromstr(sigcls, base, text_sig)

# No '__text_signature__' was found for the 'obj' class.
# Last option is to check if its '__init__' is
# object.__init__ or type.__init__.
if type not in obj.__mro__:
# We have a class (not metaclass), but no user-defined
# __init__ or __new__ for it
if (obj.__init__ is object.__init__ and
obj.__new__ is object.__new__):
# Return a signature of 'object' builtin.
return sigcls.from_callable(object)
else:
raise ValueError(
'no signature found for builtin type {!r}'.format(obj))
new = _signature_get_user_defined_method(obj, '__new__')
init = _signature_get_user_defined_method(obj, '__init__')

elif not isinstance(obj, _NonUserDefinedCallables):
# An object with __call__
# 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__')
if call is not None:
# Go through the MRO and see if any class has user-defined
# pure Python __new__ or __init__ method
for base in obj.__mro__:
# Now we check if the 'obj' class has an own '__new__' method
if new is not None and '__new__' in base.__dict__:
sig = _get_signature_of(new)
if skip_bound_arg:
sig = _signature_bound_method(sig)
return sig
# or an own '__init__' method
elif init is not None and '__init__' in base.__dict__:
return _get_signature_of(init)

# At this point we know, that `obj` is a class, with no user-
# defined '__init__', '__new__', or class-level '__call__'

for base in obj.__mro__[:-1]:
# Since '__text_signature__' is implemented as a
# descriptor that extracts text signature from the
# class docstring, if 'obj' is derived from a builtin
# class, its own '__text_signature__' may be 'None'.
# Therefore, we go through the MRO (except the last
# class in there, which is 'object') to find the first
# class with non-empty text signature.
try:
sig = _get_signature_of(call)
except ValueError as ex:
msg = 'no signature found for {!r}'.format(obj)
raise ValueError(msg) from ex

if sig is not None:
# For classes and objects we skip the first parameter of their
# __call__, __new__, or __init__ methods
if skip_bound_arg:
return _signature_bound_method(sig)
else:
return sig
text_sig = base.__text_signature__
except AttributeError:
pass
else:
if text_sig:
# If 'base' class has a __text_signature__ attribute:
# return a signature based on it
return _signature_fromstr(sigcls, base, text_sig)

# No '__text_signature__' was found for the 'obj' class.
# Last option is to check if its '__init__' is
# object.__init__ or type.__init__.
if type not in obj.__mro__:
# We have a class (not metaclass), but no user-defined
# __init__ or __new__ for it
if (obj.__init__ is object.__init__ and
obj.__new__ is object.__new__):
# Return a signature of 'object' builtin.
return sigcls.from_callable(object)
else:
raise ValueError(
'no signature found for builtin type {!r}'.format(obj))

if isinstance(obj, types.BuiltinFunctionType):
# Raise a nicer error message for builtins
msg = 'no signature found for builtin function {!r}'.format(obj)
raise ValueError(msg)
else:
# An object with __call__
call = getattr_static(type(obj), '__call__', None)
if call is not None:
call = _descriptor_get(call, obj)
return _get_signature_of(call)

raise ValueError('callable {!r} is not supported by signature'.format(obj))

Expand Down

0 comments on commit 974f589

Please sign in to comment.