diff options
author | Yury Selivanov <yselivanov@sprymix.com> | 2014-01-29 16:24:39 (GMT) |
---|---|---|
committer | Yury Selivanov <yselivanov@sprymix.com> | 2014-01-29 16:24:39 (GMT) |
commit | d82eddcf058deb8140e2d8670a4f2109872a2015 (patch) | |
tree | d2a6b2a095874e3ee7ad3f72acbbb0a055974870 | |
parent | 07a9e452accb432bd9da20b20e13f4bf8feebacb (diff) | |
download | cpython-d82eddcf058deb8140e2d8670a4f2109872a2015.zip cpython-d82eddcf058deb8140e2d8670a4f2109872a2015.tar.gz cpython-d82eddcf058deb8140e2d8670a4f2109872a2015.tar.bz2 |
inspect.getfullargspec: Use inspect.signature API behind the scenes #17481
-rw-r--r-- | Doc/whatsnew/3.4.rst | 7 | ||||
-rw-r--r-- | Lib/inspect.py | 111 | ||||
-rw-r--r-- | Lib/test/test_inspect.py | 42 | ||||
-rw-r--r-- | Misc/NEWS | 2 |
4 files changed, 155 insertions, 7 deletions
diff --git a/Doc/whatsnew/3.4.rst b/Doc/whatsnew/3.4.rst index 4718339..d7a04f6 100644 --- a/Doc/whatsnew/3.4.rst +++ b/Doc/whatsnew/3.4.rst @@ -786,6 +786,13 @@ As part of the implementation of the new :mod:`enum` module, the metaclasses (Contributed by Ethan Furman in :issue:`18929` and :issue:`19030`) +:func:`~inspect.getfullargspec` and :func:`~inspect.getargspec` +now use the :func:`~inspect.signature` API. This allows them to +support much broader range of functions, including some builtins and +callables that follow ``__signature__`` protocol. It is still +recommended to update your code to use :func:`~inspect.signature` +directly. (Contributed by Yury Selivanov in :issue:`17481`) + logging ------- diff --git a/Lib/inspect.py b/Lib/inspect.py index f06138c..4f9c1d1 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -934,7 +934,7 @@ FullArgSpec = namedtuple('FullArgSpec', 'args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations') def getfullargspec(func): - """Get the names and default values of a function's arguments. + """Get the names and default values of a callable object's arguments. A tuple of seven things is returned: (args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults annotations). @@ -948,13 +948,90 @@ def getfullargspec(func): The first four items in the tuple correspond to getargspec(). """ + builtin_method_param = None + if ismethod(func): + # There is a notable difference in behaviour between getfullargspec + # and Signature: the former always returns 'self' parameter for bound + # methods, whereas the Signature always shows the actual calling + # signature of the passed object. + # + # To simulate this behaviour, we "unbind" bound methods, to trick + # inspect.signature to always return their first parameter ("self", + # usually) func = func.__func__ - if not isfunction(func): - raise TypeError('{!r} is not a Python function'.format(func)) - args, varargs, kwonlyargs, varkw = _getfullargs(func.__code__) - return FullArgSpec(args, varargs, varkw, func.__defaults__, - kwonlyargs, func.__kwdefaults__, func.__annotations__) + + elif isbuiltin(func): + # We have a builtin function or method. For that, we check the + # special '__text_signature__' attribute, provided by the + # Argument Clinic. If it's a method, we'll need to make sure + # that its first parameter (usually "self") is always returned + # (see the previous comment). + text_signature = getattr(func, '__text_signature__', None) + if text_signature and text_signature.startswith('($'): + builtin_method_param = _signature_get_bound_param(text_signature) + + try: + sig = signature(func) + except Exception as ex: + # Most of the times 'signature' will raise ValueError. + # But, it can also raise AttributeError, and, maybe something + # else. So to be fully backwards compatible, we catch all + # possible exceptions here, and reraise a TypeError. + raise TypeError('unsupported callable') from ex + + args = [] + varargs = None + varkw = None + kwonlyargs = [] + defaults = () + annotations = {} + defaults = () + kwdefaults = {} + + if sig.return_annotation is not sig.empty: + annotations['return'] = sig.return_annotation + + for param in sig.parameters.values(): + kind = param.kind + name = param.name + + if kind is _POSITIONAL_ONLY: + args.append(name) + elif kind is _POSITIONAL_OR_KEYWORD: + args.append(name) + if param.default is not param.empty: + defaults += (param.default,) + elif kind is _VAR_POSITIONAL: + varargs = name + elif kind is _KEYWORD_ONLY: + kwonlyargs.append(name) + if param.default is not param.empty: + kwdefaults[name] = param.default + elif kind is _VAR_KEYWORD: + varkw = name + + if param.annotation is not param.empty: + annotations[name] = param.annotation + + if not kwdefaults: + # compatibility with 'func.__kwdefaults__' + kwdefaults = None + + if not defaults: + # compatibility with 'func.__defaults__' + defaults = None + + if builtin_method_param and (not args or args[0] != builtin_method_param): + # `func` is a method, and we always need to return its + # first parameter -- usually "self" (to be backwards + # compatible with the previous implementation of + # getfullargspec) + args.insert(0, builtin_method_param) + + return FullArgSpec(args, varargs, varkw, defaults, + kwonlyargs, kwdefaults, annotations) + ArgInfo = namedtuple('ArgInfo', 'args varargs keywords locals') @@ -1524,6 +1601,28 @@ def _signature_is_builtin(obj): obj in (type, object)) +def _signature_get_bound_param(spec): + # Internal helper to get first parameter name from a + # __text_signature__ of a builtin method, which should + # be in the following format: '($param1, ...)'. + # Assumptions are that the first argument won't have + # a default value or an annotation. + + assert spec.startswith('($') + + pos = spec.find(',') + if pos == -1: + pos = spec.find(')') + + cpos = spec.find(':') + assert cpos == -1 or cpos > pos + + cpos = spec.find('=') + assert cpos == -1 or cpos > pos + + return spec[2:pos] + + def signature(obj): '''Get a signature object for the passed callable.''' diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 6ee2c30..546fec5 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -578,6 +578,36 @@ class TestClassesAndFunctions(unittest.TestCase): kwonlyargs_e=['arg'], formatted='(*, arg)') + def test_getfullargspec_signature_attr(self): + def test(): + pass + spam_param = inspect.Parameter('spam', inspect.Parameter.POSITIONAL_ONLY) + test.__signature__ = inspect.Signature(parameters=(spam_param,)) + + self.assertFullArgSpecEquals(test, args_e=['spam'], formatted='(spam)') + + @unittest.skipIf(MISSING_C_DOCSTRINGS, + "Signature information for builtins requires docstrings") + def test_getfullargspec_builtin_methods(self): + self.assertFullArgSpecEquals(_pickle.Pickler.dump, + args_e=['self', 'obj'], formatted='(self, obj)') + + self.assertFullArgSpecEquals(_pickle.Pickler(io.BytesIO()).dump, + args_e=['self', 'obj'], formatted='(self, obj)') + + @unittest.skipIf(MISSING_C_DOCSTRINGS, + "Signature information for builtins requires docstrings") + def test_getfullagrspec_builtin_func(self): + builtin = _testcapi.docstring_with_signature_with_defaults + spec = inspect.getfullargspec(builtin) + self.assertEqual(spec.defaults[0], 'avocado') + + @unittest.skipIf(MISSING_C_DOCSTRINGS, + "Signature information for builtins requires docstrings") + def test_getfullagrspec_builtin_func_no_signature(self): + builtin = _testcapi.docstring_no_signature + with self.assertRaises(TypeError): + inspect.getfullargspec(builtin) def test_getargspec_method(self): class A(object): @@ -2614,6 +2644,15 @@ class TestBoundArguments(unittest.TestCase): self.assertNotEqual(ba, ba4) +class TestSignaturePrivateHelpers(unittest.TestCase): + def test_signature_get_bound_param(self): + getter = inspect._signature_get_bound_param + + self.assertEqual(getter('($self)'), 'self') + self.assertEqual(getter('($self, obj)'), 'self') + self.assertEqual(getter('($cls, /, obj)'), 'cls') + + class TestUnwrap(unittest.TestCase): def test_unwrap_one(self): @@ -2719,7 +2758,8 @@ def test_main(): TestGetcallargsFunctions, TestGetcallargsMethods, TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState, TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject, - TestBoundArguments, TestGetClosureVars, TestUnwrap, TestMain + TestBoundArguments, TestSignaturePrivateHelpers, TestGetClosureVars, + TestUnwrap, TestMain ) if __name__ == "__main__": @@ -47,6 +47,8 @@ Library - Issue #20105: the codec exception chaining now correctly sets the traceback of the original exception as its __traceback__ attribute. +- Issue #17481: inspect.getfullargspec() now uses inspect.signature() API. + IDLE ---- |