diff options
Diffstat (limited to 'sphinx/util/inspect.py')
-rw-r--r-- | sphinx/util/inspect.py | 339 |
1 files changed, 332 insertions, 7 deletions
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 704225359..62cd1a7e9 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -8,21 +8,22 @@ :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +from __future__ import absolute_import import re +import sys +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 +114,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 = [] @@ -239,3 +240,327 @@ 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.signature = inspect.signature(subject) + else: + self.argspec = getargspec(subject) + + try: + self.annotations = typing.get_type_hints(subject) # type: ignore + except Exception: + self.annotations = {} + + 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.signature.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.signature.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__ + 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__ + 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__)) + elif (isinstance(annotation, typing.TupleMeta) and # type: ignore + hasattr(annotation, '__tuple_params__') and + hasattr(annotation, '__tuple_use_ellipsis__')): + params = annotation.__tuple_params__ + if params is not None: + param_strings = [self.format_annotation(p) for p in params] + if annotation.__tuple_use_ellipsis__: + param_strings.append('...') + return '%s[%s]' % (qualified_name, + ', '.join(param_strings)) + + return qualified_name + + +if sys.version_info >= (3, 5): + getdoc = inspect.getdoc +else: + # code copied from the inspect.py module of the standard library + # of Python 3.5 + + def _findclass(func): + cls = sys.modules.get(func.__module__) + if cls is None: + return None + if hasattr(func, 'im_class'): + cls = func.im_class + else: + for name in func.__qualname__.split('.')[:-1]: + cls = getattr(cls, name) + if not inspect.isclass(cls): + return None + return cls + + def _finddoc(obj): + if inspect.isclass(obj): + for base in obj.__mro__: + if base is not object: + try: + doc = base.__doc__ + except AttributeError: + continue + if doc is not None: + return doc + return None + + if inspect.ismethod(obj) and getattr(obj, '__self__', None): + name = obj.__func__.__name__ + self = obj.__self__ + if (inspect.isclass(self) and + getattr(getattr(self, name, None), '__func__') + is obj.__func__): + # classmethod + cls = self + else: + cls = self.__class__ + elif inspect.isfunction(obj) or inspect.ismethod(obj): + name = obj.__name__ + cls = _findclass(obj) + if cls is None or getattr(cls, name) != obj: + return None + elif inspect.isbuiltin(obj): + name = obj.__name__ + self = obj.__self__ + if (inspect.isclass(self) and + self.__qualname__ + '.' + name == obj.__qualname__): + # classmethod + cls = self + else: + cls = self.__class__ + # Should be tested before isdatadescriptor(). + elif isinstance(obj, property): + func = obj.fget + name = func.__name__ + cls = _findclass(func) + if cls is None or getattr(cls, name) is not obj: + return None + elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj): + name = obj.__name__ + cls = obj.__objclass__ + if getattr(cls, name) is not obj: + return None + else: + return None + + for base in cls.__mro__: + try: + doc = getattr(base, name).__doc__ + except AttributeError: + continue + if doc is not None: + return doc + return None + + def getdoc(object): + """Get the documentation string for an object. + + All tabs are expanded to spaces. To clean up docstrings that are + indented to line up with blocks of code, any whitespace than can be + uniformly removed from the second line onwards is removed.""" + try: + doc = object.__doc__ + except AttributeError: + return None + if doc is None: + try: + doc = _finddoc(object) + except (AttributeError, TypeError): + return None + if not isinstance(doc, str): + return None + return inspect.cleandoc(doc) |