sphinx/sphinx/util/inspect.py

595 lines
22 KiB
Python

# -*- coding: utf-8 -*-
"""
sphinx.util.inspect
~~~~~~~~~~~~~~~~~~~
Helpers for inspecting Python modules.
:copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import absolute_import
import builtins
import enum
import inspect
import re
import sys
import typing
from functools import partial
from six import StringIO
from sphinx.util import logging
from sphinx.util.pycompat import NoneType
if False:
# For type annotation
from typing import Any, Callable, Mapping, List, Tuple, Type # NOQA
from sphinx.util.typing import unicode # NOQA
logger = logging.getLogger(__name__)
memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
# Copied from the definition of inspect.getfullargspec from Python master,
# and modified to remove the use of special flags that break decorated
# callables and bound methods in the name of backwards compatibility. Used
# under the terms of PSF license v2, which requires the above statement
# and the following:
#
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software
# Foundation; All Rights Reserved
def getargspec(func):
"""Like inspect.getfullargspec but supports bound methods, and wrapped
methods."""
# On 3.5+, signature(int) or similar raises ValueError. On 3.4, it
# succeeds with a bogus signature. We want a TypeError uniformly, to
# match historical behavior.
if (isinstance(func, type) and
is_builtin_class_method(func, "__new__") and
is_builtin_class_method(func, "__init__")):
raise TypeError(
"can't compute signature for built-in type {}".format(func))
sig = inspect.signature(func)
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 inspect.Parameter.POSITIONAL_ONLY:
args.append(name)
elif kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(name)
if param.default is not param.empty:
defaults += (param.default,) # type: ignore
elif kind is inspect.Parameter.VAR_POSITIONAL:
varargs = name
elif kind is inspect.Parameter.KEYWORD_ONLY:
kwonlyargs.append(name)
if param.default is not param.empty:
kwdefaults[name] = param.default
elif kind is inspect.Parameter.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
return inspect.FullArgSpec(args, varargs, varkw, defaults,
kwonlyargs, kwdefaults, annotations)
def isenumclass(x):
# type: (Type) -> bool
"""Check if the object is subclass of enum."""
if enum is None:
return False
return inspect.isclass(x) and issubclass(x, enum.Enum)
def isenumattribute(x):
# type: (Any) -> bool
"""Check if the object is attribute of enum."""
if enum is None:
return False
return isinstance(x, enum.Enum)
def ispartial(obj):
# type: (Any) -> bool
"""Check if the object is partial."""
return isinstance(obj, partial)
def isclassmethod(obj):
# type: (Any) -> bool
"""Check if the object is classmethod."""
if isinstance(obj, classmethod):
return True
elif inspect.ismethod(obj):
if getattr(obj, 'im_self', None): # py2
return True
elif getattr(obj, '__self__', None): # py3
return True
return False
def isstaticmethod(obj, cls=None, name=None):
# type: (Any, Any, unicode) -> bool
"""Check if the object is staticmethod."""
if isinstance(obj, staticmethod):
return True
elif cls and name:
# trace __mro__ if the method is defined in parent class
#
# .. note:: This only works well with new style classes.
for basecls in getattr(cls, '__mro__', [cls]):
meth = basecls.__dict__.get(name)
if meth:
if isinstance(meth, staticmethod):
return True
else:
return False
return False
def isdescriptor(x):
# type: (Any) -> bool
"""Check if the object is some kind of descriptor."""
for item in '__get__', '__set__', '__delete__':
if hasattr(safe_getattr(x, item, None), '__call__'):
return True
return False
def isfunction(obj):
# type: (Any) -> bool
"""Check if the object is function."""
return inspect.isfunction(obj) or ispartial(obj) and inspect.isfunction(obj.func)
def isbuiltin(obj):
# type: (Any) -> bool
"""Check if the object is builtin."""
return inspect.isbuiltin(obj) or ispartial(obj) and inspect.isbuiltin(obj.func)
def safe_getattr(obj, name, *defargs):
# type: (Any, unicode, unicode) -> object
"""A getattr() that turns all exceptions into AttributeErrors."""
try:
return getattr(obj, name, *defargs)
except Exception:
# sometimes accessing a property raises an exception (e.g.
# NotImplementedError), so let's try to read the attribute directly
try:
# In case the object does weird things with attribute access
# such that accessing `obj.__dict__` may raise an exception
return obj.__dict__[name]
except Exception:
pass
# this is a catch-all for all the weird things that some modules do
# with attribute access
if defargs:
return defargs[0]
raise AttributeError(name)
def safe_getmembers(object, predicate=None, attr_getter=safe_getattr):
# type: (Any, Callable[[unicode], bool], Callable) -> List[Tuple[unicode, Any]]
"""A version of inspect.getmembers() that uses safe_getattr()."""
results = [] # type: List[Tuple[unicode, Any]]
for key in dir(object):
try:
value = attr_getter(object, key, None)
except AttributeError:
continue
if not predicate or predicate(value):
results.append((key, value))
results.sort()
return results
def object_description(object):
# type: (Any) -> unicode
"""A repr() implementation that returns text safe to use in reST context."""
if isinstance(object, dict):
try:
sorted_keys = sorted(object)
except Exception:
pass # Cannot sort dict keys, fall back to generic repr
else:
items = ("%s: %s" %
(object_description(key), object_description(object[key]))
for key in sorted_keys)
return "{%s}" % ", ".join(items)
if isinstance(object, set):
try:
sorted_values = sorted(object)
except TypeError:
pass # Cannot sort set values, fall back to generic repr
else:
return "{%s}" % ", ".join(object_description(x) for x in sorted_values)
if isinstance(object, frozenset):
try:
sorted_values = sorted(object)
except TypeError:
pass # Cannot sort frozenset values, fall back to generic repr
else:
return "frozenset({%s})" % ", ".join(object_description(x)
for x in sorted_values)
try:
s = repr(object)
except Exception:
raise ValueError
# Strip non-deterministic memory addresses such as
# ``<__main__.A at 0x7f68cb685710>``
s = memory_address_re.sub('', s)
return s.replace('\n', ' ')
def is_builtin_class_method(obj, attr_name):
# type: (Any, unicode) -> bool
"""If attr_name is implemented at builtin class, return True.
>>> is_builtin_class_method(int, '__init__')
True
Why this function needed? CPython implements int.__init__ by Descriptor
but PyPy implements it by pure Python code.
"""
classes = [c for c in inspect.getmro(obj) if attr_name in c.__dict__]
cls = classes[0] if classes else object
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:
"""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:
"""The Signature object represents the call signature of a callable object and
its return annotation.
"""
def __init__(self, subject, bound_method=False, has_retval=True):
# type: (Callable, bool, 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
self.has_retval = has_retval
self.partialmethod_with_noargs = False
try:
self.signature = inspect.signature(subject)
except IndexError:
# Until python 3.6.4, cpython has been crashed on inspection for
# partialmethods not having any arguments.
# https://bugs.python.org/issue33009
if hasattr(subject, '_partialmethod'):
self.signature = None
self.partialmethod_with_noargs = True
else:
raise
try:
self.annotations = typing.get_type_hints(subject)
except Exception:
# get_type_hints() does not support some kind of objects like partial,
# ForwardRef and so on. For them, it raises an exception. In that case,
# we try to build annotations from argspec.
self.annotations = {}
if bound_method:
# client gives a hint that the subject is a bound method
if 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:
# inspect.signature recognizes type of method properly without any hints
self.skip_first_argument = False
@property
def parameters(self):
# type: () -> Mapping
if self.partialmethod_with_noargs:
return {}
else:
return self.signature.parameters
@property
def return_annotation(self):
# type: () -> Any
if self.signature:
if self.has_retval:
return self.signature.return_annotation
else:
return inspect.Parameter.empty
else:
return None
def format_args(self):
# type: () -> unicode
args = []
last_kind = None
for i, param in enumerate(self.parameters.values()):
# 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,
None):
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, str) 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))
else:
arg.write(' = ')
arg.write(object_description(param.default))
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 self.return_annotation is inspect.Parameter.empty:
return '(%s)' % ', '.join(args)
else:
if '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, str):
return annotation
elif isinstance(annotation, typing.TypeVar): # type: ignore
return annotation.__name__
elif not annotation:
return repr(annotation)
elif annotation is NoneType: # type: ignore
return 'None'
elif getattr(annotation, '__module__', None) == 'builtins':
return annotation.__qualname__
elif annotation is Ellipsis:
return '...'
if sys.version_info >= (3, 7): # py37+
return self.format_annotation_new(annotation)
else:
return self.format_annotation_old(annotation)
def format_annotation_new(self, annotation):
# type: (Any) -> str
"""format_annotation() for py37+"""
module = getattr(annotation, '__module__', None)
if module == 'typing':
if getattr(annotation, '_name', None):
qualname = annotation._name
elif getattr(annotation, '__qualname__', None):
qualname = annotation.__qualname__
elif getattr(annotation, '__forward_arg__', None):
qualname = annotation.__forward_arg__
else:
qualname = self.format_annotation(annotation.__origin__) # ex. Union
elif hasattr(annotation, '__qualname__'):
qualname = '%s.%s' % (module, annotation.__qualname__)
else:
qualname = repr(annotation)
if getattr(annotation, '__args__', None):
if qualname == 'Union':
if len(annotation.__args__) == 2 and annotation.__args__[1] is NoneType: # type: ignore # NOQA
return 'Optional[%s]' % self.format_annotation(annotation.__args__[0])
else:
args = ', '.join(self.format_annotation(a) for a in annotation.__args__)
return '%s[%s]' % (qualname, args)
elif qualname == 'Callable':
args = ', '.join(self.format_annotation(a) for a in annotation.__args__[:-1])
returns = self.format_annotation(annotation.__args__[-1])
return '%s[[%s], %s]' % (qualname, args, returns)
else:
args = ', '.join(self.format_annotation(a) for a in annotation.__args__)
return '%s[%s]' % (qualname, args)
return qualname
def format_annotation_old(self, annotation):
# type: (Any) -> str
"""format_annotation() for py36 or below"""
module = getattr(annotation, '__module__', None)
if module == 'typing':
if getattr(annotation, '_name', None):
qualname = annotation._name
elif getattr(annotation, '__qualname__', None):
qualname = annotation.__qualname__
elif getattr(annotation, '__forward_arg__', None):
qualname = annotation.__forward_arg__
elif getattr(annotation, '__origin__', None):
qualname = self.format_annotation(annotation.__origin__) # ex. Union
else:
qualname = repr(annotation).replace('typing.', '')
elif hasattr(annotation, '__qualname__'):
qualname = '%s.%s' % (module, annotation.__qualname__)
else:
qualname = repr(annotation)
if (hasattr(typing, 'TupleMeta') and
isinstance(annotation, typing.TupleMeta) and # type: ignore
not hasattr(annotation, '__tuple_params__')):
# This is for Python 3.6+, 3.5 case is handled below
params = annotation.__args__
param_str = ', '.join(self.format_annotation(p) for p in params)
return '%s[%s]' % (qualname, param_str)
elif (hasattr(typing, 'GenericMeta') and # for py36 or below
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]' % (qualname, 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]' % (qualname, 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:
if len(params) == 2 and params[1] is NoneType: # type: ignore
return 'Optional[%s]' % self.format_annotation(params[0])
else:
param_str = ', '.join(self.format_annotation(p) for p in params)
return '%s[%s]' % (qualname, param_str)
elif (hasattr(typing, 'Union') and # for py36
hasattr(annotation, '__origin__') and
annotation.__origin__ is typing.Union):
params = annotation.__args__
if params is not None:
if len(params) == 2 and params[1] is NoneType: # type: ignore
return 'Optional[%s]' % self.format_annotation(params[0])
else:
param_str = ', '.join(self.format_annotation(p) for p in params)
return 'Union[%s]' % param_str
elif (hasattr(typing, 'CallableMeta') and # for py36 or below
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 qualname
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]' % (qualname,
args_str,
self.format_annotation(annotation.__result__))
elif (hasattr(typing, 'TupleMeta') and # for py36 or below
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]' % (qualname,
', '.join(param_strings))
return qualname
def getdoc(obj, attrgetter=safe_getattr, allow_inherited=False):
# type: (Any, Callable, bool) -> unicode
"""Get the docstring for the object.
This tries to obtain the docstring for some kind of objects additionally:
* partial functions
* inherited docstring
"""
doc = attrgetter(obj, '__doc__', None)
if ispartial(obj) and doc == obj.__class__.__doc__:
return getdoc(obj.func)
elif doc is None and allow_inherited:
doc = inspect.getdoc(obj)
return doc