Skip to content

Commit 1a423e1

Browse files
yanyongyuserhiy-storchaka
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 502ca0d commit 1a423e1

File tree

3 files changed

+289
-35
lines changed

3 files changed

+289
-35
lines changed

Lib/inspect.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,17 +2015,21 @@ def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chain
20152015
if meth is None:
20162016
return None
20172017

2018+
# NOTE: The meth may wraps a non-user-defined callable.
2019+
# In this case, we treat the meth as non-user-defined callable too.
2020+
# (e.g. cls.__new__ generated by @warnings.deprecated)
2021+
unwrapped_meth = None
20182022
if follow_wrapper_chains:
2019-
meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
2023+
unwrapped_meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
20202024
or _signature_is_builtin(m)))
2021-
if isinstance(meth, _NonUserDefinedCallables):
2025+
2026+
if (isinstance(meth, _NonUserDefinedCallables)
2027+
or isinstance(unwrapped_meth, _NonUserDefinedCallables)):
20222028
# Once '__signature__' will be added to 'C'-level
20232029
# callables, this check won't be necessary
20242030
return None
20252031
if method_name != '__new__':
20262032
meth = _descriptor_get(meth, cls)
2027-
if follow_wrapper_chains:
2028-
meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__"))
20292033
return meth
20302034

20312035

Lib/test/test_inspect/test_inspect.py

Lines changed: 278 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,29 @@ def meth_self_o(self, object, /): pass
151151
def meth_type_noargs(type, /): pass
152152
def meth_type_o(type, object, /): pass
153153

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

155178
class TestPredicates(IsTestBase):
156179

@@ -4149,44 +4172,268 @@ def __init__(self, b):
41494172
('bar', 2, ..., "keyword_only")),
41504173
...))
41514174

4152-
def test_signature_on_class_with_decorated_new(self):
4153-
def identity(func):
4154-
@functools.wraps(func)
4155-
def wrapped(*args, **kwargs):
4156-
return func(*args, **kwargs)
4157-
return wrapped
4158-
4159-
class Foo:
4160-
@identity
4161-
def __new__(cls, a, b):
4175+
def test_signature_on_class_with_wrapped_metaclass_call(self):
4176+
class CM(type):
4177+
@identity_wrapper
4178+
def __call__(cls, a):
4179+
pass
4180+
class C(metaclass=CM):
4181+
def __init__(self, b):
41624182
pass
41634183

