Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1917,17 +1917,21 @@ def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chain
if meth is None:
return None

# NOTE: The meth may wraps a non-user-defined callable.
# In this case, we treat the meth as non-user-defined callable too.
# (e.g. cls.__new__ generated by @warnings.deprecated)
unwrapped_meth = None
if follow_wrapper_chains:
meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
unwrapped_meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__")
or _signature_is_builtin(m)))
if isinstance(meth, _NonUserDefinedCallables):

if (isinstance(meth, _NonUserDefinedCallables)
or isinstance(unwrapped_meth, _NonUserDefinedCallables)):
# Once '__signature__' will be added to 'C'-level
# callables, this check won't be necessary
return None
if method_name != '__new__':
meth = _descriptor_get(meth, cls)
if follow_wrapper_chains:
meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__"))
return meth


Expand Down
220 changes: 194 additions & 26 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,21 @@ def meth_self_o(self, object, /): pass
def meth_type_noargs(type, /): pass
def meth_type_o(type, object, /): pass

# Simple decorator for test cases that using decorated functions
def identity(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
return func(*args, **kwargs)
return wrapped

# Simple descriptor that decorates a function
class DescWithDeco:
def __init__(self, func):
self.func = identity(func)

def __get__(self, instance, owner):
return self.func.__get__(instance, owner)


class TestPredicates(IsTestBase):

Expand Down Expand Up @@ -4027,44 +4042,197 @@ def __init__(self, b):
('bar', 2, ..., "keyword_only")),
...))

def test_signature_on_class_with_decorated_init(self):
class C:
@identity
def __init__(self, b):
pass

C(1) # does not raise
self.assertEqual(self.signature(C),
((('b', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('classmethod'):
class C:
@classmethod
@identity
def __init__(cls, b):
pass

C(1) # does not raise
self.assertEqual(self.signature(C),
((('b', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('staticmethod'):
class C:
@staticmethod
@identity
def __init__(b):
pass

C(1) # does not raise
self.assertEqual(self.signature(C),
((('b', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('MethodType'):
class A:
@identity
def call(self, a):
pass

class C:
__init__ = A().call

C(1) # does not raise
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('partial'):
class C:
__init__ = functools.partial(identity(lambda x, a, b: None), 2)

C(1) # does not raise
self.assertEqual(self.signature(C),
((('b', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('partialmethod'):
class C:
@identity
def _init(self, x, a):
self.a = (x, a)
__init__ = functools.partialmethod(_init, 2)

self.assertEqual(C(1).a, (2, 1))
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('descriptor'):
class C:
__init__ = DescWithDeco(lambda self, a: None)

C(1) # does not raise
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

self.assertEqual(self.signature(C.__init__),
((('self', ..., ..., "positional_or_keyword"),
('a', ..., ..., "positional_or_keyword")),
...))

varargs_signature = (
(('args', ..., ..., 'var_positional'),
('kwargs', ..., ..., 'var_keyword')),
...,
)
self.assertEqual(self.signature(C, follow_wrapped=False),
varargs_signature)
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
varargs_signature)


def test_signature_on_class_with_decorated_new(self):
def identity(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
return func(*args, **kwargs)
return wrapped

class Foo:
@identity
def __new__(cls, a, b):
pass
with self.subTest('FunctionType'):
class C:
@identity
def __new__(cls, a):
return a

self.assertEqual(self.signature(Foo),
((('a', ..., ..., "positional_or_keyword"),
('b', ..., ..., "positional_or_keyword")),
...))
self.assertEqual(C(1), 1)
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

self.assertEqual(self.signature(Foo.__new__),
((('cls', ..., ..., "positional_or_keyword"),
('a', ..., ..., "positional_or_keyword"),
('b', ..., ..., "positional_or_keyword")),
...))
with self.subTest('classmethod'):
class C:
@classmethod
@identity
def __new__(cls, cls2, a):
return a

class Bar:
__new__ = identity(object.__new__)
self.assertEqual(C(1), 1)
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

varargs_signature = (
(('args', ..., ..., 'var_positional'),
('kwargs', ..., ..., 'var_keyword')),
...,
)
with self.subTest('staticmethod'):
class C:
@staticmethod
@identity
def __new__(cls, a):
return a

self.assertEqual(C(1), 1)
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

self.assertEqual(self.signature(Bar), ((), ...))
self.assertEqual(self.signature(Bar.__new__), varargs_signature)
self.assertEqual(self.signature(Bar, follow_wrapped=False),
varargs_signature)
self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False),
varargs_signature)
with self.subTest('MethodType'):
class A:
@identity
def call(self, cls, a):
return a
class C:
__new__ = A().call

self.assertEqual(C(1), 1)
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('partial'):
class C:
__new__ = functools.partial(identity(lambda x, cls, a: (x, a)), 2)

self.assertEqual(C(1), (2, 1))
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('partialmethod'):
class C:
__new__ = functools.partialmethod(identity(lambda cls, x, a: (x, a)), 2)

self.assertEqual(C(1), (2, 1))
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

with self.subTest('descriptor'):
class C:
__new__ = DescWithDeco(lambda cls, a: a)

self.assertEqual(C(1), 1)
self.assertEqual(self.signature(C),
((('a', ..., ..., "positional_or_keyword"),),
...))

self.assertEqual(self.signature(C.__new__),
((('cls', ..., ..., "positional_or_keyword"),
('a', ..., ..., "positional_or_keyword")),
...))

varargs_signature = (
(('args', ..., ..., 'var_positional'),
('kwargs', ..., ..., 'var_keyword')),
...,
)
self.assertEqual(self.signature(C, follow_wrapped=False),
varargs_signature)
self.assertEqual(self.signature(C.__new__, follow_wrapped=False),
varargs_signature)

def test_signature_on_class_with_init(self):
class C:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`inspect.signature` now correctly handles classes that use a descriptor
on a wrapped :meth:`!__init__` or :meth:`!__new__` method.
Contributed by Yongyu Yan.
Loading