Skip to content

Commit 025a213

Browse files
authored
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.
1 parent ef4dd1d commit 025a213

File tree

3 files changed

+287
-35
lines changed

3 files changed

+287
-35
lines changed

Lib/inspect.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,17 +1917,21 @@ def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chain
19171917
if meth is None:
19181918
return None
19191919

1920+
# NOTE: The meth may wraps a non-user-defined callable.
1921+
# In this case, we treat the meth as non-user-defined callable too.
1922+
# (e.g. cls.__new__ generated by @warnings.deprecated)
1923+
unwrapped_meth = None
19201924
if follow_wrapper_chains:
1921-
meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
1925+
unwrapped_meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
19221926
or _signature_is_builtin(m)))
1923-
if isinstance(meth, _NonUserDefinedCallables):
1927+
1928+
if (isinstance(meth, _NonUserDefinedCallables)
1929+
or isinstance(unwrapped_meth, _NonUserDefinedCallables)):
19241930
# Once '__signature__' will be added to 'C'-level
19251931
# callables, this check won't be necessary
19261932
return None
19271933
if method_name != '__new__':
19281934
meth = _descriptor_get(meth, cls)
1929-
if follow_wrapper_chains:
1930-
meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__"))
19311935
return meth
19321936

19331937

Lib/test/test_inspect/test_inspect.py

Lines changed: 276 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,29 @@ def meth_self_o(self, object, /): pass
148148
def meth_type_noargs(type, /): pass
149149
def meth_type_o(type, object, /): pass
150150

151+
# Decorator decorator that returns a simple wrapped function
152+
def identity_wrapper(func):
153+
@functools.wraps(func)
154+
def wrapped(*args, **kwargs):
155+
return func(*args, **kwargs)
156+
return wrapped
157+
158+
# Original signature of the simple wrapped function returned by
159+
# identity_wrapper().
160+
varargs_signature = (
161+
(('args', ..., ..., 'var_positional'),
162+
('kwargs', ..., ..., 'var_keyword')),
163+
...,
164+
)
165+
166+
# Decorator decorator that returns a simple descriptor
167+
class custom_descriptor:
168+
def __init__(self, func):
169+
self.func = func
170+
171+
def __get__(self, instance, owner):
172+
return self.func.__get__(instance, owner)
173+
151174

152175
class TestPredicates(IsTestBase):
153176

@@ -4027,44 +4050,266 @@ def __init__(self, b):
40274050
('bar', 2, ..., "keyword_only")),
40284051
...))
40294052

4030-
def test_signature_on_class_with_decorated_new(self):
4031-
def identity(func):
4032-
@functools.wraps(func)
4033-
def wrapped(*args, **kwargs):
4034-
return func(*args, **kwargs)
4035-
return wrapped
4036-
4037-
class Foo:
4038-
@identity
4039-
def __new__(cls, a, b):
4053+
def test_signature_on_class_with_wrapped_metaclass_call(self):
4054+
class CM(type):
4055+
@identity_wrapper
4056+
def __call__(cls, a):
4057+
pass
4058+
class C(metaclass=CM):
4059+
def __init__(self, b):
40404060
pass
40414061