4164-
self.assertEqual(self.signature(Foo),
4165-
((('a', ..., ..., "positional_or_keyword"),
4166-
('b', ..., ..., "positional_or_keyword")),
4184+
self.assertEqual(self.signature(C),
4185+
((('a', ..., ..., "positional_or_keyword"),),
41674186
...))
41684187

4169-
self.assertEqual(self.signature(Foo.__new__),
4170-
((('cls', ..., ..., "positional_or_keyword"),
4171-
('a', ..., ..., "positional_or_keyword"),
4172-
('b', ..., ..., "positional_or_keyword")),
4173-
...))
4188+
with self.subTest('classmethod'):
4189+
class CM(type):
4190+
@classmethod
4191+
@identity_wrapper
4192+
def __call__(cls, a):
4193+
return a
4194+
class C(metaclass=CM):
4195+
def __init__(self, b):
4196+
pass
41744197

4175-
class Bar:
4176-
__new__ = identity(object.__new__)
4198+
self.assertEqual(C(1), 1)
4199+
self.assertEqual(self.signature(C),
4200+
((('a', ..., ..., "positional_or_keyword"),),
4201+
...))
41774202

4178-
varargs_signature = (
4179-
(('args', ..., ..., 'var_positional'),
4180-
('kwargs', ..., ..., 'var_keyword')),
4181-
...,
4182-
)
4203+
with self.subTest('staticmethod'):
4204+
class CM(type):
4205+
@staticmethod
4206+
@identity_wrapper
4207+
def __call__(a):
4208+
return a
4209+
class C(metaclass=CM):
4210+
def __init__(self, b):
4211+
pass
4212+
4213+
self.assertEqual(C(1), 1)
4214+
self.assertEqual(self.signature(C),
4215+
((('a', ..., ..., "positional_or_keyword"),),
4216+
...))
4217+
4218+
with self.subTest('MethodType'):
4219+
class A:
4220+
@identity_wrapper
4221+
def call(self, a):
4222+
return a
4223+
class CM(type):
4224+
__call__ = A().call
4225+
class C(metaclass=CM):
4226+
def __init__(self, b):
4227+
pass
4228+
4229+
self.assertEqual(C(1), 1)
4230+
self.assertEqual(self.signature(C),
4231+
((('a', ..., ..., "positional_or_keyword"),),
4232+
...))
4233+
4234+
with self.subTest('descriptor'):
4235+
class CM(type):
4236+
@custom_descriptor
4237+
@identity_wrapper
4238+
def __call__(self, a):
4239+
return a
4240+
class C(metaclass=CM):
4241+
def __init__(self, b):
4242+
pass
4243+
4244+
self.assertEqual(C(1), 1)
4245+
self.assertEqual(self.signature(C),
4246+
((('a', ..., ..., "positional_or_keyword"),),
4247+
...))
4248+
self.assertEqual(self.signature(C.__call__),
4249+
((('a', ..., ..., "positional_or_keyword"),),
4250+
...))
4251+
4252+
self.assertEqual(self.signature(C, follow_wrapped=False),
4253+
varargs_signature)
4254+
self.assertEqual(self.signature(C.__call__, follow_wrapped=False),
4255+
varargs_signature)
4256+
4257+
def test_signature_on_class_with_wrapped_init(self):
4258+
class C:
4259+
@identity_wrapper
4260+
def __init__(self, b):
4261+
pass
4262+
4263+
C(1) # does not raise
4264+
self.assertEqual(self.signature(C),
4265+
((('b', ..., ..., "positional_or_keyword"),),
4266+
...))
4267+
4268+
with self.subTest('classmethod'):
4269+
class C:
4270+
@classmethod
4271+
@identity_wrapper
4272+
def __init__(cls, b):
4273+
pass
4274+
4275+
C(1) # does not raise
4276+
self.assertEqual(self.signature(C),
4277+
((('b', ..., ..., "positional_or_keyword"),),
4278+
...))
4279+
4280+
with self.subTest('staticmethod'):
4281+
class C:
4282+
@staticmethod
4283+
@identity_wrapper
4284+
def __init__(b):
4285+
pass
4286+
4287+
C(1) # does not raise
4288+
self.assertEqual(self.signature(C),
4289+
((('b', ..., ..., "positional_or_keyword"),),
4290+
...))
4291+
4292+
with self.subTest('MethodType'):
4293+
class A:
4294+
@identity_wrapper
4295+
def call(self, a):
4296+
pass
4297+
4298+
class C:
4299+
__init__ = A().call
4300+
4301+
C(1) # does not raise
4302+
self.assertEqual(self.signature(C),
4303+
((('a', ..., ..., "positional_or_keyword"),),
4304+
...))
4305+
4306+
with self.subTest('partial'):
4307+
class C:
4308+
__init__ = functools.partial(identity_wrapper(lambda x, a: None), 2)
4309+
4310+
with self.assertWarns(FutureWarning):
4311+
C(1) # does not raise
4312+
with self.assertWarns(FutureWarning):
4313+
self.assertEqual(self.signature(C),
4314+
((('a', ..., ..., "positional_or_keyword"),),
4315+
...))
4316+
4317+
with self.subTest('partialmethod'):
4318+
class C:
4319+
@identity_wrapper
4320+
def _init(self, x, a):
4321+
self.a = (x, a)
4322+
__init__ = functools.partialmethod(_init, 2)
4323+
4324+
self.assertEqual(C(1).a, (2, 1))
4325+
self.assertEqual(self.signature(C),
4326+
((('a', ..., ..., "positional_or_keyword"),),
4327+
...))
4328+
4329+
with self.subTest('descriptor'):
4330+
class C:
4331+
@custom_descriptor
4332+
@identity_wrapper
4333+
def __init__(self, a):
4334+
pass
4335+
4336+
C(1) # does not raise
4337+
self.assertEqual(self.signature(C),
4338+
((('a', ..., ..., "positional_or_keyword"),),
4339+
...))
4340+
self.assertEqual(self.signature(C.__init__),
4341+
((('self', ..., ..., "positional_or_keyword"),
4342+
('a', ..., ..., "positional_or_keyword")),
4343+
...))
4344+
4345+
self.assertEqual(self.signature(C, follow_wrapped=False),
4346+
varargs_signature)
4347+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4348+
varargs_signature)
4349+
4350+
def test_signature_on_class_with_wrapped_new(self):
4351+
with self.subTest('FunctionType'):
4352+
class C:
4353+
@identity_wrapper
4354+
def __new__(cls, a):
4355+
return a
4356+
4357+
self.assertEqual(C(1), 1)
4358+
self.assertEqual(self.signature(C),
4359+
((('a', ..., ..., "positional_or_keyword"),),
4360+
...))
4361+
4362+
with self.subTest('classmethod'):
4363+
class C:
4364+
@classmethod
4365+
@identity_wrapper
4366+
def __new__(cls, cls2, a):
4367+
return a
4368+
4369+
self.assertEqual(C(1), 1)
4370+
self.assertEqual(self.signature(C),
4371+
((('a', ..., ..., "positional_or_keyword"),),
4372+
...))
4373+
4374+
with self.subTest('staticmethod'):
4375+
class C:
4376+
@staticmethod
4377+
@identity_wrapper
4378+
def __new__(cls, a):
4379+
return a
4380+
4381+
self.assertEqual(C(1), 1)
4382+
self.assertEqual(self.signature(C),
4383+
((('a', ..., ..., "positional_or_keyword"),),
4384+
...))
4385+
4386+
with self.subTest('MethodType'):
4387+
class A:
4388+
@identity_wrapper
4389+
def call(self, cls, a):
4390+
return a
4391+
class C:
4392+
__new__ = A().call
4393+
4394+
self.assertEqual(C(1), 1)
4395+
self.assertEqual(self.signature(C),
4396+
((('a', ..., ..., "positional_or_keyword"),),
4397+
...))
4398+
4399+
with self.subTest('partial'):
4400+
class C:
4401+
__new__ = functools.partial(identity_wrapper(lambda x, cls, a: (x, a)), 2)
4402+
4403+
self.assertEqual(C(1), (2, 1))
4404+
self.assertEqual(self.signature(C),
4405+
((('a', ..., ..., "positional_or_keyword"),),
4406+
...))
4407+
4408+
with self.subTest('partialmethod'):
4409+
class C:
4410+
__new__ = functools.partialmethod(identity_wrapper(lambda cls, x, a: (x, a)), 2)
4411+
4412+
self.assertEqual(C(1), (2, 1))
4413+
self.assertEqual(self.signature(C),
4414+
((('a', ..., ..., "positional_or_keyword"),),
4415+
...))
4416+
4417+
with self.subTest('descriptor'):
4418+
class C:
4419+
@custom_descriptor
4420+
@identity_wrapper
4421+
def __new__(cls, a):
4422+
return a
4423+
4424+
self.assertEqual(C(1), 1)
4425+
self.assertEqual(self.signature(C),
4426+
((('a', ..., ..., "positional_or_keyword"),),
4427+
...))
4428+
self.assertEqual(self.signature(C.__new__),
4429+
((('cls', ..., ..., "positional_or_keyword"),
4430+
('a', ..., ..., "positional_or_keyword")),
4431+
...))
41834432

4184-
self.assertEqual(self.signature(Bar), ((), ...))
4185-
self.assertEqual(self.signature(Bar.__new__), varargs_signature)
4186-
self.assertEqual(self.signature(Bar, follow_wrapped=False),
4187-
varargs_signature)
4188-
self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False),
4189-
varargs_signature)
4433+
self.assertEqual(self.signature(C, follow_wrapped=False),
4434+
varargs_signature)
4435+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4436+
varargs_signature)
41904437

41914438
def test_signature_on_class_with_init(self):
41924439
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)