Skip to content

Commit b8633f9

Browse files
authored
gh-119605: Respect follow_wrapped for __init__ and __new__ when getting class signature with inspect.signature (#132055)
1 parent c141340 commit b8633f9

File tree

4 files changed

+136
-8
lines changed

4 files changed

+136
-8
lines changed

Lib/inspect.py

+33-7
Original file line numberDiff line numberDiff line change
@@ -1901,7 +1901,7 @@ def getasyncgenlocals(agen):
19011901
types.BuiltinFunctionType)
19021902

19031903

1904-
def _signature_get_user_defined_method(cls, method_name):
1904+
def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chains=True):
19051905
"""Private helper. Checks if ``cls`` has an attribute
19061906
named ``method_name`` and returns it only if it is a
19071907
pure python function.
@@ -1910,12 +1910,20 @@ def _signature_get_user_defined_method(cls, method_name):
19101910
meth = getattr(cls, method_name, None)
19111911
else:
19121912
meth = getattr_static(cls, method_name, None)
1913-
if meth is None or isinstance(meth, _NonUserDefinedCallables):
1913+
if meth is None:
1914+
return None
1915+
1916+
if follow_wrapper_chains:
1917+
meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
1918+
or _signature_is_builtin(m)))
1919+
if isinstance(meth, _NonUserDefinedCallables):
19141920
# Once '__signature__' will be added to 'C'-level
19151921
# callables, this check won't be necessary
19161922
return None
19171923
if method_name != '__new__':
19181924
meth = _descriptor_get(meth, cls)
1925+
if follow_wrapper_chains:
1926+
meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__"))
19191927
return meth
19201928

19211929

@@ -2507,12 +2515,26 @@ def _signature_from_callable(obj, *,
25072515

25082516
# First, let's see if it has an overloaded __call__ defined
25092517
# in its metaclass
2510-
call = _signature_get_user_defined_method(type(obj), '__call__')
2518+
call = _signature_get_user_defined_method(
2519+
type(obj),
2520+
'__call__',
2521+
follow_wrapper_chains=follow_wrapper_chains,
2522+
)
25112523
if call is not None:
25122524
return _get_signature_of(call)
25132525

2514-
new = _signature_get_user_defined_method(obj, '__new__')
2515-
init = _signature_get_user_defined_method(obj, '__init__')
2526+
# NOTE: The user-defined method can be a function with a thin wrapper
2527+
# around object.__new__ (e.g., generated by `@warnings.deprecated`)
2528+
new = _signature_get_user_defined_method(
2529+
obj,
2530+
'__new__',
2531+
follow_wrapper_chains=follow_wrapper_chains,
2532+
)
2533+
init = _signature_get_user_defined_method(
2534+
obj,
2535+
'__init__',
2536+
follow_wrapper_chains=follow_wrapper_chains,
2537+
)
25162538

25172539
# Go through the MRO and see if any class has user-defined
25182540
# pure Python __new__ or __init__ method
@@ -2552,10 +2574,14 @@ def _signature_from_callable(obj, *,
25522574
# Last option is to check if its '__init__' is
25532575
# object.__init__ or type.__init__.
25542576
if type not in obj.__mro__:
2577+
obj_init = obj.__init__
2578+
obj_new = obj.__new__
2579+
if follow_wrapper_chains:
2580+
obj_init = unwrap(obj_init)
2581+
obj_new = unwrap(obj_new)
25552582
# We have a class (not metaclass), but no user-defined
25562583
# __init__ or __new__ for it
2557-
if (obj.__init__ is object.__init__ and
2558-
obj.__new__ is object.__new__):
2584+
if obj_init is object.__init__ and obj_new is object.__new__:
25592585
# Return a signature of 'object' builtin.
25602586
return sigcls.from_callable(object)
25612587
else:

Lib/test/test_inspect/test_inspect.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -3847,7 +3847,6 @@ def wrapped_foo_call():
38473847
('b', ..., ..., "positional_or_keyword")),
38483848
...))
38493849

3850-
38513850
def test_signature_on_class(self):
38523851
class C:
38533852
def __init__(self, a):
@@ -4022,6 +4021,45 @@ def __init__(self, b):
40224021
('bar', 2, ..., "keyword_only")),
40234022
...))
40244023

4024+
def test_signature_on_class_with_decorated_new(self):
4025+
def identity(func):
4026+
@functools.wraps(func)
4027+
def wrapped(*args, **kwargs):
4028+
return func(*args, **kwargs)
4029+
return wrapped
4030+
4031+
class Foo:
4032+
@identity
4033+
def __new__(cls, a, b):
4034+
pass
4035+
4036+
self.assertEqual(self.signature(Foo),
4037+
((('a', ..., ..., "positional_or_keyword"),
4038+
('b', ..., ..., "positional_or_keyword")),
4039+
...))
4040+
4041+
self.assertEqual(self.signature(Foo.__new__),
4042+
((('cls', ..., ..., "positional_or_keyword"),
4043+
('a', ..., ..., "positional_or_keyword"),
4044+
('b', ..., ..., "positional_or_keyword")),
4045+
...))
4046+
4047+
class Bar:
4048+
__new__ = identity(object.__new__)
4049+
4050+
varargs_signature = (
4051+
(('args', ..., ..., 'var_positional'),
4052+
('kwargs', ..., ..., 'var_keyword')),
4053+
...,
4054+
)
4055+
4056+
self.assertEqual(self.signature(Bar), ((), ...))
4057+
self.assertEqual(self.signature(Bar.__new__), varargs_signature)
4058+
self.assertEqual(self.signature(Bar, follow_wrapped=False),
4059+
varargs_signature)
4060+
self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False),
4061+
varargs_signature)
4062+
40254063
def test_signature_on_class_with_init(self):
40264064
class C:
40274065
def __init__(self, b):

Lib/test/test_warnings/__init__.py

+60
Original file line numberDiff line numberDiff line change
@@ -2018,10 +2018,70 @@ async def coro(self):
20182018
self.assertFalse(inspect.iscoroutinefunction(Cls.sync))
20192019
self.assertTrue(inspect.iscoroutinefunction(Cls.coro))
20202020

2021+
def test_inspect_class_signature(self):
2022+
class Cls1: # no __init__ or __new__
2023+
pass
2024+
2025+
class Cls2: # __new__ only
2026+
def __new__(cls, x, y):
2027+
return super().__new__(cls)
2028+
2029+
class Cls3: # __init__ only
2030+
def __init__(self, x, y):
2031+
pass
2032+
2033+
class Cls4: # __new__ and __init__
2034+
def __new__(cls, x, y):
2035+
return super().__new__(cls)
2036+
2037+
def __init__(self, x, y):
2038+
pass
2039+
2040+
class Cls5(Cls1): # inherits no __init__ or __new__
2041+
pass
2042+
2043+
class Cls6(Cls2): # inherits __new__ only
2044+
pass
2045+
2046+
class Cls7(Cls3): # inherits __init__ only
2047+
pass
2048+
2049+
class Cls8(Cls4): # inherits __new__ and __init__
2050+
pass
2051+
2052+
# The `@deprecated` decorator will update the class in-place.
2053+
# Test the child classes first.
2054+
for cls in reversed((Cls1, Cls2, Cls3, Cls4, Cls5, Cls6, Cls7, Cls8)):
2055+
with self.subTest(f'class {cls.__name__} signature'):
2056+
try:
2057+
original_signature = inspect.signature(cls)
2058+
except ValueError:
2059+
original_signature = None
2060+
try:
2061+
original_new_signature = inspect.signature(cls.__new__)
2062+
except ValueError:
2063+
original_new_signature = None
2064+
2065+
deprecated_cls = deprecated("depr")(cls)
2066+
2067+
try:
2068+
deprecated_signature = inspect.signature(deprecated_cls)
2069+
except ValueError:
2070+
deprecated_signature = None
2071+
self.assertEqual(original_signature, deprecated_signature)
2072+
2073+
try:
2074+
deprecated_new_signature = inspect.signature(deprecated_cls.__new__)
2075+
except ValueError:
2076+
deprecated_new_signature = None
2077+
self.assertEqual(original_new_signature, deprecated_new_signature)
2078+
2079+
20212080
def setUpModule():
20222081
py_warnings.onceregistry.clear()
20232082
c_warnings.onceregistry.clear()
20242083

2084+
20252085
tearDownModule = setUpModule
20262086

20272087
if __name__ == "__main__":
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Respect ``follow_wrapped`` for :meth:`!__init__` and :meth:`!__new__` methods
2+
when getting the class signature for a class with :func:`inspect.signature`.
3+
Preserve class signature after wrapping with :func:`warnings.deprecated`.
4+
Patch by Xuehai Pan.

0 commit comments

Comments
 (0)