sphinx/sphinx/util/inspect.py
2018-10-16 09:57:54 +09:00

605 lines
23 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, binary_type, string_types
from sphinx.util import force_decode
from sphinx.util import logging
from sphinx.util.pycompat import NoneType
if False:
# For type annotation
from typing import Any, Callable, Dict, List, Tuple, Type # 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) # type: ignore
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: # type: ignore
args.append(name)
elif kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: # type: ignore
args.append(name)
if param.default is not param.empty:
defaults += (param.default,) # type: ignore
elif kind is inspect.Parameter.VAR_POSITIONAL: # type: ignore
varargs = name
elif kind is inspect.Parameter.KEYWORD_ONLY: # type: ignore
kwonlyargs.append(name)
if param.default is not param.empty:
kwdefaults[name] = param.default
elif kind is inspect.Parameter.VAR_KEYWORD: # type: ignore
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, # type: ignore
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
if isinstance(s, binary_type):
s = force_decode(s, None) # type: ignore
# 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) # type: ignore
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:
if ispartial(subject):
# get_type_hints() does not support partial objects
self.annotations = {} # type: Dict[str, Any]
else:
self.annotations = typing.get_type_hints(subject) # type: ignore
except Exception as exc:
if (3, 5, 0) <= sys.version_info < (3, 5, 3) and isinstance(exc, AttributeError):
# python 3.5.2 raises ValueError for classmethod-ized partial objects.
self.annotations = {}
else:
logger.warning('Invalid type annotation found on %r. Ignored: %r',
subject, exc)
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: () -> Dict
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 # type: ignore
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, 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 self.return_annotation is inspect.Parameter.empty: # type: ignore
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, string_types):
return annotation # type: ignore
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