Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
223 changes: 198 additions & 25 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -4027,44 +4027,217 @@ def __init__(self, b):
('bar', 2, ..., "keyword_only")),
...))

def test_signature_on_class_with_decorated_new(self):
def test_signature_on_class_with_decorated_init(self):
def identity(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
return func(*args, **kwargs)
return wrapped

class Foo:
class C:
@identity
def __new__(cls, a, b):
def __init__(self, b):
pass

self.assertEqual(self.signature(Foo),
((('a', ..., ..., "positional_or_keyword"),
('b', ..., ..., "positional_or_keyword")),
...))
C(1) # does not raise
self.assertEqual(self.signature(C),
((('b', ..., ..., "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 C:
@classmethod
@identity
def __init__(cls, b):
pass

class Bar:
__new__ = identity(object.__new__)
C(1) # does not raise
self.assertEqual(self.signature(C),
((('b', ..., ..., "positional_or_keyword"),),
...))

varargs_signature = (
(('args', ..., ..., 'var_positional'),
('kwargs', ..., ..., 'var_keyword')),
...,
)
with self.subTest('staticmethod'):
class C:
@staticmethod
@identity
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
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__ = identity(functools.partial(lambda x, a, b: None, 2))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not this be functools.partial(identity(lambda x, a, b: None), 2)? Although the "partial" and "partialmethod" subtests are not affected by all these changes, so I am not sure that it makes sense to keep them. If they do not fail, we cannot be sure that they are meaningful.

Copy link
Contributor Author

@yanyongyu yanyongyu Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing out the error, I will correct this.

These test cases are based on the ones below (test_signature_on_class_with_init and test_signature_on_class_with_new), and I believe they need to be this detailed to catch potential bugs from changes made in inspect.signature's behavior.


C(1) # does not raise
self.assertEqual(self.signature(C),
((('b', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('partialmethod'):
class C:
@identity
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"),),
...))

class Desc:
def __init__(self, func):
self.func = identity(func)

def __get__(self, instance, owner):
return self.func.__get__(instance, owner)

with self.subTest('descriptor'):
class C:
__init__ = Desc(lambda self, a: None)

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")),
...))

varargs_signature = (
(('args', ..., ..., 'var_positional'),
('kwargs', ..., ..., 'var_keyword')),
...,
)
self.assertEqual(self.signature(C, follow_wrapped=False),
varargs_signature)
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
varargs_signature)

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_decorated_new(self):
def identity(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
return func(*args, **kwargs)
return wrapped

with self.subTest('FunctionType'):
class C:
@identity
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
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
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
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__ = identity(functools.partial(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(lambda cls, x, a: (x, a)), 2)

self.assertEqual(C(1), (2, 1))
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

class Desc:
def __init__(self, func):
self.func = identity(func)

def __get__(self, instance, owner):
return self.func.__get__(instance, owner)

with self.subTest('descriptor'):
class C:
__new__ = Desc(lambda cls, a: 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")),
...))

varargs_signature = (
(('args', ..., ..., 'var_positional'),
('kwargs', ..., ..., 'var_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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading