From 11fff4933d375bf5e5abdcc219658792450e429b Mon Sep 17 00:00:00 2001 From: Ofey Chan Date: Wed, 23 Feb 2022 20:11:14 +0800 Subject: [PATCH 1/4] Add test_partial_update --- Lib/test/test_functools.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index abbd50a47f395f..e0aba5b4500123 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3,6 +3,7 @@ import collections import collections.abc import copy +import inspect from itertools import permutations import pickle from random import choice @@ -708,6 +709,17 @@ def wrapper(): self.assertEqual(wrapper.__name__, 'max') self.assertTrue(wrapper.__doc__.startswith('max(')) self.assertEqual(wrapper.__annotations__, {}) + + def test_partial_update(self): + def wrapper(a, b, c, d): + pass + sig_original = inspect.signature(wrapper) + curried_a = functools.partial(wrapper, 1) + sig_curried_a_1 = inspect.signature(curried_a) + functools.update_wrapper(curried_a, wrapper) + sig_curried_a_2 = inspect.signature(curried_a) + self.assertEqual(sig_curried_a_2, sig_curried_a_1) + self.assertNotEqual(sig_curried_a_2, sig_original) class TestWraps(TestUpdateWrapper): From 4a79ea647433ced87b3980ab22f6c69c95871569 Mon Sep 17 00:00:00 2001 From: Ofey Chan Date: Wed, 23 Feb 2022 22:33:42 +0800 Subject: [PATCH 2/4] handle functools.partial before follow_wrapper_chains --- Lib/inspect.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 9c1283ab3734bf..80346bf5ba9a97 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2404,6 +2404,11 @@ def _signature_from_callable(obj, *, else: return sig + # See issue 44337, handle functools.partial before follow_wrapper_chains + 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: obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__"))) @@ -2464,10 +2469,6 @@ def _signature_from_callable(obj, *, return _signature_from_builtin(sigcls, obj, skip_bound_arg=skip_bound_arg) - if isinstance(obj, functools.partial): - 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 From 27f7014b5eca536e32694cde1871d97726620470 Mon Sep 17 00:00:00 2001 From: Ofey Chan Date: Thu, 24 Feb 2022 00:17:05 +0800 Subject: [PATCH 3/4] handle partial method, and move testcase to test_inspect --- Lib/inspect.py | 46 ++++++++++++++++++++------------------ Lib/test/test_functools.py | 12 ---------- Lib/test/test_inspect.py | 24 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 80346bf5ba9a97..26a74c881aa61d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2404,32 +2404,12 @@ def _signature_from_callable(obj, *, else: return sig - # See issue 44337, handle functools.partial before follow_wrapper_chains + # 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) - # Was this function wrapped by a decorator? - if follow_wrapper_chains: - obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__"))) - 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 - try: partialmethod = obj._partialmethod except AttributeError: @@ -2458,6 +2438,28 @@ def _signature_from_callable(obj, *, new_params = (first_wrapped_param,) + sig_params return sig.replace(parameters=new_params) + + # Was this function wrapped by a decorator? + if follow_wrapper_chains: + obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__"))) + 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 # of a Python function (Cython functions, for instance), then: diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index e0aba5b4500123..abbd50a47f395f 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3,7 +3,6 @@ import collections import collections.abc import copy -import inspect from itertools import permutations import pickle from random import choice @@ -709,17 +708,6 @@ def wrapper(): self.assertEqual(wrapper.__name__, 'max') self.assertTrue(wrapper.__doc__.startswith('max(')) self.assertEqual(wrapper.__annotations__, {}) - - def test_partial_update(self): - def wrapper(a, b, c, d): - pass - sig_original = inspect.signature(wrapper) - curried_a = functools.partial(wrapper, 1) - sig_curried_a_1 = inspect.signature(curried_a) - functools.update_wrapper(curried_a, wrapper) - sig_curried_a_2 = inspect.signature(curried_a) - self.assertEqual(sig_curried_a_2, sig_curried_a_1) - self.assertNotEqual(sig_curried_a_2, sig_original) class TestWraps(TestUpdateWrapper): diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 9e3c77056d70a0..51f57c34f29089 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -2921,6 +2921,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): import functools From 26108b02b2afe52b611c1db9e7577891837a0e71 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 05:38:21 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2022-03-07-05-38-20.bpo-46761.4mt4cJ.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2022-03-07-05-38-20.bpo-46761.4mt4cJ.rst 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.