Skip to content

Commit 1a85ae7

Browse files
yanyongyumiss-islington
authored andcommitted
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 0e46c04 commit 1a85ae7

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
@@ -1913,17 +1913,21 @@ def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chain
19131913
if meth is None:
19141914
return None
19151915

1916+
# NOTE: The meth may wraps a non-user-defined callable.
1917+
# In this case, we treat the meth as non-user-defined callable too.
1918+
# (e.g. cls.__new__ generated by @warnings.deprecated)
1919+
unwrapped_meth = None
19161920
if follow_wrapper_chains:
1917-
meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
1921+
unwrapped_meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
19181922
or _signature_is_builtin(m)))
1919-
if isinstance(meth, _NonUserDefinedCallables):
1923+
1924+
if (isinstance(meth, _NonUserDefinedCallables)
1925+
or isinstance(unwrapped_meth, _NonUserDefinedCallables)):
19201926
# Once '__signature__' will be added to 'C'-level
19211927
# callables, this check won't be necessary
19221928
return None
19231929
if method_name != '__new__':
19241930
meth = _descriptor_get(meth, cls)
1925-
if follow_wrapper_chains:
1926-
meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__"))
19271931
return meth
19281932

19291933

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

@@ -4021,44 +4044,266 @@ def __init__(self, b):
40214044
('bar', 2, ..., "keyword_only")),
40224045
...))
40234046

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):
4047+
def test_signature_on_class_with_wrapped_metaclass_call(self):
4048+
class CM(type):
4049+
@identity_wrapper
4050+
def __call__(cls, a):
4051+
pass
4052+
class C(metaclass=CM):
4053+
def __init__(self, b):
40344054
pass
40354055

4036-
self.assertEqual(self.signature(Foo),
4037-
((('a', ..., ..., "positional_or_keyword"),
4038-
('b', ..., ..., "positional_or_keyword")),
4056+
self.assertEqual(self.signature(C),
4057+
((('a', ..., ..., "positional_or_keyword"),),
40394058
...))
40404059

4041-
self.assertEqual(self.signature(Foo.__new__),
4042-
((('cls', ..., ..., "positional_or_keyword"),
4043-
('a', ..., ..., "positional_or_keyword"),
4044-
('b', ..., ..., "positional_or_keyword")),
4045-
...))
4060+
with self.subTest('classmethod'):
4061+
class CM(type):
4062+
@classmethod
4063+
@identity_wrapper
4064+
def __call__(cls, a):
4065+
return a
4066+
class C(metaclass=CM):
4067+
def __init__(self, b):
4068+
pass
40464069

4047-
class Bar:
4048-
__new__ = identity(object.__new__)
4070+
self.assertEqual(C(1), 1)
4071+
self.assertEqual(self.signature(C),
4072+
((('a', ..., ..., "positional_or_keyword"),),
4073+
...))
40494074

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

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)
4303+
self.assertEqual(self.signature(C, follow_wrapped=False),
4304+
varargs_signature)
4305+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4306+
varargs_signature)
40624307

40634308
def test_signature_on_class_with_init(self):
40644309
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)