Skip to content

Commit b93b688

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 502ca0d commit b93b688

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
@@ -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: 276 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,266 @@ 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, b: None), 2)
4309+
4310+
C(1) # does not raise
4311+
self.assertEqual(self.signature(C),
4312+
((('b', ..., ..., "positional_or_keyword"),),
4313+
...))
4314+
4315+
with self.subTest('partialmethod'):
4316+
class C:
4317+
@identity_wrapper
4318+
def _init(self, x, a):
4319+
self.a = (x, a)
4320+
__init__ = functools.partialmethod(_init, 2)
4321+
4322+
self.assertEqual(C(1).a, (2, 1))
4323+
self.assertEqual(self.signature(C),
4324+
((('a', ..., ..., "positional_or_keyword"),),
4325+
...))
4326+
4327+
with self.subTest('descriptor'):
4328+
class C:
4329+
@custom_descriptor
4330+
@identity_wrapper
4331+
def __init__(self, a):
4332+
pass
4333+
4334+
C(1) # does not raise
4335+
self.assertEqual(self.signature(C),
4336+
((('a', ..., ..., "positional_or_keyword"),),
4337+
...))
4338+
self.assertEqual(self.signature(C.__init__),
4339+
((('self', ..., ..., "positional_or_keyword"),
4340+
('a', ..., ..., "positional_or_keyword")),
4341+
...))
4342+
4343+
self.assertEqual(self.signature(C, follow_wrapped=False),
4344+
varargs_signature)
4345+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4346+
varargs_signature)
4347+
4348+
def test_signature_on_class_with_wrapped_new(self):
4349+
with self.subTest('FunctionType'):
4350+
class C:
4351+
@identity_wrapper
4352+
def __new__(cls, a):
4353+
return a
4354+
4355+
self.assertEqual(C(1), 1)
4356+
self.assertEqual(self.signature(C),
4357+
((('a', ..., ..., "positional_or_keyword"),),
4358+
...))
4359+
4360+
with self.subTest('classmethod'):
4361+
class C:
4362+
@classmethod
4363+
@identity_wrapper
4364+
def __new__(cls, cls2, a):
4365+
return a
4366+
4367+
self.assertEqual(C(1), 1)
4368+
self.assertEqual(self.signature(C),
4369+
((('a', ..., ..., "positional_or_keyword"),),
4370+
...))
4371+
4372+
with self.subTest('staticmethod'):
4373+
class C:
4374+
@staticmethod
4375+
@identity_wrapper
4376+
def __new__(cls, a):
4377+
return a
4378+
4379+
self.assertEqual(C(1), 1)
4380+
self.assertEqual(self.signature(C),
4381+
((('a', ..., ..., "positional_or_keyword"),),
4382+
...))
4383+
4384+
with self.subTest('MethodType'):
4385+
class A:
4386+
@identity_wrapper
4387+
def call(self, cls, a):
4388+
return a
4389+
class C:
4390+
__new__ = A().call
4391+
4392+
self.assertEqual(C(1), 1)
4393+
self.assertEqual(self.signature(C),
4394+
((('a', ..., ..., "positional_or_keyword"),),
4395+
...))
4396+
4397+
with self.subTest('partial'):
4398+
class C:
4399+
__new__ = functools.partial(identity_wrapper(lambda x, cls, a: (x, a)), 2)
4400+
4401+
self.assertEqual(C(1), (2, 1))
4402+
self.assertEqual(self.signature(C),
4403+
((('a', ..., ..., "positional_or_keyword"),),
4404+
...))
4405+
4406+
with self.subTest('partialmethod'):
4407+
class C:
4408+
__new__ = functools.partialmethod(identity_wrapper(lambda cls, x, a: (x, a)), 2)
4409+
4410+
self.assertEqual(C(1), (2, 1))
4411+
self.assertEqual(self.signature(C),
4412+
((('a', ..., ..., "positional_or_keyword"),),
4413+
...))
4414+
4415+
with self.subTest('descriptor'):
4416+
class C:
4417+
@custom_descriptor
4418+
@identity_wrapper
4419+
def __new__(cls, a):
4420+
return a
4421+
4422+
self.assertEqual(C(1), 1)
4423+
self.assertEqual(self.signature(C),
4424+
((('a', ..., ..., "positional_or_keyword"),),
4425+
...))
4426+
self.assertEqual(self.signature(C.__new__),
4427+
((('cls', ..., ..., "positional_or_keyword"),
4428+
('a', ..., ..., "positional_or_keyword")),
4429+
...))
41834430

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)
4431+
self.assertEqual(self.signature(C, follow_wrapped=False),
4432+
varargs_signature)
4433+
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
4434+
varargs_signature)
41904435

41914436
def test_signature_on_class_with_init(self):
41924437
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)