From 6f4602f1cce4ff9f4578faa64dd381e38e9fc829 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 26 Feb 2024 20:07:41 +0200 Subject: [PATCH 1/2] gh-112006: Fix inspect.unwrap() for types where __wrapped__ is a data descriptor (GH-115540) This also fixes inspect.Signature.from_callable() for builtins classmethod() and staticmethod(). (cherry picked from commit 68c79d21fa791d7418a858b7aa4604880e988a02) Co-authored-by: Serhiy Storchaka --- Lib/inspect.py | 10 ++---- Lib/test/test_inspect/test_inspect.py | 32 +++++++++++++++---- ...-02-15-23-42-54.gh-issue-112006.4wxcK-.rst | 3 ++ 3 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 655b04b0eed4d5..8c5f921936aa38 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -748,18 +748,14 @@ def unwrap(func, *, stop=None): :exc:`ValueError` is raised if a cycle is encountered. """ - if stop is None: - def _is_wrapper(f): - return hasattr(f, '__wrapped__') - else: - def _is_wrapper(f): - return hasattr(f, '__wrapped__') and not stop(f) f = func # remember the original func for error reporting # Memoise by id to tolerate non-hashable objects, but store objects to # ensure they aren't destroyed, which would allow their IDs to be reused. memo = {id(f): f} recursion_limit = sys.getrecursionlimit() - while _is_wrapper(func): + while not isinstance(func, type) and hasattr(func, '__wrapped__'): + if stop is not None and stop(func): + break func = func.__wrapped__ id_func = id(func) if (id_func in memo) or (len(memo) >= recursion_limit): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 84cdd25cb7879b..169a1996e75a64 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -2732,6 +2732,10 @@ def m1d(*args, **kwargs): int)) def test_signature_on_classmethod(self): + self.assertEqual(self.signature(classmethod), + ((('function', ..., ..., "positional_only"),), + ...)) + class Test: @classmethod def foo(cls, arg1, *, arg2=1): @@ -2750,6 +2754,10 @@ def foo(cls, arg1, *, arg2=1): ...)) def test_signature_on_staticmethod(self): + self.assertEqual(self.signature(staticmethod), + ((('function', ..., ..., "positional_only"),), + ...)) + class Test: @staticmethod def foo(cls, *, arg): @@ -3273,16 +3281,20 @@ class Bar(Spam, Foo): ((('a', ..., ..., "positional_or_keyword"),), ...)) - class Wrapped: - pass - Wrapped.__wrapped__ = lambda a: None - self.assertEqual(self.signature(Wrapped), + def test_signature_on_wrapper(self): + class Wrapper: + def __call__(self, b): + pass + wrapper = Wrapper() + wrapper.__wrapped__ = lambda a: None + self.assertEqual(self.signature(wrapper), ((('a', ..., ..., "positional_or_keyword"),), ...)) # wrapper loop: - Wrapped.__wrapped__ = Wrapped + wrapper = Wrapper() + wrapper.__wrapped__ = wrapper with self.assertRaisesRegex(ValueError, 'wrapper loop'): - self.signature(Wrapped) + self.signature(wrapper) def test_signature_on_lambdas(self): self.assertEqual(self.signature((lambda a=10: a)), @@ -4433,6 +4445,14 @@ def test_recursion_limit(self): with self.assertRaisesRegex(ValueError, 'wrapper loop'): inspect.unwrap(obj) + def test_wrapped_descriptor(self): + self.assertIs(inspect.unwrap(NTimesUnwrappable), NTimesUnwrappable) + self.assertIs(inspect.unwrap(staticmethod), staticmethod) + self.assertIs(inspect.unwrap(classmethod), classmethod) + self.assertIs(inspect.unwrap(staticmethod(classmethod)), classmethod) + self.assertIs(inspect.unwrap(classmethod(staticmethod)), staticmethod) + + class TestMain(unittest.TestCase): def test_only_source(self): module = importlib.import_module('unittest') diff --git a/Misc/NEWS.d/next/Library/2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst b/Misc/NEWS.d/next/Library/2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst new file mode 100644 index 00000000000000..32af2bd24e54f2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst @@ -0,0 +1,3 @@ +Fix :func:`inspect.unwrap` for types with the ``__wrapper__`` data +descriptor. Fix :meth:`inspect.Signature.from_callable` for builtins +:func:`classmethod` and :func:`staticmethod`. From abc76714a46977ab22405e6319da5c9a83839a62 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 27 Feb 2024 20:18:31 +0200 Subject: [PATCH 2/2] Fix tests. --- Lib/test/test_inspect/test_inspect.py | 8 -------- .../2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 169a1996e75a64..914bb7d43a59b4 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -2732,10 +2732,6 @@ def m1d(*args, **kwargs): int)) def test_signature_on_classmethod(self): - self.assertEqual(self.signature(classmethod), - ((('function', ..., ..., "positional_only"),), - ...)) - class Test: @classmethod def foo(cls, arg1, *, arg2=1): @@ -2754,10 +2750,6 @@ def foo(cls, arg1, *, arg2=1): ...)) def test_signature_on_staticmethod(self): - self.assertEqual(self.signature(staticmethod), - ((('function', ..., ..., "positional_only"),), - ...)) - class Test: @staticmethod def foo(cls, *, arg): diff --git a/Misc/NEWS.d/next/Library/2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst b/Misc/NEWS.d/next/Library/2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst index 32af2bd24e54f2..7e9fe97a72b5eb 100644 --- a/Misc/NEWS.d/next/Library/2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst +++ b/Misc/NEWS.d/next/Library/2024-02-15-23-42-54.gh-issue-112006.4wxcK-.rst @@ -1,3 +1,2 @@ Fix :func:`inspect.unwrap` for types with the ``__wrapper__`` data -descriptor. Fix :meth:`inspect.Signature.from_callable` for builtins -:func:`classmethod` and :func:`staticmethod`. +descriptor.