diff options
author | Larry Hastings <larry@hastings.org> | 2013-11-23 23:37:55 (GMT) |
---|---|---|
committer | Larry Hastings <larry@hastings.org> | 2013-11-23 23:37:55 (GMT) |
commit | 44e2eaab5491881120aab43e2838da8afe7ab70e (patch) | |
tree | 92847876fa89736ab40d027431ff27e4973409c9 /Lib | |
parent | 7fa6e1aeea111e7d954b753fb483afc18f21add0 (diff) | |
download | cpython-44e2eaab5491881120aab43e2838da8afe7ab70e.zip cpython-44e2eaab5491881120aab43e2838da8afe7ab70e.tar.gz cpython-44e2eaab5491881120aab43e2838da8afe7ab70e.tar.bz2 |
Issue #19674: inspect.signature() now produces a correct signature
for some builtins.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/inspect.py | 62 | ||||
-rwxr-xr-x | Lib/pydoc.py | 52 | ||||
-rw-r--r-- | Lib/test/test_capi.py | 29 | ||||
-rw-r--r-- | Lib/test/test_inspect.py | 7 |
4 files changed, 118 insertions, 32 deletions
diff --git a/Lib/inspect.py b/Lib/inspect.py index edbf927..dc94e44 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -31,6 +31,7 @@ Here are some of the useful functions provided by this module: __author__ = ('Ka-Ping Yee <ping@lfw.org>', 'Yury Selivanov <yselivanov@sprymix.com>') +import ast import importlib.machinery import itertools import linecache @@ -1461,6 +1462,9 @@ def signature(obj): if isinstance(obj, types.FunctionType): return Signature.from_function(obj) + if isinstance(obj, types.BuiltinFunctionType): + return Signature.from_builtin(obj) + if isinstance(obj, functools.partial): sig = signature(obj.func) @@ -1942,6 +1946,64 @@ class Signature: return_annotation=annotations.get('return', _empty), __validate_parameters__=False) + @classmethod + def from_builtin(cls, func): + s = getattr(func, "__text_signature__", None) + if not s: + return None + + if s.endswith("/)"): + kind = Parameter.POSITIONAL_ONLY + s = s[:-2] + ')' + else: + kind = Parameter.POSITIONAL_OR_KEYWORD + + s = "def foo" + s + ": pass" + + try: + module = ast.parse(s) + except SyntaxError: + return None + if not isinstance(module, ast.Module): + return None + + # ast.FunctionDef + f = module.body[0] + + parameters = [] + empty = Parameter.empty + + def p(name_node, default_node, default=empty): + name = name_node.arg + + if isinstance(default_node, ast.Num): + default = default.n + elif isinstance(default_node, ast.NameConstant): + default = default_node.value + parameters.append(Parameter(name, kind, default=default, annotation=empty)) + + # non-keyword-only parameters + for name, default in reversed(list(itertools.zip_longest(reversed(f.args.args), reversed(f.args.defaults), fillvalue=None))): + p(name, default) + + # *args + if f.args.vararg: + kind = Parameter.VAR_POSITIONAL + p(f.args.vararg, empty) + + # keyword-only arguments + kind = Parameter.KEYWORD_ONLY + for name, default in zip(f.args.kwonlyargs, f.args.kw_defaults): + p(name, default) + + # **kwargs + if f.args.kwarg: + kind = Parameter.VAR_KEYWORD + p(f.args.kwarg, empty) + + return cls(parameters, return_annotation=cls.empty) + + @property def parameters(self): return self._parameters diff --git a/Lib/pydoc.py b/Lib/pydoc.py index e8127a2..2e63226 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -916,20 +916,18 @@ class HTMLDoc(Doc): reallink = realname title = '<a name="%s"><strong>%s</strong></a> = %s' % ( anchor, name, reallink) - if inspect.isfunction(object): - args, varargs, kwonlyargs, kwdefaults, varkw, defaults, ann = \ - inspect.getfullargspec(object) - argspec = inspect.formatargspec( - args, varargs, kwonlyargs, kwdefaults, varkw, defaults, ann, - formatvalue=self.formatvalue, - formatannotation=inspect.formatannotationrelativeto(object)) - if realname == '<lambda>': - title = '<strong>%s</strong> <em>lambda</em> ' % name - # XXX lambda's won't usually have func_annotations['return'] - # since the syntax doesn't support but it is possible. - # So removing parentheses isn't truly safe. - argspec = argspec[1:-1] # remove parentheses - else: + argspec = None + if inspect.isfunction(object) or inspect.isbuiltin(object): + signature = inspect.signature(object) + if signature: + argspec = str(signature) + if realname == '<lambda>': + title = '<strong>%s</strong> <em>lambda</em> ' % name + # XXX lambda's won't usually have func_annotations['return'] + # since the syntax doesn't support but it is possible. + # So removing parentheses isn't truly safe. + argspec = argspec[1:-1] # remove parentheses + if not argspec: argspec = '(...)' decl = title + argspec + (note and self.grey( @@ -1313,20 +1311,18 @@ location listed above. cl.__dict__[realname] is object): skipdocs = 1 title = self.bold(name) + ' = ' + realname - if inspect.isfunction(object): - args, varargs, varkw, defaults, kwonlyargs, kwdefaults, ann = \ - inspect.getfullargspec(object) - argspec = inspect.formatargspec( - args, varargs, varkw, defaults, kwonlyargs, kwdefaults, ann, - formatvalue=self.formatvalue, - formatannotation=inspect.formatannotationrelativeto(object)) - if realname == '<lambda>': - title = self.bold(name) + ' lambda ' - # XXX lambda's won't usually have func_annotations['return'] - # since the syntax doesn't support but it is possible. - # So removing parentheses isn't truly safe. - argspec = argspec[1:-1] # remove parentheses - else: + argspec = None + if inspect.isfunction(object) or inspect.isbuiltin(object): + signature = inspect.signature(object) + if signature: + argspec = str(signature) + if realname == '<lambda>': + title = self.bold(name) + ' lambda ' + # XXX lambda's won't usually have func_annotations['return'] + # since the syntax doesn't support but it is possible. + # So removing parentheses isn't truly safe. + argspec = argspec[1:-1] # remove parentheses + if not argspec: argspec = '(...)' decl = title + argspec + note diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py index 000079e..6f75b77 100644 --- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -109,6 +109,35 @@ class CAPITest(unittest.TestCase): self.assertRaises(TypeError, _posixsubprocess.fork_exec, Z(),[b'1'],3,[1, 2],5,6,7,8,9,10,11,12,13,14,15,16,17) + def test_docstring_signature_parsing(self): + + self.assertEqual(_testcapi.no_docstring.__doc__, None) + self.assertEqual(_testcapi.no_docstring.__text_signature__, None) + + self.assertEqual(_testcapi.docstring_empty.__doc__, "") + self.assertEqual(_testcapi.docstring_empty.__text_signature__, None) + + self.assertEqual(_testcapi.docstring_no_signature.__doc__, + "This docstring has no signature.") + self.assertEqual(_testcapi.docstring_no_signature.__text_signature__, None) + + self.assertEqual(_testcapi.docstring_with_invalid_signature.__doc__, + "docstring_with_invalid_signature (boo)\n" + "\n" + "This docstring has an invalid signature." + ) + self.assertEqual(_testcapi.docstring_with_invalid_signature.__text_signature__, None) + + self.assertEqual(_testcapi.docstring_with_signature.__doc__, + "This docstring has a valid signature.") + self.assertEqual(_testcapi.docstring_with_signature.__text_signature__, "(sig)") + + self.assertEqual(_testcapi.docstring_with_signature_and_extra_newlines.__doc__, + "This docstring has a valid signature and some extra newlines.") + self.assertEqual(_testcapi.docstring_with_signature_and_extra_newlines.__text_signature__, + "(parameter)") + + @unittest.skipUnless(threading, 'Threading required for this test.') class TestPendingCalls(unittest.TestCase): diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 9d34904..0258d3df 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -1588,10 +1588,9 @@ class TestSignatureObject(unittest.TestCase): with self.assertRaisesRegex(ValueError, 'not supported by signature'): # support for 'method-wrapper' inspect.signature(min.__call__) - with self.assertRaisesRegex(ValueError, - 'no signature found for builtin function'): - # support for 'method-wrapper' - inspect.signature(min) + self.assertEqual(inspect.signature(min), None) + signature = inspect.signature(os.stat) + self.assertTrue(isinstance(signature, inspect.Signature)) def test_signature_on_non_function(self): with self.assertRaisesRegex(TypeError, 'is not a callable object'): |