Skip to content

Commit 766c5f7

Browse files
[3.13] pythongh-119605: Respect follow_wrapped for __init__ and __new__ when getting class signature with inspect.signature (pythonGH-132055) (python#133277)
pythongh-119605: Respect `follow_wrapped` for `__init__` and `__new__` when getting class signature with `inspect.signature` (pythonGH-132055) (cherry picked from commit b8633f9) Co-authored-by: Xuehai Pan <[email protected]>
1 parent f7d1109 commit 766c5f7

File tree

4 files changed

+136
-8
lines changed

4 files changed

+136
-8
lines changed

Lib/inspect.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,7 +2000,7 @@ def getasyncgenlocals(agen):
20002000
types.BuiltinFunctionType)
20012001

20022002

2003-
def _signature_get_user_defined_method(cls, method_name):
2003+
def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chains=True):
20042004
"""Private helper. Checks if ``cls`` has an attribute
20052005
named ``method_name`` and returns it only if it is a
20062006
pure python function.
@@ -2009,12 +2009,20 @@ def _signature_get_user_defined_method(cls, method_name):
20092009
meth = getattr(cls, method_name, None)
20102010
else:
20112011
meth = getattr_static(cls, method_name, None)
2012-
if meth is None or isinstance(meth, _NonUserDefinedCallables):
2012+
if meth is None:
2013+
return None
2014+
2015+
if follow_wrapper_chains:
2016+
meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
2017+
or _signature_is_builtin(m)))
2018+
if isinstance(meth, _NonUserDefinedCallables):
20132019
# Once '__signature__' will be added to 'C'-level
20142020
# callables, this check won't be necessary
20152021
return None
20162022
if method_name != '__new__':
20172023
meth = _descriptor_get(meth, cls)
2024+
if follow_wrapper_chains:
2025+
meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__"))
20182026
return meth
20192027

20202028

@@ -2589,12 +2597,26 @@ def _signature_from_callable(obj, *,
25892597

25902598
# First, let's see if it has an overloaded __call__ defined
25912599
# in its metaclass
2592-
call = _signature_get_user_defined_method(type(obj), '__call__')
2600+
call = _signature_get_user_defined_method(
2601+
type(obj),
2602+
'__call__',
2603+
follow_wrapper_chains=follow_wrapper_chains,
2604+
)
25932605
if call is not None:
25942606
return _get_signature_of(call)
25952607

2596-
new = _signature_get_user_defined_method(obj, '__new__')
2597-
init = _signature_get_user_defined_method(obj, '__init__')
2608+
# NOTE: The user-defined method can be a function with a thin wrapper
2609+
# around object.__new__ (e.g., generated by `@warnings.deprecated`)
2610+
new = _signature_get_user_defined_method(
2611+
obj,
2612+
'__new__',
2613+
follow_wrapper_chains=follow_wrapper_chains,
2614+
)
2615+
init = _signature_get_user_defined_method(
2616+
obj,
2617+
'__init__',
2618+
follow_wrapper_chains=follow_wrapper_chains,
2619+
)
25982620

25992621
# Go through the MRO and see if any class has user-defined
26002622
# pure Python __new__ or __init__ method
@@ -2634,10 +2656,14 @@ def _signature_from_callable(obj, *,
26342656
# Last option is to check if its '__init__' is
26352657
# object.__init__ or type.__init__.
26362658
if type not in obj.__mro__:
2659+
obj_init = obj.__init__
2660+
obj_new = obj.__new__
2661+
if follow_wrapper_chains:
2662+
obj_init = unwrap(obj_init)
2663+
obj_new = unwrap(obj_new)
26372664
# We have a class (not metaclass), but no user-defined
26382665
# __init__ or __new__ for it
2639-
if (obj.__init__ is object.__init__ and
2640-
obj.__new__ is object.__new__):
2666+
if obj_init is object.__init__ and obj_new is object.__new__:
26412667
# Return a signature of 'object' builtin.
26422668
return sigcls.from_callable(object)
26432669
else:

Lib/test/test_inspect/test_inspect.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3967,7 +3967,6 @@ def wrapped_foo_call():
39673967
('b', ..., ..., "positional_or_keyword")),
39683968
...))
39693969

3970-
39713970
def test_signature_on_class(self):
39723971
class C:
39733972
def __init__(self, a):
@@ -4144,6 +4143,45 @@ def __init__(self, b):
41444143
('bar', 2, ..., "keyword_only")),
41454144
...))
41464145

4146+
def test_signature_on_class_with_decorated_new(self):
4147+
def identity(func):
4148+
@functools.wraps(func)
4149+
def wrapped(*args, **kwargs):
4150+
return func(*args, **kwargs)
4151+
return wrapped
4152+
4153+
class Foo:
4154+
@identity
4155+
def __new__(cls, a, b):
4156+
pass
4157+
4158+
self.assertEqual(self.signature(Foo),
4159+
((('a', ..., ..., "positional_or_keyword"),
4160+
('b', ..., ..., "positional_or_keyword")),
4161+
...))
4162+
4163+
self.assertEqual(self.signature(Foo.__new__),
4164+
((('cls', ..., ..., "positional_or_keyword"),
4165+
('a', ..., ..., "positional_or_keyword"),
4166+
('b', ..., ..., "positional_or_keyword")),
4167+
...))
4168+
4169+
class Bar:
4170+
__new__ = identity(object.__new__)
4171+
4172+
varargs_signature = (
4173+
(('args', ..., ..., 'var_positional'),
4174+
('kwargs', ..., ..., 'var_keyword')),
4175+
...,
4176+
)
4177+
4178+
self.assertEqual(self.signature(Bar), ((), ...))
4179+
self.assertEqual(self.signature(Bar.__new__), varargs_signature)
4180+
self.assertEqual(self.signature(Bar, follow_wrapped=False),
4181+
varargs_signature)
4182+
self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False),
4183+
varargs_signature)
4184+
41474185
def test_signature_on_class_with_init(self):
41484186
class C:
41494187
def __init__(self, b):

Lib/test/test_warnings/__init__.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,10 +1826,70 @@ async def coro(self):
18261826
self.assertFalse(inspect.iscoroutinefunction(Cls.sync))
18271827
self.assertTrue(inspect.iscoroutinefunction(Cls.coro))
18281828

1829+
def test_inspect_class_signature(self):
1830+
class Cls1: # no __init__ or __new__
1831+
pass
1832+
1833+
class Cls2: # __new__ only
1834+
def __new__(cls, x, y):
1835+
return super().__new__(cls)
1836+
1837+
class Cls3: # __init__ only
1838+
def __init__(self, x, y):
1839+
pass
1840+
1841+
class Cls4: # __new__ and __init__
1842+
def __new__(cls, x, y):
1843+
return super().__new__(cls)
1844+
1845+
def __init__(self, x, y):
1846+
pass
1847+
1848+
class Cls5(Cls1): # inherits no __init__ or __new__
1849+
pass
1850+
1851+
class Cls6(Cls2): # inherits __new__ only
1852+
pass
1853+
1854+
class Cls7(Cls3): # inherits __init__ only
1855+
pass
1856+
1857+
class Cls8(Cls4): # inherits __new__ and __init__
1858+
pass
1859+
1860+
# The `@deprecated` decorator will update the class in-place.
1861+
# Test the child classes first.
1862+
for cls in reversed((Cls1, Cls2, Cls3, Cls4, Cls5, Cls6, Cls7, Cls8)):
1863+
with self.subTest(f'class {cls.__name__} signature'):
1864+
try:
1865+
original_signature = inspect.signature(cls)
1866+
except ValueError:
1867+
original_signature = None
1868+
try:
1869+
original_new_signature = inspect.signature(cls.__new__)
1870+
except ValueError:
1871+
original_new_signature = None
1872+
1873+
deprecated_cls = deprecated("depr")(cls)
1874+
1875+
try:
1876+
deprecated_signature = inspect.signature(deprecated_cls)
1877+
except ValueError:
1878+
deprecated_signature = None
1879+
self.assertEqual(original_signature, deprecated_signature)
1880+
1881+
try:
1882+
deprecated_new_signature = inspect.signature(deprecated_cls.__new__)
1883+
except ValueError:
1884+
deprecated_new_signature = None
1885+
self.assertEqual(original_new_signature, deprecated_new_signature)
1886+
1887+
18291888
def setUpModule():
18301889
py_warnings.onceregistry.clear()
18311890
c_warnings.onceregistry.clear()
18321891

1892+
18331893
tearDownModule = setUpModule
18341894

18351895
if __name__ == "__main__":
Lines changed: 4 additions & 0 deletions
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)