summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2024-03-01 11:32:16 (GMT)
committerGitHub <noreply@github.com>2024-03-01 11:32:16 (GMT)
commit59167c962efcae72e8d88aa4b33062ed3de4f120 (patch)
tree5d7802a8e021d025ef69181ec38dcc2a6b1f06e4
parent8ab6c2775c4b2566477589cfc50fb64f020dc4de (diff)
downloadcpython-59167c962efcae72e8d88aa4b33062ed3de4f120.zip
cpython-59167c962efcae72e8d88aa4b33062ed3de4f120.tar.gz
cpython-59167c962efcae72e8d88aa4b33062ed3de4f120.tar.bz2
gh-101293: Fix support of custom callables and types in inspect.Signature.from_callable() (GH-115530)
Support callables with the __call__() method and types with __new__() and __init__() methods set to class methods, static methods, bound methods, partial functions, and other types of methods and descriptors. Add tests for numerous types of callables and descriptors.
-rw-r--r--Lib/inspect.py161
-rw-r--r--Lib/test/test_inspect/test_inspect.py362
-rw-r--r--Misc/NEWS.d/next/Library/2024-02-15-19-11-49.gh-issue-101293.898b8l.rst4
3 files changed, 438 insertions, 89 deletions
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 9191d47..8a2b2c9 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -2039,15 +2039,17 @@ def _signature_get_user_defined_method(cls, method_name):
named ``method_name`` and returns it only if it is a
pure python function.
"""
- try:
- meth = getattr(cls, method_name)
- except AttributeError:
- return
+ if method_name == '__new__':
+ meth = getattr(cls, method_name, None)
else:
- if not isinstance(meth, _NonUserDefinedCallables):
- # Once '__signature__' will be added to 'C'-level
- # callables, this check won't be necessary
- return meth
+ meth = getattr_static(cls, method_name, None)
+ if meth is None or isinstance(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)
+ return meth
def _signature_get_partial(wrapped_sig, partial, extra_args=()):
@@ -2492,6 +2494,15 @@ def _signature_from_function(cls, func, skip_bound_arg=True,
__validate_parameters__=is_duck_function)
+def _descriptor_get(descriptor, obj):
+ if isclass(descriptor):
+ return descriptor
+ get = getattr(type(descriptor), '__get__', _sentinel)
+ if get is _sentinel:
+ return descriptor
+ return get(descriptor, obj, type(obj))
+
+
def _signature_from_callable(obj, *,
follow_wrapper_chains=True,
skip_bound_arg=True,
@@ -2600,7 +2611,6 @@ def _signature_from_callable(obj, *,
wrapped_sig = _get_signature_of(obj.func)
return _signature_get_partial(wrapped_sig, obj)
- sig = None
if isinstance(obj, type):
# obj is a class or a metaclass
@@ -2608,88 +2618,65 @@ def _signature_from_callable(obj, *,
# in its metaclass
call = _signature_get_user_defined_method(type(obj), '__call__')
if call is not None:
- sig = _get_signature_of(call)
- else:
- factory_method = None
- new = _signature_get_user_defined_method(obj, '__new__')
- init = _signature_get_user_defined_method(obj, '__init__')
-
- # Go through the MRO and see if any class has user-defined
- # pure Python __new__ or __init__ method
- for base in obj.__mro__:
- # Now we check if the 'obj' class has an own '__new__' method
- if new is not None and '__new__' in base.__dict__:
- factory_method = new
- break
- # or an own '__init__' method
- elif init is not None and '__init__' in base.__dict__:
- factory_method = init
- break
+ return _get_signature_of(call)
- if factory_method is not None:
- sig = _get_signature_of(factory_method)
-
- if sig is None:
- # At this point we know, that `obj` is a class, with no user-
- # defined '__init__', '__new__', or class-level '__call__'
-
- for base in obj.__mro__[:-1]:
- # Since '__text_signature__' is implemented as a
- # descriptor that extracts text signature from the
- # class docstring, if 'obj' is derived from a builtin
- # class, its own '__text_signature__' may be 'None'.
- # Therefore, we go through the MRO (except the last
- # class in there, which is 'object') to find the first
- # class with non-empty text signature.
- try:
- text_sig = base.__text_signature__
- except AttributeError:
- pass
- else:
- if text_sig:
- # If 'base' class has a __text_signature__ attribute:
- # return a signature based on it
- return _signature_fromstr(sigcls, base, text_sig)
-
- # No '__text_signature__' was found for the 'obj' class.
- # Last option is to check if its '__init__' is
- # object.__init__ or type.__init__.
- if type not in obj.__mro__:
- # We have a class (not metaclass), but no user-defined
- # __init__ or __new__ for it
- if (obj.__init__ is object.__init__ and
- obj.__new__ is object.__new__):
- # Return a signature of 'object' builtin.
- return sigcls.from_callable(object)
- else:
- raise ValueError(
- 'no signature found for builtin type {!r}'.format(obj))
+ new = _signature_get_user_defined_method(obj, '__new__')
+ init = _signature_get_user_defined_method(obj, '__init__')
- elif not isinstance(obj, _NonUserDefinedCallables):
- # An object with __call__
- # We also check that the 'obj' is not an instance of
- # types.WrapperDescriptorType or types.MethodWrapperType to avoid
- # infinite recursion (and even potential segfault)
- call = _signature_get_user_defined_method(type(obj), '__call__')
- if call is not None:
+ # Go through the MRO and see if any class has user-defined
+ # pure Python __new__ or __init__ method
+ for base in obj.__mro__:
+ # Now we check if the 'obj' class has an own '__new__' method
+ if new is not None and '__new__' in base.__dict__:
+ sig = _get_signature_of(new)
+ if skip_bound_arg:
+ sig = _signature_bound_method(sig)
+ return sig
+ # or an own '__init__' method
+ elif init is not None and '__init__' in base.__dict__:
+ return _get_signature_of(init)
+
+ # At this point we know, that `obj` is a class, with no user-
+ # defined '__init__', '__new__', or class-level '__call__'
+
+ for base in obj.__mro__[:-1]:
+ # Since '__text_signature__' is implemented as a
+ # descriptor that extracts text signature from the
+ # class docstring, if 'obj' is derived from a builtin
+ # class, its own '__text_signature__' may be 'None'.
+ # Therefore, we go through the MRO (except the last
+ # class in there, which is 'object') to find the first
+ # class with non-empty text signature.
try:
- sig = _get_signature_of(call)
- except ValueError as ex:
- msg = 'no signature found for {!r}'.format(obj)
- raise ValueError(msg) from ex
-
- if sig is not None:
- # For classes and objects we skip the first parameter of their
- # __call__, __new__, or __init__ methods
- if skip_bound_arg:
- return _signature_bound_method(sig)
- else:
- return sig
+ text_sig = base.__text_signature__
+ except AttributeError:
+ pass
+ else:
+ if text_sig:
+ # If 'base' class has a __text_signature__ attribute:
+ # return a signature based on it
+ return _signature_fromstr(sigcls, base, text_sig)
+
+ # No '__text_signature__' was found for the 'obj' class.
+ # Last option is to check if its '__init__' is
+ # object.__init__ or type.__init__.
+ if type not in obj.__mro__:
+ # We have a class (not metaclass), but no user-defined
+ # __init__ or __new__ for it
+ if (obj.__init__ is object.__init__ and
+ obj.__new__ is object.__new__):
+ # Return a signature of 'object' builtin.
+ return sigcls.from_callable(object)
+ else:
+ raise ValueError(
+ 'no signature found for builtin type {!r}'.format(obj))
- if isinstance(obj, types.BuiltinFunctionType):
- # Raise a nicer error message for builtins
- msg = 'no signature found for builtin function {!r}'.format(obj)
- raise ValueError(msg)
+ else:
+ # An object with __call__
+ call = getattr_static(type(obj), '__call__', None)
+ if call is not None:
+ call = _descriptor_get(call, obj)
+ return _get_signature_of(call)
raise ValueError('callable {!r} is not supported by signature'.format(obj))
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index 9dc3785..52cf68b 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -2928,9 +2928,12 @@ class TestSignatureObject(unittest.TestCase):
# This doesn't work now.
# (We don't have a valid signature for "type" in 3.4)
+ class ThisWorksNow:
+ __call__ = type
+ # TODO: Support type.
+ self.assertEqual(ThisWorksNow()(1), int)
+ self.assertEqual(ThisWorksNow()('A', (), {}).__name__, 'A')
with self.assertRaisesRegex(ValueError, "no signature found"):
- class ThisWorksNow:
- __call__ = type
test_callable(ThisWorksNow())
# Regression test for issue #20786
@@ -3521,6 +3524,98 @@ class TestSignatureObject(unittest.TestCase):
((('a', ..., ..., "positional_or_keyword"),),
...))
+ with self.subTest('classmethod'):
+ class CM(type):
+ @classmethod
+ def __call__(cls, a):
+ return a
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('staticmethod'):
+ class CM(type):
+ @staticmethod
+ def __call__(a):
+ return a
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('MethodType'):
+ class A:
+ def call(self, a):
+ return a
+ class CM(type):
+ __call__ = A().call
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('partial'):
+ class CM(type):
+ __call__ = functools.partial(lambda x, a: (x, a), 2)
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(1), (2, 1))
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('partialmethod'):
+ class CM(type):
+ __call__ = functools.partialmethod(lambda self, x, a: (x, a), 2)
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(1), (2, 1))
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('BuiltinMethodType'):
+ class CM(type):
+ __call__ = ':'.join
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(['a', 'bc']), 'a:bc')
+ # BUG: Returns '<Signature (b)>'
+ with self.assertRaises(AssertionError):
+ self.assertEqual(self.signature(C), self.signature(''.join))
+
+ with self.subTest('MethodWrapperType'):
+ class CM(type):
+ __call__ = (2).__pow__
+ class C(metaclass=CM):
+ def __init__(self, b):
+ pass
+
+ self.assertEqual(C(3), 8)
+ self.assertEqual(C(3, 7), 1)
+ # BUG: Returns '<Signature (b)>'
+ with self.assertRaises(AssertionError):
+ self.assertEqual(self.signature(C), self.signature((0).__pow__))
+
class CM(type):
def __new__(mcls, name, bases, dct, *, foo=1):
return super().__new__(mcls, name, bases, dct)
@@ -3582,6 +3677,169 @@ class TestSignatureObject(unittest.TestCase):
('bar', 2, ..., "keyword_only")),
...))
+ def test_signature_on_class_with_init(self):
+ class C:
+ 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
+ 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
+ def __init__(b):
+ pass
+
+ C(1) # does not raise
+ self.assertEqual(self.signature(C),
+ ((('b', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('MethodType'):
+ class A:
+ 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(lambda x, a: None, 2)
+
+ C(1) # does not raise
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('partialmethod'):
+ class C:
+ 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"),),
+ ...))
+
+ def test_signature_on_class_with_new(self):
+ with self.subTest('FunctionType'):
+ class C:
+ def __new__(cls, a):
+ return a
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('classmethod'):
+ class C:
+ @classmethod
+ def __new__(cls, cls2, a):
+ return a
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('staticmethod'):
+ class C:
+ @staticmethod
+ def __new__(cls, a):
+ return a
+
+ self.assertEqual(C(1), 1)
+ self.assertEqual(self.signature(C),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('MethodType'):
+ class A:
+ 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(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(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('BuiltinMethodType'):
+ class C:
+ __new__ = str.__subclasscheck__
+
+ self.assertEqual(C(), False)
+ # TODO: Support BuiltinMethodType
+ # self.assertEqual(self.signature(C), ((), ...))
+ self.assertRaises(ValueError, self.signature, C)
+
+ with self.subTest('MethodWrapperType'):
+ class C:
+ __new__ = type.__or__.__get__(int, type)
+
+ self.assertEqual(C(), C | int)
+ # TODO: Support MethodWrapperType
+ # self.assertEqual(self.signature(C), ((), ...))
+ self.assertRaises(ValueError, self.signature, C)
+
+ # TODO: Test ClassMethodDescriptorType
+
+ with self.subTest('MethodDescriptorType'):
+ class C:
+ __new__ = type.__dict__['__subclasscheck__']
+
+ self.assertEqual(C(C), True)
+ self.assertEqual(self.signature(C), self.signature(C.__subclasscheck__))
+
+ with self.subTest('WrapperDescriptorType'):
+ class C:
+ __new__ = type.__or__
+
+ self.assertEqual(C(int), C | int)
+ # TODO: Support WrapperDescriptorType
+ # self.assertEqual(self.signature(C), self.signature(C.__or__))
+ self.assertRaises(ValueError, self.signature, C)
+
def test_signature_on_subclass(self):
class A:
def __new__(cls, a=1, *args, **kwargs):
@@ -3635,8 +3893,11 @@ class TestSignatureObject(unittest.TestCase):
# Test meta-classes without user-defined __init__ or __new__
class C(type): pass
class D(C): pass
+ self.assertEqual(C('A', (), {}).__name__, 'A')
+ # TODO: Support type.
with self.assertRaisesRegex(ValueError, "callable.*is not supported"):
self.assertEqual(inspect.signature(C), None)
+ self.assertEqual(D('A', (), {}).__name__, 'A')
with self.assertRaisesRegex(ValueError, "callable.*is not supported"):
self.assertEqual(inspect.signature(D), None)
@@ -3686,6 +3947,103 @@ class TestSignatureObject(unittest.TestCase):
((('a', ..., ..., "positional_or_keyword"),),
...))
+ with self.subTest('classmethod'):
+ class C:
+ @classmethod
+ def __call__(cls, a):
+ pass
+
+ self.assertEqual(self.signature(C()),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('staticmethod'):
+ class C:
+ @staticmethod
+ def __call__(a):
+ pass
+
+ self.assertEqual(self.signature(C()),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('MethodType'):
+ class A:
+ def call(self, a):
+ return a
+ class C:
+ __call__ = A().call
+
+ self.assertEqual(C()(1), 1)
+ self.assertEqual(self.signature(C()),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('partial'):
+ class C:
+ __call__ = functools.partial(lambda x, 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:
+ __call__ = functools.partialmethod(lambda self, x, a: (x, a), 2)
+
+ self.assertEqual(C()(1), (2, 1))
+ self.assertEqual(self.signature(C()),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
+ with self.subTest('BuiltinMethodType'):
+ class C:
+ __call__ = ':'.join
+
+ self.assertEqual(C()(['a', 'bc']), 'a:bc')
+ self.assertEqual(self.signature(C()), self.signature(''.join))
+
+ with self.subTest('MethodWrapperType'):
+ class C:
+ __call__ = (2).__pow__
+
+ self.assertEqual(C()(3), 8)
+ self.assertEqual(self.signature(C()), self.signature((0).__pow__))
+
+ with self.subTest('ClassMethodDescriptorType'):
+ class C(dict):
+ __call__ = dict.__dict__['fromkeys']
+
+ res = C()([1, 2], 3)
+ self.assertEqual(res, {1: 3, 2: 3})
+ self.assertEqual(type(res), C)
+ self.assertEqual(self.signature(C()), self.signature(dict.fromkeys))
+
+ with self.subTest('MethodDescriptorType'):
+ class C(str):
+ __call__ = str.join
+
+ self.assertEqual(C(':')(['a', 'bc']), 'a:bc')
+ self.assertEqual(self.signature(C()), self.signature(''.join))
+
+ with self.subTest('WrapperDescriptorType'):
+ class C(int):
+ __call__ = int.__pow__
+
+ self.assertEqual(C(2)(3), 8)
+ self.assertEqual(self.signature(C()), self.signature((0).__pow__))
+
+ with self.subTest('MemberDescriptorType'):
+ class C:
+ __slots__ = '__call__'
+ c = C()
+ c.__call__ = lambda a: a
+ self.assertEqual(c(1), 1)
+ self.assertEqual(self.signature(c),
+ ((('a', ..., ..., "positional_or_keyword"),),
+ ...))
+
def test_signature_on_wrapper(self):
class Wrapper:
def __call__(self, b):
diff --git a/Misc/NEWS.d/next/Library/2024-02-15-19-11-49.gh-issue-101293.898b8l.rst b/Misc/NEWS.d/next/Library/2024-02-15-19-11-49.gh-issue-101293.898b8l.rst
new file mode 100644
index 0000000..98365d2
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-02-15-19-11-49.gh-issue-101293.898b8l.rst
@@ -0,0 +1,4 @@
+Support callables with the ``__call__()`` method and types with
+``__new__()`` and ``__init__()`` methods set to class methods, static
+methods, bound methods, partial functions, and other types of methods and
+descriptors in :meth:`inspect.Signature.from_callable`.