diff --git a/Lib/inspect.py b/Lib/inspect.py index d06c8078ded870..af2943454832f2 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2486,29 +2486,11 @@ def _signature_from_callable(obj, *, else: return sig - # Was this function wrapped by a decorator? - if follow_wrapper_chains: - # Unwrap until we find an explicit signature or a MethodType (which will be - # handled explicitly below). - obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__") - or isinstance(f, types.MethodType))) - if isinstance(obj, types.MethodType): - # If the unwrapped object is a *method*, we might want to - # skip its first parameter (self). - # See test_signature_wrapped_bound_method for details. - return _get_signature_of(obj) - - try: - sig = obj.__signature__ - except AttributeError: - pass - else: - if sig is not None: - if not isinstance(sig, Signature): - raise TypeError( - 'unexpected object {!r} in __signature__ ' - 'attribute'.format(sig)) - return sig + # See issue 44337, handle functools.partial and functools.partialmethod + # before follow_wrapper_chains. + if isinstance(obj, functools.partial): + wrapped_sig = _get_signature_of(obj.func) + return _signature_get_partial(wrapped_sig, obj) try: partialmethod = obj.__partialmethod__ @@ -2543,9 +2525,29 @@ def _signature_from_callable(obj, *, new_params = (first_wrapped_param,) + sig_params return sig.replace(parameters=new_params) - if isinstance(obj, functools.partial): - wrapped_sig = _get_signature_of(obj.func) - return _signature_get_partial(wrapped_sig, obj) + # Was this function wrapped by a decorator? + if follow_wrapper_chains: + # Unwrap until we find an explicit signature or a MethodType (which will be + # handled explicitly below). + obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__") + or isinstance(f, types.MethodType))) + if isinstance(obj, types.MethodType): + # If the unwrapped object is a *method*, we might want to + # skip its first parameter (self). + # See test_signature_wrapped_bound_method for details. + return _get_signature_of(obj) + + try: + sig = obj.__signature__ + except AttributeError: + pass + else: + if sig is not None: + if not isinstance(sig, Signature): + raise TypeError( + 'unexpected object {!r} in __signature__ ' + 'attribute'.format(sig)) + return sig if isfunction(obj) or _signature_is_functionlike(obj): # If it's a pure Python function, or an object that is duck type diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index c5581041f044b1..e52a41ff37d018 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4087,6 +4087,30 @@ def foo(a): pass foo.__partialmethod__ = 'spam' self.assertEqual(str(inspect.signature(foo)), '(a)') + def test_signature_on_partial_unchange_in_update_wrapper(self): + from functools import partial, update_wrapper + + def test(a, b, c, d): + pass + p = partial(test, a=1) + sig_partial = inspect.signature(p) + update_wrapper(p, test) + sig_updated = inspect.signature(p) + self.assertEqual(sig_updated, sig_partial) + + def test_signature_on_partial_method_unchange_in_update_wrapper(self): + from functools import partialmethod, update_wrapper + + class Spam: + def test(it, a, b, c, d) -> 'spam': + pass + ham = partialmethod(test, a=1) + + sig_partial = inspect.signature(Spam.ham) + update_wrapper(Spam.ham, Spam.test) + sig_updated = inspect.signature(Spam.ham) + self.assertEqual(sig_updated, sig_partial) + def test_signature_on_decorated(self): def decorator(func): @functools.wraps(func) diff --git a/Misc/NEWS.d/next/Library/2022-03-07-05-38-20.bpo-46761.4mt4cJ.rst b/Misc/NEWS.d/next/Library/2022-03-07-05-38-20.bpo-46761.4mt4cJ.rst new file mode 100644 index 00000000000000..dc138949339e52 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-03-07-05-38-20.bpo-46761.4mt4cJ.rst @@ -0,0 +1 @@ +Make :func:`inspect.signature()` returns correct signature after :func:`functools.update_wrapper()` is called. Patch by Ofey Chan.