From da39645ad300a097decc4e35bb3ea6dbf0633886 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Thu, 27 Mar 2014 12:09:24 -0400 Subject: [PATCH] inspect.Signature: Add 'Signature.from_callable' classmethod. Closes #17373 --- Doc/library/inspect.rst | 12 ++++++ Doc/whatsnew/3.5.rst | 4 ++ Lib/inspect.py | 86 ++++++++++++++++++++++++++-------------- Lib/test/test_inspect.py | 13 ++++++ Misc/NEWS | 2 + 5 files changed, 87 insertions(+), 30 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 0c087123e80..85d9593693b 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -506,6 +506,18 @@ function. >>> str(new_sig) "(a, b) -> 'new return anno'" + .. classmethod:: Signature.from_callable(obj) + + Return a :class:`Signature` (or its subclass) object for a given callable + ``obj``. This method simplifies subclassing of :class:`Signature`: + + :: + + class MySignature(Signature): + pass + sig = MySignature.from_callable(min) + assert isinstance(sig, MySignature) + .. class:: Parameter(name, kind, \*, default=Parameter.empty, annotation=Parameter.empty) diff --git a/Doc/whatsnew/3.5.rst b/Doc/whatsnew/3.5.rst index c94102da241..78bddcb9536 100644 --- a/Doc/whatsnew/3.5.rst +++ b/Doc/whatsnew/3.5.rst @@ -140,6 +140,10 @@ Improved Modules * :class:`inspect.Signature` and :class:`inspect.Parameter` are now picklable (contributed by Yury Selivanov in :issue:`20726`). +* New class method :meth:`inspect.Signature.from_callable`, which makes + subclassing of :class:`~inspect.Signature` easier (contributed + by Yury Selivanov and Eric Snow in :issue:`17373`). + Optimizations ============= diff --git a/Lib/inspect.py b/Lib/inspect.py index bce0516a090..acb80714ffa 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -969,7 +969,8 @@ def getfullargspec(func): sig = _signature_internal(func, follow_wrapper_chains=False, - skip_bound_arg=False) + skip_bound_arg=False, + sigcls=Signature) except Exception as ex: # Most of the times 'signature' will raise ValueError. # But, it can also raise AttributeError, and, maybe something @@ -1861,7 +1862,10 @@ def _signature_from_builtin(cls, func, skip_bound_arg=True): return _signature_fromstr(cls, func, s, skip_bound_arg) -def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True): +def _signature_internal(obj, *, + follow_wrapper_chains=True, + skip_bound_arg=True, + sigcls): if not callable(obj): raise TypeError('{!r} is not a callable object'.format(obj)) @@ -1869,9 +1873,12 @@ def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True): if isinstance(obj, types.MethodType): # In this case we skip the first parameter of the underlying # function (usually `self` or `cls`). - sig = _signature_internal(obj.__func__, - follow_wrapper_chains, - skip_bound_arg) + sig = _signature_internal( + obj.__func__, + follow_wrapper_chains=follow_wrapper_chains, + skip_bound_arg=skip_bound_arg, + sigcls=sigcls) + if skip_bound_arg: return _signature_bound_method(sig) else: @@ -1902,9 +1909,12 @@ def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True): # (usually `self`, or `cls`) will not be passed # automatically (as for boundmethods) - wrapped_sig = _signature_internal(partialmethod.func, - follow_wrapper_chains, - skip_bound_arg) + wrapped_sig = _signature_internal( + partialmethod.func, + follow_wrapper_chains=follow_wrapper_chains, + skip_bound_arg=skip_bound_arg, + sigcls=sigcls) + sig = _signature_get_partial(wrapped_sig, partialmethod, (None,)) first_wrapped_param = tuple(wrapped_sig.parameters.values())[0] @@ -1915,16 +1925,18 @@ def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True): 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 sigcls.from_function(obj) if _signature_is_builtin(obj): - return _signature_from_builtin(Signature, obj, + return _signature_from_builtin(sigcls, obj, skip_bound_arg=skip_bound_arg) if isinstance(obj, functools.partial): - wrapped_sig = _signature_internal(obj.func, - follow_wrapper_chains, - skip_bound_arg) + wrapped_sig = _signature_internal( + obj.func, + follow_wrapper_chains=follow_wrapper_chains, + skip_bound_arg=skip_bound_arg, + sigcls=sigcls) return _signature_get_partial(wrapped_sig, obj) sig = None @@ -1935,23 +1947,29 @@ def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True): # in its metaclass call = _signature_get_user_defined_method(type(obj), '__call__') if call is not None: - sig = _signature_internal(call, - follow_wrapper_chains, - skip_bound_arg) + sig = _signature_internal( + call, + follow_wrapper_chains=follow_wrapper_chains, + skip_bound_arg=skip_bound_arg, + sigcls=sigcls) else: # Now we check if the 'obj' class has a '__new__' method new = _signature_get_user_defined_method(obj, '__new__') if new is not None: - sig = _signature_internal(new, - follow_wrapper_chains, - skip_bound_arg) + sig = _signature_internal( + new, + follow_wrapper_chains=follow_wrapper_chains, + skip_bound_arg=skip_bound_arg, + sigcls=sigcls) else: # Finally, we should have at least __init__ implemented init = _signature_get_user_defined_method(obj, '__init__') if init is not None: - sig = _signature_internal(init, - follow_wrapper_chains, - skip_bound_arg) + sig = _signature_internal( + init, + follow_wrapper_chains=follow_wrapper_chains, + skip_bound_arg=skip_bound_arg, + sigcls=sigcls) if sig is None: # At this point we know, that `obj` is a class, with no user- @@ -1973,7 +1991,7 @@ def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True): if text_sig: # If 'obj' class has a __text_signature__ attribute: # return a signature based on it - return _signature_fromstr(Signature, obj, text_sig) + return _signature_fromstr(sigcls, obj, text_sig) # No '__text_signature__' was found for the 'obj' class. # Last option is to check if its '__init__' is @@ -1993,9 +2011,11 @@ def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True): call = _signature_get_user_defined_method(type(obj), '__call__') if call is not None: try: - sig = _signature_internal(call, - follow_wrapper_chains, - skip_bound_arg) + sig = _signature_internal( + call, + follow_wrapper_chains=follow_wrapper_chains, + skip_bound_arg=skip_bound_arg, + sigcls=sigcls) except ValueError as ex: msg = 'no signature found for {!r}'.format(obj) raise ValueError(msg) from ex @@ -2015,10 +2035,6 @@ def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True): raise ValueError('callable {!r} is not supported by signature'.format(obj)) -def signature(obj): - '''Get a signature object for the passed callable.''' - return _signature_internal(obj) - class _void: '''A private marker - used in Parameter & Signature''' @@ -2464,6 +2480,10 @@ class Signature: def from_builtin(cls, func): return _signature_from_builtin(cls, func) + @classmethod + def from_callable(cls, obj): + return _signature_internal(obj, sigcls=cls) + @property def parameters(self): return self._parameters @@ -2723,6 +2743,12 @@ class Signature: return rendered + +def signature(obj): + '''Get a signature object for the passed callable.''' + return Signature.from_callable(obj) + + def _main(): """ Logic for inspecting an object given at command line """ import argparse diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 373bd4c360f..5bdd327903d 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -2517,6 +2517,19 @@ class TestSignatureObject(unittest.TestCase): self.assertEqual(self.signature(Spam.foo), self.signature(Ham.foo)) + def test_signature_from_callable_python_obj(self): + class MySignature(inspect.Signature): pass + def foo(a, *, b:1): pass + foo_sig = MySignature.from_callable(foo) + self.assertTrue(isinstance(foo_sig, MySignature)) + + @unittest.skipIf(MISSING_C_DOCSTRINGS, + "Signature information for builtins requires docstrings") + def test_signature_from_callable_builtin_obj(self): + class MySignature(inspect.Signature): pass + sig = MySignature.from_callable(_pickle.Pickler) + self.assertTrue(isinstance(sig, MySignature)) + class TestParameterObject(unittest.TestCase): def test_signature_parameter_kinds(self): diff --git a/Misc/NEWS b/Misc/NEWS index 20e8c29c8cb..b53c518b907 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -109,6 +109,8 @@ Library - Issue #20726: inspect.signature: Make Signature and Parameter picklable. +- Issue #17373: Add inspect.Signature.from_callable method. + Documentation -------------