inspect.signature: Support duck-types of Python functions (Cython, for instance) #17159
This commit is contained in:
parent
4ded1f3553
commit
63da7c7b0c
|
@ -793,6 +793,10 @@ callables that follow ``__signature__`` protocol. It is still
|
||||||
recommended to update your code to use :func:`~inspect.signature`
|
recommended to update your code to use :func:`~inspect.signature`
|
||||||
directly. (Contributed by Yury Selivanov in :issue:`17481`)
|
directly. (Contributed by Yury Selivanov in :issue:`17481`)
|
||||||
|
|
||||||
|
:func:`~inspect.signature` now supports duck types of CPython functions,
|
||||||
|
which adds support for functions compiled with Cython. (Contributed
|
||||||
|
by Stefan Behnel and Yury Selivanov in :issue:`17159`)
|
||||||
|
|
||||||
|
|
||||||
logging
|
logging
|
||||||
-------
|
-------
|
||||||
|
|
|
@ -1601,6 +1601,30 @@ def _signature_is_builtin(obj):
|
||||||
obj in (type, object))
|
obj in (type, object))
|
||||||
|
|
||||||
|
|
||||||
|
def _signature_is_functionlike(obj):
|
||||||
|
# Internal helper to test if `obj` is a duck type of FunctionType.
|
||||||
|
# A good example of such objects are functions compiled with
|
||||||
|
# Cython, which have all attributes that a pure Python function
|
||||||
|
# would have, but have their code statically compiled.
|
||||||
|
|
||||||
|
if not callable(obj) or isclass(obj):
|
||||||
|
# All function-like objects are obviously callables,
|
||||||
|
# and not classes.
|
||||||
|
return False
|
||||||
|
|
||||||
|
name = getattr(obj, '__name__', None)
|
||||||
|
code = getattr(obj, '__code__', None)
|
||||||
|
defaults = getattr(obj, '__defaults__', _void) # Important to use _void ...
|
||||||
|
kwdefaults = getattr(obj, '__kwdefaults__', _void) # ... and not None here
|
||||||
|
annotations = getattr(obj, '__annotations__', None)
|
||||||
|
|
||||||
|
return (isinstance(code, types.CodeType) and
|
||||||
|
isinstance(name, str) and
|
||||||
|
(defaults is None or isinstance(defaults, tuple)) and
|
||||||
|
(kwdefaults is None or isinstance(kwdefaults, dict)) and
|
||||||
|
isinstance(annotations, dict))
|
||||||
|
|
||||||
|
|
||||||
def _signature_get_bound_param(spec):
|
def _signature_get_bound_param(spec):
|
||||||
# Internal helper to get first parameter name from a
|
# Internal helper to get first parameter name from a
|
||||||
# __text_signature__ of a builtin method, which should
|
# __text_signature__ of a builtin method, which should
|
||||||
|
@ -1670,7 +1694,9 @@ def signature(obj):
|
||||||
if _signature_is_builtin(obj):
|
if _signature_is_builtin(obj):
|
||||||
return Signature.from_builtin(obj)
|
return Signature.from_builtin(obj)
|
||||||
|
|
||||||
if isinstance(obj, types.FunctionType):
|
if isfunction(obj) or _signature_is_functionlike(obj):
|
||||||
|
# If it's a pure Python function, or an object that is duck type
|
||||||
|
# of a Python function (Cython functions, for instance), then:
|
||||||
return Signature.from_function(obj)
|
return Signature.from_function(obj)
|
||||||
|
|
||||||
if isinstance(obj, functools.partial):
|
if isinstance(obj, functools.partial):
|
||||||
|
@ -2071,7 +2097,9 @@ class Signature:
|
||||||
def from_function(cls, func):
|
def from_function(cls, func):
|
||||||
'''Constructs Signature for the given python function'''
|
'''Constructs Signature for the given python function'''
|
||||||
|
|
||||||
if not isinstance(func, types.FunctionType):
|
if not (isfunction(func) or _signature_is_functionlike(func)):
|
||||||
|
# If it's not a pure Python function, and not a duck type
|
||||||
|
# of pure function:
|
||||||
raise TypeError('{!r} is not a Python function'.format(func))
|
raise TypeError('{!r} is not a Python function'.format(func))
|
||||||
|
|
||||||
Parameter = cls._parameter_cls
|
Parameter = cls._parameter_cls
|
||||||
|
|
|
@ -1740,6 +1740,66 @@ class TestSignatureObject(unittest.TestCase):
|
||||||
with self.assertRaisesRegex(TypeError, 'is not a Python builtin'):
|
with self.assertRaisesRegex(TypeError, 'is not a Python builtin'):
|
||||||
inspect.Signature.from_builtin(42)
|
inspect.Signature.from_builtin(42)
|
||||||
|
|
||||||
|
def test_signature_from_functionlike_object(self):
|
||||||
|
def func(a,b, *args, kwonly=True, kwonlyreq, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class funclike:
|
||||||
|
# Has to be callable, and have correct
|
||||||
|
# __code__, __annotations__, __defaults__, __name__,
|
||||||
|
# and __kwdefaults__ attributes
|
||||||
|
|
||||||
|
def __init__(self, func):
|
||||||
|
self.__name__ = func.__name__
|
||||||
|
self.__code__ = func.__code__
|
||||||
|
self.__annotations__ = func.__annotations__
|
||||||
|
self.__defaults__ = func.__defaults__
|
||||||
|
self.__kwdefaults__ = func.__kwdefaults__
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self.func(*args, **kwargs)
|
||||||
|
|
||||||
|
sig_func = inspect.Signature.from_function(func)
|
||||||
|
|
||||||
|
sig_funclike = inspect.Signature.from_function(funclike(func))
|
||||||
|
self.assertEqual(sig_funclike, sig_func)
|
||||||
|
|
||||||
|
sig_funclike = inspect.signature(funclike(func))
|
||||||
|
self.assertEqual(sig_funclike, sig_func)
|
||||||
|
|
||||||
|
# If object is not a duck type of function, then
|
||||||
|
# signature will try to get a signature for its '__call__'
|
||||||
|
# method
|
||||||
|
fl = funclike(func)
|
||||||
|
del fl.__defaults__
|
||||||
|
self.assertEqual(self.signature(fl),
|
||||||
|
((('args', ..., ..., "var_positional"),
|
||||||
|
('kwargs', ..., ..., "var_keyword")),
|
||||||
|
...))
|
||||||
|
|
||||||
|
def test_signature_functionlike_class(self):
|
||||||
|
# We only want to duck type function-like objects,
|
||||||
|
# not classes.
|
||||||
|
|
||||||
|
def func(a,b, *args, kwonly=True, kwonlyreq, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class funclike:
|
||||||
|
def __init__(self, marker):
|
||||||
|
pass
|
||||||
|
|
||||||
|
__name__ = func.__name__
|
||||||
|
__code__ = func.__code__
|
||||||
|
__annotations__ = func.__annotations__
|
||||||
|
__defaults__ = func.__defaults__
|
||||||
|
__kwdefaults__ = func.__kwdefaults__
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, 'is not a Python function'):
|
||||||
|
inspect.Signature.from_function(funclike)
|
||||||
|
|
||||||
|
self.assertEqual(str(inspect.signature(funclike)), '(marker)')
|
||||||
|
|
||||||
def test_signature_on_method(self):
|
def test_signature_on_method(self):
|
||||||
class Test:
|
class Test:
|
||||||
def __init__(*args):
|
def __init__(*args):
|
||||||
|
|
|
@ -13,6 +13,9 @@ Core and Builtins
|
||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #17159: inspect.signature now accepts duck types of functions,
|
||||||
|
which adds support for Cython functions. Initial patch by Stefan Behnel.
|
||||||
|
|
||||||
- Issue #18801: Fix inspect.classify_class_attrs to correctly classify
|
- Issue #18801: Fix inspect.classify_class_attrs to correctly classify
|
||||||
object.__new__ and object.__init__.
|
object.__new__ and object.__init__.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue