summaryrefslogtreecommitdiff
path: root/sphinx/util/inspect.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/util/inspect.py')
-rw-r--r--sphinx/util/inspect.py339
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)