https://github.com/python/cpython/commit/025a2135eff848abf24f9dc52c81386eea9da397
commit: 025a2135eff848abf24f9dc52c81386eea9da397
branch: main
author: Ju4tCode <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2025-08-28T17:57:53+03:00
summary:
gh-137317: Fix inspect.signature() for class with wrapped __init__ or __new__
(GH-137862)
Fixed several cases where __init__, __new__ or metaclass` __call__ is a
descriptor
that returns a wrapped function.
files:
A Misc/NEWS.d/next/Library/2025-08-16-16-04-15.gh-issue-137317.Dl13B5.rst
M Lib/inspect.py
M Lib/test/test_inspect/test_inspect.py
diff --git a/Lib/inspect.py b/Lib/inspect.py
index f10401e8853317..5a46987b78b437 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -1917,17 +1917,21 @@ def _signature_get_user_defined_method(cls,
method_name, *, follow_wrapper_chain
if meth is None:
return None
+ # NOTE: The meth may wraps a non-user-defined callable.
+ # In this case, we treat the meth as non-user-defined callable too.
+ # (e.g. cls.__new__ generated by @warnings.deprecated)
+ unwrapped_meth = None
if follow_wrapper_chains:
- meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
+ unwrapped_meth = unwrap(meth, stop=(lambda m: hasattr(m,
"__signature__")
or _signature_is_builtin(m)))
- if isinstance(meth, _NonUserDefinedCallables):
+
+ if (isinstance(meth, _NonUserDefinedCallables)
+ or isinstance(unwrapped_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
diff --git a/Lib/test/test_inspect/test_inspect.py
b/Lib/test/test_inspect/test_inspect.py
index 8de2be07aa040d..88fa4b7460c412 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -148,6 +148,29 @@ def meth_self_o(self, object, /): pass
def meth_type_noargs(type, /): pass
def meth_type_o(type, object, /): pass
+# Decorator decorator that returns a simple wrapped function
+def identity_wrapper(func):
+ @functools.wraps(func)
+ def wrapped(*args, **kwargs):
+ return func(*args, **kwargs)
+ return wrapped
+
+# Original signature of the simple wrapped function returned by
+# identity_wrapper().
+varargs_signature = (
+ (('args', ..., ..., 'var_positional'),
+ ('kwargs', ..., ..., 'var_keyword')),
+ ...,
+)
+
+# Decorator decorator that returns a simple descriptor
+class custom_descriptor:
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, instance, owner):
+ return self.func.__get__(instance, owner)
+
class TestPredicates(IsTestBase):
@@ -4027,44 +4050,266 @@ 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):
+ def test_signature_on_class_with_wrapped_metaclass_call(self):
+ class CM(type):
+ @identity_wrapper
+ def __call__(cls, a):
+ pass
+ class C(metaclass=CM):
+ def __init__(self, b):
pass
- self.assertEqual(self.signature(Foo),
- ((('a', ..., ..., "positional_or_keyword"),
- ('b', ..., ..., "positional_or_keyword")),
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
...))
- self.assertEqual(self.signature(Foo.__new__),
- ((('cls', ..., ..., "positional_or_keyword"),
- ('a', ..., ..., "positional_or_keyword"),
- ('b', ..., ..., "positional_or_keyword")),
- ...))
+ with self.subTest('classmethod'):
+ class CM(type):
+ @classmethod
+ @identity_wrapper
+ def __call__(cls, a):
+ return a
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
- class Bar:
- __new__ = identity(object.__new__)
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
- varargs_signature = (
- (('args', ..., ..., 'var_positional'),
- ('kwargs', ..., ..., 'var_keyword')),
- ...,
- )
+ with self.subTest('staticmethod'):
+ class CM(type):
+ @staticmethod
+ @identity_wrapper
+ def __call__(a):
+ return a
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('MethodType'):
+ class A:
+ @identity_wrapper
+ def call(self, a):
+ return a
+ class CM(type):
+ __call__ = A().call
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('descriptor'):
+ class CM(type):
+ @custom_descriptor
+ @identity_wrapper
+ def __call__(self, a):
+ return a
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+ self.assertEqual(self.signature(C.__call__),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ self.assertEqual(self.signature(C, follow_wrapped=False),
+ varargs_signature)
+ self.assertEqual(self.signature(C.__call__, follow_wrapped=False),
+ varargs_signature)
+
+ def test_signature_on_class_with_wrapped_init(self):
+ class C:
+ @identity_wrapper
+ def __init__(self, b):
+ pass
+
+ C(1) # does not raise
+ self.assertEqual(self.signature(C),
+ ((('b', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('classmethod'):
+ class C:
+ @classmethod
+ @identity_wrapper
+ def __init__(cls, b):
+ pass
+
+ C(1) # does not raise
+ self.assertEqual(self.signature(C),
+ ((('b', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('staticmethod'):
+ class C:
+ @staticmethod
+ @identity_wrapper
+ def __init__(b):
+ pass
+
+ C(1) # does not raise
+ self.assertEqual(self.signature(C),
+ ((('b', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('MethodType'):
+ class A:
+ @identity_wrapper
+ def call(self, a):
+ pass
+
+ class C:
+ __init__ = A().call
+
+ C(1) # does not raise
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('partial'):
+ class C:
+ __init__ = functools.partial(identity_wrapper(lambda x, a, b:
None), 2)
+
+ C(1) # does not raise
+ self.assertEqual(self.signature(C),
+ ((('b', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('partialmethod'):
+ class C:
+ @identity_wrapper
+ def _init(self, x, a):
+ self.a = (x, a)
+ __init__ = functools.partialmethod(_init, 2)
+
+ self.assertEqual(C(1).a, (2, 1))
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('descriptor'):
+ class C:
+ @custom_descriptor
+ @identity_wrapper
+ def __init__(self, a):
+ pass
+
+ C(1) # does not raise
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+ self.assertEqual(self.signature(C.__init__),
+ ((('self', ..., ..., "positional_or_keyword"),
+ ('a', ..., ..., "positional_or_keyword")),
+ ...))
+
+ self.assertEqual(self.signature(C, follow_wrapped=False),
+ varargs_signature)
+ self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
+ varargs_signature)
+
+ def test_signature_on_class_with_wrapped_new(self):
+ with self.subTest('FunctionType'):
+ class C:
+ @identity_wrapper
+ def __new__(cls, a):
+ return a
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('classmethod'):
+ class C:
+ @classmethod
+ @identity_wrapper
+ def __new__(cls, cls2, a):
+ return a
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('staticmethod'):
+ class C:
+ @staticmethod
+ @identity_wrapper
+ def __new__(cls, a):
+ return a
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('MethodType'):
+ class A:
+ @identity_wrapper
+ def call(self, cls, a):
+ return a
+ class C:
+ __new__ = A().call
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('partial'):
+ class C:
+ __new__ = functools.partial(identity_wrapper(lambda x, cls, a:
(x, a)), 2)
+
+ self.assertEqual(C(1), (2, 1))
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('partialmethod'):
+ class C:
+ __new__ = functools.partialmethod(identity_wrapper(lambda cls,
x, a: (x, a)), 2)
+
+ self.assertEqual(C(1), (2, 1))
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('descriptor'):
+ class C:
+ @custom_descriptor
+ @identity_wrapper
+ def __new__(cls, a):
+ return a
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+ self.assertEqual(self.signature(C.__new__),
+ ((('cls', ..., ..., "positional_or_keyword"),
+ ('a', ..., ..., "positional_or_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)
+ self.assertEqual(self.signature(C, follow_wrapped=False),
+ varargs_signature)
+ self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
+ varargs_signature)
def test_signature_on_class_with_init(self):
class C:
diff --git
a/Misc/NEWS.d/next/Library/2025-08-16-16-04-15.gh-issue-137317.Dl13B5.rst
b/Misc/NEWS.d/next/Library/2025-08-16-16-04-15.gh-issue-137317.Dl13B5.rst
new file mode 100644
index 00000000000000..026cc320455963
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-08-16-16-04-15.gh-issue-137317.Dl13B5.rst
@@ -0,0 +1,3 @@
+:func:`inspect.signature` now correctly handles classes that use a descriptor
+on a wrapped :meth:`!__init__` or :meth:`!__new__` method.
+Contributed by Yongyu Yan.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]