diff -r 9cfb3d1cf0d1 Lib/inspect.py --- a/Lib/inspect.py Wed Feb 19 13:32:34 2014 +0100 +++ b/Lib/inspect.py Wed Feb 19 23:24:34 2014 +1000 @@ -960,6 +960,11 @@ # To simulate this behaviour, we "unbind" bound methods, to trick # inspect.signature to always return their first parameter ("self", # usually) + # + # As this is primarily for backwards compatibility, other descriptors + # (such as functools.partialmethod) will behave the same as they + # do when accessed through signature() (i.e. hiding their first + # parameter if it is already bound) func = func.__func__ elif isbuiltin(func): @@ -973,7 +978,9 @@ builtin_method_param = _signature_get_bound_param(text_signature) try: - sig = signature(func) + # getfullargspec() historically ignored __wrapped__ attributes, + # so we ensure that remains the case in 3.3+ + sig = _signature_internal(func, follow_wrapper_chains=False) except Exception as ex: # Most of the times 'signature' will raise ValueError. # But, it can also raise AttributeError, and, maybe something @@ -1851,8 +1858,7 @@ return cls(parameters, return_annotation=cls.empty) -def signature(obj): - '''Get a signature object for the passed callable.''' +def _signature_internal(obj, follow_wrapper_chains=True): if not callable(obj): raise TypeError('{!r} is not a callable object'.format(obj)) @@ -1860,11 +1866,12 @@ if isinstance(obj, types.MethodType): # In this case we skip the first parameter of the underlying # function (usually `self` or `cls`). - sig = signature(obj.__func__) + sig = _signature_internal(obj.__func__, follow_wrapper_chains) return _signature_bound_method(sig) # 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: sig = obj.__signature__ @@ -1887,7 +1894,8 @@ # (usually `self`, or `cls`) will not be passed # automatically (as for boundmethods) - wrapped_sig = signature(partialmethod.func) + wrapped_sig = _signature_internal(partialmethod.func, + follow_wrapper_chains) sig = _signature_get_partial(wrapped_sig, partialmethod, (None,)) first_wrapped_param = tuple(wrapped_sig.parameters.values())[0] @@ -1904,7 +1912,7 @@ return Signature.from_function(obj) if isinstance(obj, functools.partial): - wrapped_sig = signature(obj.func) + wrapped_sig = _signature_internal(obj.func, follow_wrapper_chains) return _signature_get_partial(wrapped_sig, obj) sig = None @@ -1915,17 +1923,17 @@ # in its metaclass call = _signature_get_user_defined_method(type(obj), '__call__') if call is not None: - sig = signature(call) + sig = _signature_internal(call, follow_wrapper_chains) else: # Now we check if the 'obj' class has a '__new__' method new = _signature_get_user_defined_method(obj, '__new__') if new is not None: - sig = signature(new) + sig = _signature_internal(new, follow_wrapper_chains) else: # Finally, we should have at least __init__ implemented init = _signature_get_user_defined_method(obj, '__init__') if init is not None: - sig = signature(init) + sig = _signature_internal(init, follow_wrapper_chains) if sig is None: # At this point we know, that `obj` is a class, with no user- @@ -1967,7 +1975,7 @@ call = _signature_get_user_defined_method(type(obj), '__call__') if call is not None: try: - sig = signature(call) + sig = _signature_internal(call, follow_wrapper_chains) except ValueError as ex: msg = 'no signature found for {!r}'.format(obj) raise ValueError(msg) from ex @@ -1984,6 +1992,10 @@ 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: '''A private marker - used in Parameter & Signature''' diff -r 9cfb3d1cf0d1 Lib/test/test_inspect.py --- a/Lib/test/test_inspect.py Wed Feb 19 13:32:34 2014 +0100 +++ b/Lib/test/test_inspect.py Wed Feb 19 23:24:34 2014 +1000 @@ -577,6 +577,48 @@ kwonlyargs_e=['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)') + # Other variants + def check_variant(f): + self.assertArgSpecEquals(f, ['x', 'y'], formatted='(x, y)') + def check_method(f): + self.assertArgSpecEquals(f, ['self', 'x', 'y'], + formatted='(self, x, y)') + check_variant(functools.partial(ham)) + 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_variant(C()) + check_method(C.ham) + check_method(C().ham) + check_method(C.pham) + check_variant(C().pham) + + class C_new: + @functools.wraps(mod.spam) + def __new__(cls, x, y): + pass + check_variant(C_new) + + class C_init: + @functools.wraps(mod.spam) + def __init__(self, x, y): + pass + check_variant(C_init) + + def test_getfullargspec_signature_attr(self): def test(): pass