https://github.com/python/cpython/commit/766c5f75b9c3961dfb0ddc5e9637e91006b066dd
commit: 766c5f75b9c3961dfb0ddc5e9637e91006b066dd
branch: 3.13
author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
date: 2025-05-01T20:06:07-07:00
summary:

[3.13] gh-119605: Respect `follow_wrapped` for `__init__` and `__new__` when 
getting class signature with `inspect.signature` (GH-132055) (#133277)

gh-119605: Respect `follow_wrapped` for `__init__` and `__new__` when getting 
class signature with `inspect.signature` (GH-132055)
(cherry picked from commit b8633f9aca9b198e5592106b649389d638cbc620)

Co-authored-by: Xuehai Pan <xuehai...@pku.edu.cn>

files:
A Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst
M Lib/inspect.py
M Lib/test/test_inspect/test_inspect.py
M Lib/test/test_warnings/__init__.py

diff --git a/Lib/inspect.py b/Lib/inspect.py
index 401a76391aa1e2..de2fe7e1ae0f6d 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -2000,7 +2000,7 @@ def getasyncgenlocals(agen):
                             types.BuiltinFunctionType)
 
 
-def _signature_get_user_defined_method(cls, method_name):
+def _signature_get_user_defined_method(cls, method_name, *, 
follow_wrapper_chains=True):
     """Private helper. Checks if ``cls`` has an attribute
     named ``method_name`` and returns it only if it is a
     pure python function.
@@ -2009,12 +2009,20 @@ def _signature_get_user_defined_method(cls, 
method_name):
         meth = getattr(cls, method_name, None)
     else:
         meth = getattr_static(cls, method_name, None)
-    if meth is None or isinstance(meth, _NonUserDefinedCallables):
+    if meth is None:
+        return None
+
+    if follow_wrapper_chains:
+        meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
+                                  or _signature_is_builtin(m)))
+    if isinstance(meth, _NonUserDefinedCallables):
         # Once '__signature__' will be added to 'C'-level
         # callables, this check won't be necessary
         return None
     if method_name != '__new__':
         meth = _descriptor_get(meth, cls)
+        if follow_wrapper_chains:
+            meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__"))
     return meth
 
 
@@ -2589,12 +2597,26 @@ def _signature_from_callable(obj, *,
 
         # First, let's see if it has an overloaded __call__ defined
         # in its metaclass
-        call = _signature_get_user_defined_method(type(obj), '__call__')
+        call = _signature_get_user_defined_method(
+            type(obj),
+            '__call__',
+            follow_wrapper_chains=follow_wrapper_chains,
+        )
         if call is not None:
             return _get_signature_of(call)
 
-        new = _signature_get_user_defined_method(obj, '__new__')
-        init = _signature_get_user_defined_method(obj, '__init__')
+        # NOTE: The user-defined method can be a function with a thin wrapper
+        # around object.__new__ (e.g., generated by `@warnings.deprecated`)
+        new = _signature_get_user_defined_method(
+            obj,
+            '__new__',
+            follow_wrapper_chains=follow_wrapper_chains,
+        )
+        init = _signature_get_user_defined_method(
+            obj,
+            '__init__',
+            follow_wrapper_chains=follow_wrapper_chains,
+        )
 
         # Go through the MRO and see if any class has user-defined
         # pure Python __new__ or __init__ method
@@ -2634,10 +2656,14 @@ def _signature_from_callable(obj, *,
         # Last option is to check if its '__init__' is
         # object.__init__ or type.__init__.
         if type not in obj.__mro__:
+            obj_init = obj.__init__
+            obj_new = obj.__new__
+            if follow_wrapper_chains:
+                obj_init = unwrap(obj_init)
+                obj_new = unwrap(obj_new)
             # We have a class (not metaclass), but no user-defined
             # __init__ or __new__ for it
-            if (obj.__init__ is object.__init__ and
-                obj.__new__ is object.__new__):
+            if obj_init is object.__init__ and obj_new is object.__new__:
                 # Return a signature of 'object' builtin.
                 return sigcls.from_callable(object)
             else:
diff --git a/Lib/test/test_inspect/test_inspect.py 
b/Lib/test/test_inspect/test_inspect.py
index abacb4ad6c29b2..d729447929f612 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -3967,7 +3967,6 @@ def wrapped_foo_call():
                            ('b', ..., ..., "positional_or_keyword")),
                           ...))
 
-
     def test_signature_on_class(self):
         class C:
             def __init__(self, a):
@@ -4144,6 +4143,45 @@ def __init__(self, b):
                            ('bar', 2, ..., "keyword_only")),
                           ...))
 
+    def test_signature_on_class_with_decorated_new(self):
+        def identity(func):
+            @functools.wraps(func)
+            def wrapped(*args, **kwargs):
+                return func(*args, **kwargs)
+            return wrapped
+
+        class Foo:
+            @identity
+            def __new__(cls, a, b):
+                pass
+
+        self.assertEqual(self.signature(Foo),
+                         ((('a', ..., ..., "positional_or_keyword"),
+                           ('b', ..., ..., "positional_or_keyword")),
+                          ...))
+
+        self.assertEqual(self.signature(Foo.__new__),
+                         ((('cls', ..., ..., "positional_or_keyword"),
+                           ('a', ..., ..., "positional_or_keyword"),
+                           ('b', ..., ..., "positional_or_keyword")),
+                          ...))
+
+        class Bar:
+            __new__ = identity(object.__new__)
+
+        varargs_signature = (
+            (('args', ..., ..., 'var_positional'),
+             ('kwargs', ..., ..., 'var_keyword')),
+            ...,
+        )
+
+        self.assertEqual(self.signature(Bar), ((), ...))
+        self.assertEqual(self.signature(Bar.__new__), varargs_signature)
+        self.assertEqual(self.signature(Bar, follow_wrapped=False),
+                         varargs_signature)
+        self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False),
+                         varargs_signature)
+
     def test_signature_on_class_with_init(self):
         class C:
             def __init__(self, b):
diff --git a/Lib/test/test_warnings/__init__.py 
b/Lib/test/test_warnings/__init__.py
index e2dd909647078b..a1c9bd67cdcd90 100644
--- a/Lib/test/test_warnings/__init__.py
+++ b/Lib/test/test_warnings/__init__.py
@@ -1826,10 +1826,70 @@ async def coro(self):
         self.assertFalse(inspect.iscoroutinefunction(Cls.sync))
         self.assertTrue(inspect.iscoroutinefunction(Cls.coro))
 
+    def test_inspect_class_signature(self):
+        class Cls1:  # no __init__ or __new__
+            pass
+
+        class Cls2:  # __new__ only
+            def __new__(cls, x, y):
+                return super().__new__(cls)
+
+        class Cls3:  # __init__ only
+            def __init__(self, x, y):
+                pass
+
+        class Cls4:  # __new__ and __init__
+            def __new__(cls, x, y):
+                return super().__new__(cls)
+
+            def __init__(self, x, y):
+                pass
+
+        class Cls5(Cls1):  # inherits no __init__ or __new__
+            pass
+
+        class Cls6(Cls2):  # inherits __new__ only
+            pass
+
+        class Cls7(Cls3):  # inherits __init__ only
+            pass
+
+        class Cls8(Cls4):  # inherits __new__ and __init__
+            pass
+
+        # The `@deprecated` decorator will update the class in-place.
+        # Test the child classes first.
+        for cls in reversed((Cls1, Cls2, Cls3, Cls4, Cls5, Cls6, Cls7, Cls8)):
+            with self.subTest(f'class {cls.__name__} signature'):
+                try:
+                    original_signature = inspect.signature(cls)
+                except ValueError:
+                    original_signature = None
+                try:
+                    original_new_signature = inspect.signature(cls.__new__)
+                except ValueError:
+                    original_new_signature = None
+
+                deprecated_cls = deprecated("depr")(cls)
+
+                try:
+                    deprecated_signature = inspect.signature(deprecated_cls)
+                except ValueError:
+                    deprecated_signature = None
+                self.assertEqual(original_signature, deprecated_signature)
+
+                try:
+                    deprecated_new_signature = 
inspect.signature(deprecated_cls.__new__)
+                except ValueError:
+                    deprecated_new_signature = None
+                self.assertEqual(original_new_signature, 
deprecated_new_signature)
+
+
 def setUpModule():
     py_warnings.onceregistry.clear()
     c_warnings.onceregistry.clear()
 
+
 tearDownModule = setUpModule
 
 if __name__ == "__main__":
diff --git 
a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst 
b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst
new file mode 100644
index 00000000000000..cf8065afc2e5b7
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst
@@ -0,0 +1,4 @@
+Respect ``follow_wrapped`` for :meth:`!__init__` and :meth:`!__new__` methods
+when getting the class signature for a class with :func:`inspect.signature`.
+Preserve class signature after wrapping with :func:`warnings.deprecated`.
+Patch by Xuehai Pan.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to