4042-
self.assertEqual(self.signature(Foo),
4043-
((('a', ..., ..., "positional_or_keyword"),
4044-
('b', ..., ..., "positional_or_keyword")),
4062+
self.assertEqual(self.signature(C),
4063+
((('a', ..., ..., "positional_or_keyword"),),
40454064
...))
40464065

4047-
self.assertEqual(self.signature(Foo.__new__),
4048-
((('cls', ..., ..., "positional_or_keyword"),
4049-
('a', ..., ..., "positional_or_keyword"),
4050-
('b', ..., ..., "positional_or_keyword")),
4051-
...))
4066+
with self.subTest('classmethod'):
4067+
class CM(type):
4068+
@classmethod
4069+
@identity_wrapper
4070+
def __call__(cls, a):
4071+
return a
4072+
class C(metaclass=CM):
4073+
def __init__(self, b):
4074+
pass
40524075

4053-
class Bar:
4054-
__new__ = identity(object.__new__)
4076+
self.assertEqual(C(1), 1)
4077+
self.assertEqual(self.signature(C),
4078+
((('a', ..., ..., "positional_or_keyword"),),
4079+
...))
40554080

4056-
varargs_signature = (
4057-
(('args', ..., ..., 'var_positional'),
4058-
('kwargs', ..., ..., 'var_keyword')),
4059-
...,
4060-
)
4081+
with self.subTest('staticmethod'):
4082+
class CM(type):
4083+
@staticmethod
4084+
@identity_wrapper
4085+
def __call__(a):
4086+
return a
4087+
class C(metaclass=CM):
4088+
def __init__(self, b):
4089+
pass
4090+
4091+
self.assertEqual(C(1), 1)
4092+
self.assertEqual(self.signature(C),
4093+
((('a', ..., ..., "positional_or_keyword"),),
4094+
...))
4095+
4096+
with self.subTest('MethodType'):
4097+
class A:
4098+
@identity_wrapper
4099+
def call(self, a):
4100+
return a
4101+
class CM(type):
4102+
__call__ = A().call
4103+
class C(metaclass=CM):
4104+
def __init__(self, b):
4105+
pass
4106+
4107+
self.assertEqual(C(1), 1)
4108+
self.assertEqual(self.signature(C),
4109+
((('a', ..., ..., "positional_or_keyword"),),
4110+
...))
4111+
4112+
with self.subTest('descriptor'):
4113+
class CM(type):
4114+
@custom_descriptor
4115+
@identity_wrapper
4116+
def __call__(self, a):
4117+
return a
4118+
class C(metaclass=CM):
4119+
def __init__(self, b):
4120+
pass
4121+
4122+
self.assertEqual(C(1), 1)
4123+
self.assertEqual(self.signature(C),
4124+
((('a', ..., ..., "positional_or_keyword"),),
4125+
...))
4126+
self.assertEqual(self.signature(C.__call__),
4127+
((('a', ..., ..., "positional_or_keyword"),),
4128+
...))
4129+
4130+
self.assertEqual(self.signature(C, follow_wrapped=False),
4131+
varargs_signature)
4132+
self.assertEqual(self.signature(C.__call__, follow_wrapped=False),
4133+
varargs_signature)
4134+
4135+
def test_signature_on_class_with_wrapped_init(self):
4136+
class C:
4137+
@identity_wrapper
4138+
def __init__(self, b):
4139+
pass
4140+
4141+
C(1) # does not raise
4142+
self.assertEqual(self.signature(C),
4143+
((('b', ..., ..., "positional_or_keyword"),),
4144+
...))
4145+
4146+
with self.subTest('classmethod'):
4147+
class C:
4148+
@classmethod
4149+
@identity_wrapper
4150+
def __init__(cls, b):
4151+
pass
4152+
4153+
C(1) # does not raise
4154+
self.assertEqual(self.signature(C),
4155+
((('b', ..., ..., "positional_or_keyword"),),
4156+
...))
4157+
4158+
with self.subTest('staticmethod'):
4159+
class C:
4160+
@staticmethod
4161+
@identity_wrapper
4162+
def __init__(b):
4163+
pass
4164+
4165+
C(1) # does not raise
4166+
self.assertEqual(self.signature(C),
4167+
((('b', ..., ..., "positional_or_keyword"),),
4168+
...))
4169+
4170+
with self.subTest('MethodType'):
4171+
class A:
4172+
@identity_wrapper
4173+
def call(self, a):
4174+
pass
4175+
4176+
class C:
4177+
__init__ = A().call
4178+
4179+
C(1) # does not raise
4180+
self.assertEqual(self.signature(C),
4181+
((('a', ..., ..., "positional_or_keyword"),),
4182+
...))
4183+
4184+
with self.subTest('partial'):
4185+
class C:
4186+
__init__ = functools.partial(identity_wrapper(lambda x, a, b: None), 2)
4187+
4188+
C(1) # does not raise
4189+
self.assertEqual(self.signature(C),
4190+
((('b', ..., ..., "positional_or_keyword"),),
4191+
...))
4192+
4193+
with self.subTest('partialmethod'):
4194+
class C:
4195+
@identity_wrapper
4196+
def _init(self, x, a):
4197+
self.a = (x, a)
4198+
__init__ = functools.partialmethod(_init, 2)
4199+
4200+
self.assertEqual(C(1).a, (2, 1))
4201+
self.assertEqual(self.signature(C),
4202+
((('a', ..., ..., "positional_or_keyword"),),
4203+
...))
4204+
4205+
with self.subTest('descriptor'):
4206+
class C:
4207+
@custom_descriptor
4208+
@identity_wrapper
4209+
def __init__(self, a):
4210+
pass
4211+
4212+
C(1) # does not raise
4213+
self.assertEqual(self.signature(C),
4214+
((('a', ..., ..., "positional_or_keyword"),),
4215+
...))
4216+
self.assertEqual(self.signature(C.__init__),
4217+
((('self', ..., ..., "positional_or_keyword"),
4218+
('a', ..., ..., "positional_or_keyword")),
4219+
...))
4220+
4221+
self.assertEqual(self.signature(C, follow_wrapped=False),
4222+
varargs_signature)
4223+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4224+
varargs_signature)
4225+
4226+
def test_signature_on_class_with_wrapped_new(self):
4227+
with self.subTest('FunctionType'):
4228+
class C:
4229+
@identity_wrapper
4230+
def __new__(cls, a):
4231+
return a
4232+
4233+
self.assertEqual(C(1), 1)
4234+
self.assertEqual(self.signature(C),
4235+
((('a', ..., ..., "positional_or_keyword"),),
4236+
...))
4237+
4238+
with self.subTest('classmethod'):
4239+
class C:
4240+
@classmethod
4241+
@identity_wrapper
4242+
def __new__(cls, cls2, a):
4243+
return a
4244+
4245+
self.assertEqual(C(1), 1)
4246+
self.assertEqual(self.signature(C),
4247+
((('a', ..., ..., "positional_or_keyword"),),
4248+
...))
4249+
4250+
with self.subTest('staticmethod'):
4251+
class C:
4252+
@staticmethod
4253+
@identity_wrapper
4254+
def __new__(cls, a):
4255+
return a
4256+
4257+
self.assertEqual(C(1), 1)
4258+
self.assertEqual(self.signature(C),
4259+
((('a', ..., ..., "positional_or_keyword"),),
4260+
...))
4261+
4262+
with self.subTest('MethodType'):
4263+
class A:
4264+
@identity_wrapper
4265+
def call(self, cls, a):
4266+
return a
4267+
class C:
4268+
__new__ = A().call
4269+
4270+
self.assertEqual(C(1), 1)
4271+
self.assertEqual(self.signature(C),
4272+
((('a', ..., ..., "positional_or_keyword"),),
4273+
...))
4274+
4275+
with self.subTest('partial'):
4276+
class C:
4277+
__new__ = functools.partial(identity_wrapper(lambda x, cls, a: (x, a)), 2)
4278+
4279+
self.assertEqual(C(1), (2, 1))
4280+
self.assertEqual(self.signature(C),
4281+
((('a', ..., ..., "positional_or_keyword"),),
4282+
...))
4283+
4284+
with self.subTest('partialmethod'):
4285+
class C:
4286+
__new__ = functools.partialmethod(identity_wrapper(lambda cls, x, a: (x, a)), 2)
4287+
4288+
self.assertEqual(C(1), (2, 1))
4289+
self.assertEqual(self.signature(C),
4290+
((('a', ..., ..., "positional_or_keyword"),),
4291+
...))
4292+
4293+
with self.subTest('descriptor'):
4294+
class C:
4295+
@custom_descriptor
4296+
@identity_wrapper
4297+
def __new__(cls, a):
4298+
return a
4299+
4300+
self.assertEqual(C(1), 1)
4301+
self.assertEqual(self.signature(C),
4302+
((('a', ..., ..., "positional_or_keyword"),),
4303+
...))
4304+
self.assertEqual(self.signature(C.__new__),
4305+
((('cls', ..., ..., "positional_or_keyword"),
4306+
('a', ..., ..., "positional_or_keyword")),
4307+
...))
40614308

4062-
self.assertEqual(self.signature(Bar), ((), ...))
4063-
self.assertEqual(self.signature(Bar.__new__), varargs_signature)
4064-
self.assertEqual(self.signature(Bar, follow_wrapped=False),
4065-
varargs_signature)
4066-
self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False),
4067-
varargs_signature)
4309+
self.assertEqual(self.signature(C, follow_wrapped=False),
4310+
varargs_signature)
4311+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4312+
varargs_signature)
40684313

40694314
def test_signature_on_class_with_init(self):
40704315
class C:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`inspect.signature` now correctly handles classes that use a descriptor
2+
on a wrapped :meth:`!__init__` or :meth:`!__new__` method.
3+
Contributed by Yongyu Yan.

0 commit comments

Comments
 (0)