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.
This commit is contained in:
Ju4tCode 2025-08-28 22:57:53 +08:00 committed by GitHub
parent ef4dd1d655
commit 025a2135ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 287 additions and 35 deletions

View file

@ -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

View file

@ -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 @@ class TestSignatureObject(unittest.TestCase):
('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(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(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(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:

View file

@ -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.