diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index c50b55387..c81909668 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -33,7 +33,7 @@ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.application import ExtensionError from sphinx.util import logging from sphinx.util.nodes import nested_parse_with_titles -from sphinx.util.inspect import getargspec, isdescriptor, safe_getmembers, \ +from sphinx.util.inspect import Signature, isdescriptor, safe_getmembers, \ safe_getattr, object_description, is_builtin_class_method, \ isenumclass, isenumattribute from sphinx.util.docstrings import prepare_docstring @@ -1358,7 +1358,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ # cannot introspect arguments of a C function or method return None try: - argspec = getargspec(self.object) + args = Signature(self.object).format_args() except TypeError: if (is_builtin_class_method(self.object, '__new__') and is_builtin_class_method(self.object, '__init__')): @@ -1368,12 +1368,10 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ # typing) we try to use the constructor signature as function # signature without the first argument. try: - argspec = getargspec(self.object.__new__) + args = Signature(self.object.__new__, bound_method=True).format_args() except TypeError: - argspec = getargspec(self.object.__init__) - if argspec[0]: - del argspec[0][0] - args = formatargspec(self.object, *argspec) + args = Signature(self.object.__init__, bound_method=True).format_args() + # escape backslashes for reST args = args.replace('\\', '\\\\') return args @@ -1425,14 +1423,11 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): return None try: - argspec = getargspec(initmeth) + return Signature(initmeth, bound_method=True).format_args() except TypeError: # still not possible: happens e.g. for old-style classes # with __init__ in C return None - if argspec[0] and argspec[0][0] in ('cls', 'self'): - del argspec[0][0] - return formatargspec(initmeth, *argspec) def format_signature(self): # type: () -> unicode @@ -1619,10 +1614,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: inspect.ismethoddescriptor(self.object): # can never get arguments of a C function or method return None - argspec = getargspec(self.object) - if argspec[0] and argspec[0][0] in ('cls', 'self'): - del argspec[0][0] - args = formatargspec(self.object, *argspec) + args = Signature(self.object, bound_method=True).format_args() # escape backslashes for reST args = args.replace('\\', '\\\\') return args diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 5e0d219ec..39842db18 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -8,21 +8,21 @@ :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +from __future__ import absolute_import import re +import typing +import inspect +from collections import OrderedDict -from six import PY3, binary_type +from six import PY2, PY3, StringIO, binary_type, string_types, itervalues from six.moves import builtins from sphinx.util import force_decode if False: # For type annotation - from typing import Any, Callable, List, Tuple, Type # NOQA - -# this imports the standard library inspect module without resorting to -# relatively import this module -inspect = __import__('inspect') + from typing import Any, Callable, Dict, List, Tuple, Type # NOQA memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE) @@ -113,7 +113,7 @@ else: # 2.7 func = func.func if not inspect.isfunction(func): raise TypeError('%r is not a Python function' % func) - args, varargs, varkw = inspect.getargs(func.__code__) + args, varargs, varkw = inspect.getargs(func.__code__) # type: ignore func_defaults = func.__defaults__ if func_defaults is None: func_defaults = [] @@ -231,3 +231,228 @@ def is_builtin_class_method(obj, attr_name): if not hasattr(builtins, safe_getattr(cls, '__name__', '')): # type: ignore return False return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls # type: ignore + + +class Parameter(object): + """Fake parameter class for python2.""" + POSITIONAL_ONLY = 0 + POSITIONAL_OR_KEYWORD = 1 + VAR_POSITIONAL = 2 + KEYWORD_ONLY = 3 + VAR_KEYWORD = 4 + empty = object() + + def __init__(self, name, kind=POSITIONAL_OR_KEYWORD, default=empty): + # type: (str, int, Any) -> None + self.name = name + self.kind = kind + self.default = default + self.annotation = self.empty + + +class Signature(object): + """The Signature object represents the call signature of a callable object and + its return annotation. + """ + + def __init__(self, subject, bound_method=False): + # type: (Callable, bool) -> None + # check subject is not a built-in class (ex. int, str) + if (isinstance(subject, type) and + is_builtin_class_method(subject, "__new__") and + is_builtin_class_method(subject, "__init__")): + raise TypeError("can't compute signature for built-in type {}".format(subject)) + + self.subject = subject + + if PY3: + self.signatures = inspect.signature(subject) + else: + self.argspec = getargspec(subject) + + try: + self.annotations = typing.get_type_hints(subject) + except: + self.annotations = None + + if bound_method: + # client gives a hint that the subject is a bound method + + if PY3 and inspect.ismethod(subject): + # inspect.signature already considers the subject is bound method. + # So it is not need to skip first argument. + self.skip_first_argument = False + else: + self.skip_first_argument = True + else: + if PY3: + # inspect.signature recognizes type of method properly without any hints + self.skip_first_argument = False + else: + # check the subject is bound method or not + self.skip_first_argument = inspect.ismethod(subject) and subject.__self__ # type: ignore # NOQA + + @property + def parameters(self): + # type: () -> Dict + if PY3: + return self.signatures.parameters + else: + params = OrderedDict() # type: Dict + positionals = len(self.argspec.args) - len(self.argspec.defaults) + for i, arg in enumerate(self.argspec.args): + if i < positionals: + params[arg] = Parameter(arg) + else: + default = self.argspec.defaults[i - positionals] + params[arg] = Parameter(arg, default=default) + if self.argspec.varargs: + params[self.argspec.varargs] = Parameter(self.argspec.varargs, + Parameter.VAR_POSITIONAL) + if self.argspec.keywords: + params[self.argspec.keywords] = Parameter(self.argspec.keywords, + Parameter.VAR_KEYWORD) + return params + + @property + def return_annotation(self): + # type: () -> Any + if PY3: + return self.signatures.return_annotation + else: + return None + + def format_args(self): + # type: () -> unicode + args = [] + last_kind = None + for i, param in enumerate(itervalues(self.parameters)): + # skip first argument if subject is bound method + if self.skip_first_argument and i == 0: + continue + + arg = StringIO() + + # insert '*' between POSITIONAL args and KEYWORD_ONLY args:: + # func(a, b, *, c, d): + if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, + param.POSITIONAL_ONLY): + args.append('*') + + if param.kind in (param.POSITIONAL_ONLY, + param.POSITIONAL_OR_KEYWORD, + param.KEYWORD_ONLY): + arg.write(param.name) + if param.annotation is not param.empty: + if isinstance(param.annotation, string_types) and \ + param.name in self.annotations: + arg.write(': ') + arg.write(self.format_annotation(self.annotations[param.name])) + else: + arg.write(': ') + arg.write(self.format_annotation(param.annotation)) + if param.default is not param.empty: + if param.annotation is param.empty: + arg.write('=') + arg.write(object_description(param.default)) # type: ignore + else: + arg.write(' = ') + arg.write(object_description(param.default)) # type: ignore + elif param.kind == param.VAR_POSITIONAL: + arg.write('*') + arg.write(param.name) + elif param.kind == param.VAR_KEYWORD: + arg.write('**') + arg.write(param.name) + + args.append(arg.getvalue()) + last_kind = param.kind + + if PY2 or self.return_annotation is inspect.Parameter.empty: + return '(%s)' % ', '.join(args) + else: + if isinstance(self.return_annotation, string_types) and \ + 'return' in self.annotations: + annotation = self.format_annotation(self.annotations['return']) + else: + annotation = self.format_annotation(self.return_annotation) + + return '(%s) -> %s' % (', '.join(args), annotation) + + def format_annotation(self, annotation): + # type: (Any) -> str + """Return formatted representation of a type annotation. + + Show qualified names for types and additional details for types from + the ``typing`` module. + + Displaying complex types from ``typing`` relies on its private API. + """ + if isinstance(annotation, string_types): + return annotation # type: ignore + if isinstance(annotation, typing.TypeVar): # type: ignore + return annotation.__name__ + if annotation == Ellipsis: + return '...' + if not isinstance(annotation, type): + return repr(annotation) + + qualified_name = (annotation.__module__ + '.' + annotation.__qualname__ # type: ignore + if annotation else repr(annotation)) + + if annotation.__module__ == 'builtins': + return annotation.__qualname__ # type: ignore + elif isinstance(annotation, typing.GenericMeta): + # In Python 3.5.2+, all arguments are stored in __args__, + # whereas __parameters__ only contains generic parameters. + # + # Prior to Python 3.5.2, __args__ is not available, and all + # arguments are in __parameters__. + params = None + if hasattr(annotation, '__args__'): + if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA + params = annotation.__args__ # type: ignore + else: # typing.Callable + args = ', '.join(self.format_annotation(arg) for arg + in annotation.__args__[:-1]) # type: ignore + result = self.format_annotation(annotation.__args__[-1]) # type: ignore + return '%s[[%s], %s]' % (qualified_name, args, result) + elif hasattr(annotation, '__parameters__'): + params = annotation.__parameters__ # type: ignore + if params is not None: + param_str = ', '.join(self.format_annotation(p) for p in params) + return '%s[%s]' % (qualified_name, param_str) + elif (hasattr(typing, 'UnionMeta') and # for py35 or below + isinstance(annotation, typing.UnionMeta) and # type: ignore + hasattr(annotation, '__union_params__')): + params = annotation.__union_params__ # type: ignore + if params is not None: + param_str = ', '.join(self.format_annotation(p) for p in params) + return '%s[%s]' % (qualified_name, param_str) + elif (isinstance(annotation, typing.CallableMeta) and # type: ignore + getattr(annotation, '__args__', None) is not None and + hasattr(annotation, '__result__')): + # Skipped in the case of plain typing.Callable + args = annotation.__args__ # type: ignore + if args is None: + return qualified_name + elif args is Ellipsis: + args_str = '...' + else: + formatted_args = (self.format_annotation(a) for a in args) + args_str = '[%s]' % ', '.join(formatted_args) + return '%s[%s, %s]' % (qualified_name, + args_str, + self.format_annotation(annotation.__result__)) # type: ignore # NOQA + elif (isinstance(annotation, typing.TupleMeta) and # type: ignore + hasattr(annotation, '__tuple_params__') and + hasattr(annotation, '__tuple_use_ellipsis__')): + params = annotation.__tuple_params__ # type: ignore + if params is not None: + param_strings = [self.format_annotation(p) for p in params] + if annotation.__tuple_use_ellipsis__: # type: ignore + param_strings.append('...') + return '%s[%s]' % (qualified_name, + ', '.join(param_strings)) + + return qualified_name diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index e04e38bb3..caf31b7e9 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -235,7 +235,7 @@ def test_format_signature(): pass assert formatsig('method', 'H.foo', H.foo1, None, None) == '(b, *c)' assert formatsig('method', 'H.foo', H.foo1, 'a', None) == '(a)' - assert formatsig('method', 'H.foo', H.foo2, None, None) == '(b, *c)' + assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)' assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\\n')" # test exception handling (exception is caught and args is '') diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 5e6439705..6176449b8 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -113,6 +113,146 @@ def test_getargspec_bound_methods(): assert expected_bound == inspect.getargspec(wrapped_bound_method) +def test_Signature(): + # literals + with pytest.raises(TypeError): + inspect.Signature(1) + + with pytest.raises(TypeError): + inspect.Signature('') + + # builitin classes + with pytest.raises(TypeError): + inspect.Signature(int) + + with pytest.raises(TypeError): + inspect.Signature(str) + + # normal function + def func(a, b, c=1, d=2, *e, **f): + pass + + sig = inspect.Signature(func).format_args() + assert sig == '(a, b, c=1, d=2, *e, **f)' + + +def test_Signature_partial(): + def fun(a, b, c=1, d=2): + pass + p = functools.partial(fun, 10, c=11) + + sig = inspect.Signature(p).format_args() + if sys.version_info < (3,): + assert sig == '(b, d=2)' + else: + assert sig == '(b, *, c=11, d=2)' + + +def test_Signature_methods(): + class Foo: + def meth1(self, arg1, **kwargs): + pass + + @classmethod + def meth2(cls, arg1, *args, **kwargs): + pass + + @staticmethod + def meth3(arg1, *args, **kwargs): + pass + + @functools.wraps(Foo().meth1) + def wrapped_bound_method(*args, **kwargs): + pass + + # unbound method + sig = inspect.Signature(Foo.meth1).format_args() + assert sig == '(self, arg1, **kwargs)' + + sig = inspect.Signature(Foo.meth1, bound_method=True).format_args() + assert sig == '(arg1, **kwargs)' + + # bound method + sig = inspect.Signature(Foo().meth1).format_args() + assert sig == '(arg1, **kwargs)' + + # class method + sig = inspect.Signature(Foo.meth2).format_args() + assert sig == '(arg1, *args, **kwargs)' + + sig = inspect.Signature(Foo().meth2).format_args() + assert sig == '(arg1, *args, **kwargs)' + + # static method + sig = inspect.Signature(Foo.meth3).format_args() + assert sig == '(arg1, *args, **kwargs)' + + sig = inspect.Signature(Foo().meth3).format_args() + assert sig == '(arg1, *args, **kwargs)' + + # wrapped bound method + sig = inspect.Signature(wrapped_bound_method).format_args() + if sys.version_info < (3,): + assert sig == '(*args, **kwargs)' + elif sys.version_info < (3, 4, 4): + assert sig == '(self, arg1, **kwargs)' + else: + assert sig == '(arg1, **kwargs)' + + +@pytest.mark.skipif(sys.version_info < (3, 5), + reason='type annotation test is available on py35 or above') +def test_Signature_annotations(): + from typing_test_data import f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11 + + # Class annotations + sig = inspect.Signature(f0).format_args() + assert sig == '(x: int, y: numbers.Integral) -> None' + + # Generic types with concrete parameters + sig = inspect.Signature(f1).format_args() + assert sig == '(x: typing.List[int]) -> typing.List[int]' + + # TypeVars and generic types with TypeVars + sig = inspect.Signature(f2).format_args() + assert sig == '(x: typing.List[T], y: typing.List[T_co], z: T) -> typing.List[T_contra]' + + # Union types + sig = inspect.Signature(f3).format_args() + assert sig == '(x: typing.Union[str, numbers.Integral]) -> None' + + # Quoted annotations + sig = inspect.Signature(f4).format_args() + assert sig == '(x: str, y: str) -> None' + + # Keyword-only arguments + sig = inspect.Signature(f5).format_args() + assert sig == '(x: int, *, y: str, z: str) -> None' + + # Keyword-only arguments with varargs + sig = inspect.Signature(f6).format_args() + assert sig == '(x: int, *args, y: str, z: str) -> None' + + # Space around '=' for defaults + sig = inspect.Signature(f7).format_args() + assert sig == '(x: int = None, y: dict = {}) -> None' + + # Callable types + sig = inspect.Signature(f8).format_args() + assert sig == '(x: typing.Callable[[int, str], int]) -> None' + + sig = inspect.Signature(f9).format_args() + assert sig == '(x: typing.Callable) -> None' + + # Tuple types + sig = inspect.Signature(f10).format_args() + assert sig == '(x: typing.Tuple[int, str], y: typing.Tuple[int, ...]) -> None' + + # Instance annotations + sig = inspect.Signature(f11).format_args() + assert sig == '(x: CustomAnnotation, y: 123) -> None' + + def test_safe_getattr_with_default(): class Foo(object): def __getattr__(self, item):