Skip to content

Commit 7e23d36

Browse files
[3.14] pythongh-137317: Fix inspect.signature() for class with wrapped __init__ or __new__ (pythonGH-137862) (python#138224)
pythongh-137317: Fix inspect.signature() for class with wrapped __init__ or __new__ (pythonGH-137862) Fixed several cases where __init__, __new__ or metaclass` __call__ is a descriptor that returns a wrapped function. (cherry picked from commit 025a213) Co-authored-by: Ju4tCode <[email protected]>
1 parent c050535 commit 7e23d36

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
@@ -1916,17 +1916,21 @@ def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chain
19161916
if meth is None:
19171917
return None
19181918

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

19321936

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

@@ -4068,44 +4091,266 @@ def __init__(self, b):
40684091
('bar', 2, ..., "keyword_only")),
40694092
...))
40704093

4071-
def test_signature_on_class_with_decorated_new(self):
4072-
def identity(func):
4073-
@functools.wraps(func)
4074-
def wrapped(*args, **kwargs):
4075-
return func(*args, **kwargs)
4076-
return wrapped
4077-
4078-
class Foo:
4079-
@identity
4080-
def __new__(cls, a, b):
4094+
def test_signature_on_class_with_wrapped_metaclass_call(self):
4095+
class CM(type):
4096+
@identity_wrapper
4097+
def __call__(cls, a):
4098+
pass
4099+
class C(metaclass=CM):
4100+
def __init__(self, b):
40814101
pass
40824102

4083-
self.assertEqual(self.signature(Foo),
4084-
((('a', ..., ..., "positional_or_keyword"),
4085-
('b', ..., ..., "positional_or_keyword")),
4103+
self.assertEqual(self.signature(C),
4104+
((('a', ..., ..., "positional_or_keyword"),),
40864105
...))
40874106

4088-
self.assertEqual(self.signature(Foo.__new__),
4089-
((('cls', ..., ..., "positional_or_keyword"),
4090-
('a', ..., ..., "positional_or_keyword"),
4091-
('b', ..., ..., "positional_or_keyword")),
4092-
...))
4107+
with self.subTest('classmethod'):
4108+
class CM(type):
4109+
@classmethod
4110+
@identity_wrapper
4111+
def __call__(cls, a):
4112+
return a
4113+
class C(metaclass=CM):
4114+
def __init__(self, b):
4115+
pass
40934116

4094-
class Bar:
4095-
__new__ = identity(object.__new__)
4117+
self.assertEqual(C(1), 1)
4118+
self.assertEqual(self.signature(C),
4119+
((('a', ..., ..., "positional_or_keyword"),),
4120+
...))
40964121

4097-
varargs_signature = (
4098-
(('args', ..., ..., 'var_positional'),
4099-
('kwargs', ..., ..., 'var_keyword')),
4100-
...,
4101-
)
4122+
with self.subTest('staticmethod'):
4123+
class CM(type):
4124+
@staticmethod
4125+
@identity_wrapper
4126+
def __call__(a):
4127+
return a
4128+
class C(metaclass=CM):
4129+
def __init__(self, b):
4130+
pass
4131+
4132+
self.assertEqual(C(1), 1)
4133+
self.assertEqual(self.signature(C),
4134+
((('a', ..., ..., "positional_or_keyword"),),
4135+
...))
4136+
4137+
with self.subTest('MethodType'):
4138+
class A:
4139+
@identity_wrapper
4140+
def call(self, a):
4141+
return a
4142+
class CM(type):
4143+
__call__ = A().call
4144+
class C(metaclass=CM):
4145+
def __init__(self, b):
4146+
pass
4147+
4148+
self.assertEqual(C(1), 1)
4149+
self.assertEqual(self.signature(C),
4150+
((('a', ..., ..., "positional_or_keyword"),),
4151+
...))
4152+
4153+
with self.subTest('descriptor'):
4154+
class CM(type):
4155+
@custom_descriptor
4156+
@identity_wrapper
4157+
def __call__(self, a):
4158+
return a
4159+
class C(metaclass=CM):
4160+
def __init__(self, b):
4161+
pass
4162+
4163+
self.assertEqual(C(1), 1)
4164+
self.assertEqual(self.signature(C),
4165+
((('a', ..., ..., "positional_or_keyword"),),
4166+
...))
4167+
self.assertEqual(self.signature(C.__call__),
4168+
((('a', ..., ..., "positional_or_keyword"),),
4169+
...))
4170+
4171+
self.assertEqual(self.signature(C, follow_wrapped=False),
4172+
varargs_signature)
4173+
self.assertEqual(self.signature(C.__call__, follow_wrapped=False),
4174+
varargs_signature)
4175+
4176+
def test_signature_on_class_with_wrapped_init(self):
4177+
class C:
4178+
@identity_wrapper
4179+
def __init__(self, b):
4180+
pass
4181+
4182+
C(1) # does not raise
4183+
self.assertEqual(self.signature(C),
4184+
((('b', ..., ..., "positional_or_keyword"),),
4185+
...))
4186+
4187+
with self.subTest('classmethod'):
4188+
class C:
4189+
@classmethod
4190+
@identity_wrapper
4191+
def __init__(cls, b):
4192+
pass
4193+
4194+
C(1) # does not raise
4195+
self.assertEqual(self.signature(C),
4196+
((('b', ..., ..., "positional_or_keyword"),),
4197+
...))
4198+
4199+
with self.subTest('staticmethod'):
4200+
class C:
4201+
@staticmethod
4202+
@identity_wrapper
4203+
def __init__(b):
4204+
pass
4205+
4206+
C(1) # does not raise
4207+
self.assertEqual(self.signature(C),
4208+
((('b', ..., ..., "positional_or_keyword"),),
4209+
...))
4210+
4211+
with self.subTest('MethodType'):
4212+
class A:
4213+
@identity_wrapper
4214+
def call(self, a):
4215+
pass
4216+
4217+
class C:
4218+
__init__ = A().call
4219+
4220+
C(1) # does not raise
4221+
self.assertEqual(self.signature(C),
4222+
((('a', ..., ..., "positional_or_keyword"),),
4223+
...))
4224+
4225+
with self.subTest('partial'):
4226+
class C:
4227+
__init__ = functools.partial(identity_wrapper(lambda x, a, b: None), 2)
4228+
4229+
C(1) # does not raise
4230+
self.assertEqual(self.signature(C),
4231+
((('b', ..., ..., "positional_or_keyword"),),
4232+
...))
4233+
4234+
with self.subTest('partialmethod'):
4235+
class C:
4236+
@identity_wrapper
4237+
def _init(self, x, a):
4238+
self.a = (x, a)
4239+
__init__ = functools.partialmethod(_init, 2)
4240+
4241+
self.assertEqual(C(1).a, (2, 1))
4242+
self.assertEqual(self.signature(C),
4243+
((('a', ..., ..., "positional_or_keyword"),),
4244+
...))
4245+
4246+
with self.subTest('descriptor'):
4247+
class C:
4248+
@custom_descriptor
4249+
@identity_wrapper
4250+
def __init__(self, a):
4251+
pass
4252+
4253+
C(1) # does not raise
4254+
self.assertEqual(self.signature(C),
4255+
((('a', ..., ..., "positional_or_keyword"),),
4256+
...))
4257+
self.assertEqual(self.signature(C.__init__),
4258+
((('self', ..., ..., "positional_or_keyword"),
4259+
('a', ..., ..., "positional_or_keyword")),
4260+
...))
4261+
4262+
self.assertEqual(self.signature(C, follow_wrapped=False),
4263+
varargs_signature)
4264+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4265+
varargs_signature)
4266+
4267+
def test_signature_on_class_with_wrapped_new(self):
4268+
with self.subTest('FunctionType'):
4269+
class C:
4270+
@identity_wrapper
4271+
def __new__(cls, a):
4272+
return a
4273+
4274+
self.assertEqual(C(1), 1)
4275+
self.assertEqual(self.signature(C),
4276+
((('a', ..., ..., "positional_or_keyword"),),
4277+
...))
4278+
4279+
with self.subTest('classmethod'):
4280+
class C:
4281+
@classmethod
4282+
@identity_wrapper
4283+
def __new__(cls, cls2, a):
4284+
return a
4285+
4286+
self.assertEqual(C(1), 1)
4287+
self.assertEqual(self.signature(C),
4288+
((('a', ..., ..., "positional_or_keyword"),),
4289+
...))
4290+
4291+
with self.subTest('staticmethod'):
4292+
class C:
4293+
@staticmethod
4294+
@identity_wrapper
4295+
def __new__(cls, a):
4296+
return a
4297+
4298+
self.assertEqual(C(1), 1)
4299+
self.assertEqual(self.signature(C),
4300+
((('a', ..., ..., "positional_or_keyword"),),
4301+
...))
4302+
4303+
with self.subTest('MethodType'):
4304+
class A:
4305+
@identity_wrapper
4306+
def call(self, cls, a):
4307+
return a
4308+
class C:
4309+
__new__ = A().call
4310+
4311+
self.assertEqual(C(1), 1)
4312+
self.assertEqual(self.signature(C),
4313+
((('a', ..., ..., "positional_or_keyword"),),
4314+
...))
4315+
4316+
with self.subTest('partial'):
4317+
class C:
4318+
__new__ = functools.partial(identity_wrapper(lambda x, cls, a: (x, a)), 2)
4319+
4320+
self.assertEqual(C(1), (2, 1))
4321+
self.assertEqual(self.signature(C),
4322+
((('a', ..., ..., "positional_or_keyword"),),
4323+
...))
4324+
4325+
with self.subTest('partialmethod'):
4326+
class C:
4327+
__new__ = functools.partialmethod(identity_wrapper(lambda cls, x, a: (x, a)), 2)
4328+
4329+
self.assertEqual(C(1), (2, 1))
4330+
self.assertEqual(self.signature(C),
4331+
((('a', ..., ..., "positional_or_keyword"),),
4332+
...))
4333+
4334+
with self.subTest('descriptor'):
4335+
class C:
4336+
@custom_descriptor
4337+
@identity_wrapper
4338+
def __new__(cls, a):
4339+
return a
4340+
4341+
self.assertEqual(C(1), 1)
4342+
self.assertEqual(self.signature(C),
4343+
((('a', ..., ..., "positional_or_keyword"),),
4344+
...))
4345+
self.assertEqual(self.signature(C.__new__),
4346+
((('cls', ..., ..., "positional_or_keyword"),
4347+
('a', ..., ..., "positional_or_keyword")),
4348+
...))
41024349

4103-
self.assertEqual(self.signature(Bar), ((), ...))
4104-
self.assertEqual(self.signature(Bar.__new__), varargs_signature)
4105-
self.assertEqual(self.signature(Bar, follow_wrapped=False),
4106-
varargs_signature)
4107-
self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False),
4108-
varargs_signature)
4350+
self.assertEqual(self.signature(C, follow_wrapped=False),
4351+
varargs_signature)
4352+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4353+
varargs_signature)
41094354

41104355
def test_signature_on_class_with_init(self):
41114356
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)