Commit 57d240ef authored by Yury Selivanov's avatar Yury Selivanov

inspect: Fix getfullargspec() to not to follow __wrapped__ chains

Initial patch by Nick Coghlan.
parent 4ac30f17
...@@ -949,9 +949,9 @@ def getfullargspec(func): ...@@ -949,9 +949,9 @@ def getfullargspec(func):
The first four items in the tuple correspond to getargspec(). The first four items in the tuple correspond to getargspec().
""" """
builtin_method_param = None try:
# Re: `skip_bound_arg=False`
if ismethod(func): #
# There is a notable difference in behaviour between getfullargspec # There is a notable difference in behaviour between getfullargspec
# and Signature: the former always returns 'self' parameter for bound # and Signature: the former always returns 'self' parameter for bound
# methods, whereas the Signature always shows the actual calling # methods, whereas the Signature always shows the actual calling
...@@ -960,20 +960,15 @@ def getfullargspec(func): ...@@ -960,20 +960,15 @@ def getfullargspec(func):
# To simulate this behaviour, we "unbind" bound methods, to trick # To simulate this behaviour, we "unbind" bound methods, to trick
# inspect.signature to always return their first parameter ("self", # inspect.signature to always return their first parameter ("self",
# usually) # usually)
func = func.__func__
elif isbuiltin(func): # Re: `follow_wrapper_chains=False`
# We have a builtin function or method. For that, we check the #
# special '__text_signature__' attribute, provided by the # getfullargspec() historically ignored __wrapped__ attributes,
# Argument Clinic. If it's a method, we'll need to make sure # so we ensure that remains the case in 3.3+
# that its first parameter (usually "self") is always returned
# (see the previous comment).
text_signature = getattr(func, '__text_signature__', None)
if text_signature and text_signature.startswith('($'):
builtin_method_param = _signature_get_bound_param(text_signature)
try: sig = _signature_internal(func,
sig = signature(func) follow_wrapper_chains=False,
skip_bound_arg=False)
except Exception as ex: except Exception as ex:
# Most of the times 'signature' will raise ValueError. # Most of the times 'signature' will raise ValueError.
# But, it can also raise AttributeError, and, maybe something # But, it can also raise AttributeError, and, maybe something
...@@ -1023,13 +1018,6 @@ def getfullargspec(func): ...@@ -1023,13 +1018,6 @@ def getfullargspec(func):
# compatibility with 'func.__defaults__' # compatibility with 'func.__defaults__'
defaults = None defaults = None
if builtin_method_param and (not args or args[0] != builtin_method_param):
# `func` is a method, and we always need to return its
# first parameter -- usually "self" (to be backwards
# compatible with the previous implementation of
# getfullargspec)
args.insert(0, builtin_method_param)
return FullArgSpec(args, varargs, varkw, defaults, return FullArgSpec(args, varargs, varkw, defaults,
kwonlyargs, kwdefaults, annotations) kwonlyargs, kwdefaults, annotations)
...@@ -1719,7 +1707,7 @@ def _signature_strip_non_python_syntax(signature): ...@@ -1719,7 +1707,7 @@ def _signature_strip_non_python_syntax(signature):
return clean_signature, self_parameter, last_positional_only return clean_signature, self_parameter, last_positional_only
def _signature_fromstr(cls, obj, s): def _signature_fromstr(cls, obj, s, skip_bound_arg=True):
# Internal helper to parse content of '__text_signature__' # Internal helper to parse content of '__text_signature__'
# and return a Signature based on it # and return a Signature based on it
Parameter = cls._parameter_cls Parameter = cls._parameter_cls
...@@ -1840,7 +1828,7 @@ def _signature_fromstr(cls, obj, s): ...@@ -1840,7 +1828,7 @@ def _signature_fromstr(cls, obj, s):
if self_parameter is not None: if self_parameter is not None:
assert parameters assert parameters
if getattr(obj, '__self__', None): if getattr(obj, '__self__', None) and skip_bound_arg:
# strip off self, it's already been bound # strip off self, it's already been bound
parameters.pop(0) parameters.pop(0)
else: else:
...@@ -1851,8 +1839,21 @@ def _signature_fromstr(cls, obj, s): ...@@ -1851,8 +1839,21 @@ def _signature_fromstr(cls, obj, s):
return cls(parameters, return_annotation=cls.empty) return cls(parameters, return_annotation=cls.empty)
def signature(obj): def _signature_from_builtin(cls, func, skip_bound_arg=True):
'''Get a signature object for the passed callable.''' # Internal helper function to get signature for
# builtin callables
if not _signature_is_builtin(func):
raise TypeError("{!r} is not a Python builtin "
"function".format(func))
s = getattr(func, "__text_signature__", None)
if not s:
raise ValueError("no signature found for builtin {!r}".format(func))
return _signature_fromstr(cls, func, s, skip_bound_arg)
def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True):
if not callable(obj): if not callable(obj):
raise TypeError('{!r} is not a callable object'.format(obj)) raise TypeError('{!r} is not a callable object'.format(obj))
...@@ -1860,11 +1861,17 @@ def signature(obj): ...@@ -1860,11 +1861,17 @@ def signature(obj):
if isinstance(obj, types.MethodType): if isinstance(obj, types.MethodType):
# In this case we skip the first parameter of the underlying # In this case we skip the first parameter of the underlying
# function (usually `self` or `cls`). # function (usually `self` or `cls`).
sig = signature(obj.__func__) sig = _signature_internal(obj.__func__,
return _signature_bound_method(sig) follow_wrapper_chains,
skip_bound_arg)
if skip_bound_arg:
return _signature_bound_method(sig)
else:
return sig
# Was this function wrapped by a decorator? # Was this function wrapped by a decorator?
obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__"))) if follow_wrapper_chains:
obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__")))
try: try:
sig = obj.__signature__ sig = obj.__signature__
...@@ -1887,7 +1894,9 @@ def signature(obj): ...@@ -1887,7 +1894,9 @@ def signature(obj):
# (usually `self`, or `cls`) will not be passed # (usually `self`, or `cls`) will not be passed
# automatically (as for boundmethods) # automatically (as for boundmethods)
wrapped_sig = signature(partialmethod.func) wrapped_sig = _signature_internal(partialmethod.func,
follow_wrapper_chains,
skip_bound_arg)
sig = _signature_get_partial(wrapped_sig, partialmethod, (None,)) sig = _signature_get_partial(wrapped_sig, partialmethod, (None,))
first_wrapped_param = tuple(wrapped_sig.parameters.values())[0] first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]
...@@ -1896,7 +1905,8 @@ def signature(obj): ...@@ -1896,7 +1905,8 @@ def signature(obj):
return sig.replace(parameters=new_params) return sig.replace(parameters=new_params)
if _signature_is_builtin(obj): if _signature_is_builtin(obj):
return Signature.from_builtin(obj) return _signature_from_builtin(Signature, obj,
skip_bound_arg=skip_bound_arg)
if isfunction(obj) or _signature_is_functionlike(obj): if isfunction(obj) or _signature_is_functionlike(obj):
# If it's a pure Python function, or an object that is duck type # If it's a pure Python function, or an object that is duck type
...@@ -1904,7 +1914,9 @@ def signature(obj): ...@@ -1904,7 +1914,9 @@ def signature(obj):
return Signature.from_function(obj) return Signature.from_function(obj)
if isinstance(obj, functools.partial): if isinstance(obj, functools.partial):
wrapped_sig = signature(obj.func) wrapped_sig = _signature_internal(obj.func,
follow_wrapper_chains,
skip_bound_arg)
return _signature_get_partial(wrapped_sig, obj) return _signature_get_partial(wrapped_sig, obj)
sig = None sig = None
...@@ -1915,17 +1927,23 @@ def signature(obj): ...@@ -1915,17 +1927,23 @@ def signature(obj):
# in its metaclass # in its metaclass
call = _signature_get_user_defined_method(type(obj), '__call__') call = _signature_get_user_defined_method(type(obj), '__call__')
if call is not None: if call is not None:
sig = signature(call) sig = _signature_internal(call,
follow_wrapper_chains,
skip_bound_arg)
else: else:
# Now we check if the 'obj' class has a '__new__' method # Now we check if the 'obj' class has a '__new__' method
new = _signature_get_user_defined_method(obj, '__new__') new = _signature_get_user_defined_method(obj, '__new__')
if new is not None: if new is not None:
sig = signature(new) sig = _signature_internal(new,
follow_wrapper_chains,
skip_bound_arg)
else: else:
# Finally, we should have at least __init__ implemented # Finally, we should have at least __init__ implemented
init = _signature_get_user_defined_method(obj, '__init__') init = _signature_get_user_defined_method(obj, '__init__')
if init is not None: if init is not None:
sig = signature(init) sig = _signature_internal(init,
follow_wrapper_chains,
skip_bound_arg)
if sig is None: if sig is None:
# At this point we know, that `obj` is a class, with no user- # At this point we know, that `obj` is a class, with no user-
...@@ -1967,7 +1985,9 @@ def signature(obj): ...@@ -1967,7 +1985,9 @@ def signature(obj):
call = _signature_get_user_defined_method(type(obj), '__call__') call = _signature_get_user_defined_method(type(obj), '__call__')
if call is not None: if call is not None:
try: try:
sig = signature(call) sig = _signature_internal(call,
follow_wrapper_chains,
skip_bound_arg)
except ValueError as ex: except ValueError as ex:
msg = 'no signature found for {!r}'.format(obj) msg = 'no signature found for {!r}'.format(obj)
raise ValueError(msg) from ex raise ValueError(msg) from ex
...@@ -1975,7 +1995,10 @@ def signature(obj): ...@@ -1975,7 +1995,10 @@ def signature(obj):
if sig is not None: if sig is not None:
# For classes and objects we skip the first parameter of their # For classes and objects we skip the first parameter of their
# __call__, __new__, or __init__ methods # __call__, __new__, or __init__ methods
return _signature_bound_method(sig) if skip_bound_arg:
return _signature_bound_method(sig)
else:
return sig
if isinstance(obj, types.BuiltinFunctionType): if isinstance(obj, types.BuiltinFunctionType):
# Raise a nicer error message for builtins # Raise a nicer error message for builtins
...@@ -1984,6 +2007,10 @@ def signature(obj): ...@@ -1984,6 +2007,10 @@ def signature(obj):
raise ValueError('callable {!r} is not supported by signature'.format(obj)) raise ValueError('callable {!r} is not supported by signature'.format(obj))
def signature(obj):
'''Get a signature object for the passed callable.'''
return _signature_internal(obj)
class _void: class _void:
'''A private marker - used in Parameter & Signature''' '''A private marker - used in Parameter & Signature'''
...@@ -2417,15 +2444,7 @@ class Signature: ...@@ -2417,15 +2444,7 @@ class Signature:
@classmethod @classmethod
def from_builtin(cls, func): def from_builtin(cls, func):
if not _signature_is_builtin(func): return _signature_from_builtin(cls, func)
raise TypeError("{!r} is not a Python builtin "
"function".format(func))
s = getattr(func, "__text_signature__", None)
if not s:
raise ValueError("no signature found for builtin {!r}".format(func))
return _signature_fromstr(cls, func, s)
@property @property
def parameters(self): def parameters(self):
......
...@@ -577,6 +577,46 @@ class TestClassesAndFunctions(unittest.TestCase): ...@@ -577,6 +577,46 @@ class TestClassesAndFunctions(unittest.TestCase):
kwonlyargs_e=['arg'], kwonlyargs_e=['arg'],
formatted='(*, arg)') formatted='(*, arg)')
def test_argspec_api_ignores_wrapped(self):
# Issue 20684: low level introspection API must ignore __wrapped__
@functools.wraps(mod.spam)
def ham(x, y):
pass
# Basic check
self.assertArgSpecEquals(ham, ['x', 'y'], formatted='(x, y)')
self.assertFullArgSpecEquals(ham, ['x', 'y'], formatted='(x, y)')
self.assertFullArgSpecEquals(functools.partial(ham),
['x', 'y'], formatted='(x, y)')
# Other variants
def check_method(f):
self.assertArgSpecEquals(f, ['self', 'x', 'y'],
formatted='(self, x, y)')
class C:
@functools.wraps(mod.spam)
def ham(self, x, y):
pass
pham = functools.partialmethod(ham)
@functools.wraps(mod.spam)
def __call__(self, x, y):
pass
check_method(C())
check_method(C.ham)
check_method(C().ham)
check_method(C.pham)
check_method(C().pham)
class C_new:
@functools.wraps(mod.spam)
def __new__(self, x, y):
pass
check_method(C_new)
class C_init:
@functools.wraps(mod.spam)
def __init__(self, x, y):
pass
check_method(C_init)
def test_getfullargspec_signature_attr(self): def test_getfullargspec_signature_attr(self):
def test(): def test():
pass pass
......
...@@ -66,6 +66,10 @@ Library ...@@ -66,6 +66,10 @@ Library
loop.set_exception_handler(), loop.default_exception_handler(), and loop.set_exception_handler(), loop.default_exception_handler(), and
loop.call_exception_handler(). loop.call_exception_handler().
- Issue #20684: Fix inspect.getfullargspec() to not to follow __wrapped__
chains. Make its behaviour consistent with bound methods first argument.
Patch by Nick Coghlan and Yury Selivanov.
Tests Tests
----- -----
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment