""" sphinx.util.inspect ~~~~~~~~~~~~~~~~~~~ Helpers for inspecting Python modules. :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import builtins import enum import inspect import re import sys import typing import warnings from functools import partial, partialmethod from inspect import ( # NOQA Parameter, isclass, ismethod, ismethoddescriptor ) from io import StringIO from typing import Any, Callable, Mapping, List, Optional, Tuple from typing import cast from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.pycode.ast import ast # for py35-37 from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import logging from sphinx.util.typing import stringify as stringify_annotation if sys.version_info > (3, 7): from types import ( ClassMethodDescriptorType, MethodDescriptorType, WrapperDescriptorType ) else: ClassMethodDescriptorType = type(object.__init__) MethodDescriptorType = type(str.join) WrapperDescriptorType = type(dict.__dict__['fromkeys']) 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: Callable) -> Any: """Like inspect.getfullargspec but supports bound methods, and wrapped methods.""" warnings.warn('sphinx.ext.inspect.getargspec() is deprecated', RemovedInSphinx50Warning) # 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 Parameter.POSITIONAL_ONLY: args.append(name) elif kind is Parameter.POSITIONAL_OR_KEYWORD: args.append(name) if param.default is not param.empty: defaults += (param.default,) # type: ignore elif kind is Parameter.VAR_POSITIONAL: varargs = name elif kind is Parameter.KEYWORD_ONLY: kwonlyargs.append(name) if param.default is not param.empty: kwdefaults[name] = param.default elif kind is 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 unwrap(obj: Any) -> Any: """Get an original object from wrapped object (wrapped functions).""" try: return inspect.unwrap(obj) except ValueError: # might be a mock object return obj def unwrap_all(obj: Any, *, stop: Callable = None) -> Any: """ Get an original object from wrapped object (unwrapping partials, wrapped functions, and other decorators). """ while True: if stop and stop(obj): return obj elif ispartial(obj): obj = obj.func elif inspect.isroutine(obj) and hasattr(obj, '__wrapped__'): obj = obj.__wrapped__ elif isclassmethod(obj): obj = obj.__func__ elif isstaticmethod(obj): obj = obj.__func__ else: return obj def isenumclass(x: Any) -> bool: """Check if the object is subclass of enum.""" return inspect.isclass(x) and issubclass(x, enum.Enum) def isenumattribute(x: Any) -> bool: """Check if the object is attribute of enum.""" return isinstance(x, enum.Enum) def unpartial(obj: Any) -> Any: """Get an original object from partial object. This returns given object itself if not partial. """ while ispartial(obj): obj = obj.func return obj def ispartial(obj: Any) -> bool: """Check if the object is partial.""" return isinstance(obj, (partial, partialmethod)) def isclassmethod(obj: Any) -> bool: """Check if the object is classmethod.""" if isinstance(obj, classmethod): return True elif inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__): return True return False def isstaticmethod(obj: Any, cls: Any = None, name: str = None) -> 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: 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 isabstractmethod(obj: Any) -> bool: """Check if the object is an abstractmethod.""" return safe_getattr(obj, '__isabstractmethod__', False) is True def is_cython_function_or_method(obj: Any) -> bool: """Check if the object is a function or method in cython.""" try: return obj.__class__.__name__ == 'cython_function_or_method' except AttributeError: return False def isattributedescriptor(obj: Any) -> bool: """Check if the object is an attribute like descriptor.""" if inspect.isdatadescriptor(obj): # data descriptor is kind of attribute return True elif isdescriptor(obj): # non data descriptor unwrapped = unwrap(obj) if isfunction(unwrapped) or isbuiltin(unwrapped) or inspect.ismethod(unwrapped): # attribute must not be either function, builtin and method return False elif is_cython_function_or_method(unwrapped): # attribute must not be either function and method (for cython) return False elif inspect.isclass(unwrapped): # attribute must not be a class return False elif isinstance(unwrapped, (ClassMethodDescriptorType, MethodDescriptorType, WrapperDescriptorType)): # attribute must not be a method descriptor return False elif type(unwrapped).__name__ == "instancemethod": # attribute must not be an instancemethod (C-API) return False else: return True else: return False def is_singledispatch_function(obj: Any) -> bool: """Check if the object is singledispatch function.""" if (inspect.isfunction(obj) and hasattr(obj, 'dispatch') and hasattr(obj, 'register') and obj.dispatch.__module__ == 'functools'): return True else: return False def is_singledispatch_method(obj: Any) -> bool: """Check if the object is singledispatch method.""" try: from functools import singledispatchmethod # type: ignore return isinstance(obj, singledispatchmethod) except ImportError: # py35-37 return False def isfunction(obj: Any) -> bool: """Check if the object is function.""" return inspect.isfunction(unwrap_all(obj)) def isbuiltin(obj: Any) -> bool: """Check if the object is builtin.""" return inspect.isbuiltin(unwrap_all(obj)) def isroutine(obj: Any) -> bool: """Check is any kind of function or method.""" return inspect.isroutine(unwrap_all(obj)) def iscoroutinefunction(obj: Any) -> bool: """Check if the object is coroutine-function.""" # unwrap staticmethod, classmethod and partial (except wrappers) obj = unwrap_all(obj, stop=lambda o: hasattr(o, '__wrapped__')) if hasattr(obj, '__code__') and inspect.iscoroutinefunction(obj): # check obj.__code__ because iscoroutinefunction() crashes for custom method-like # objects (see https://github.com/sphinx-doc/sphinx/issues/6605) return True else: return False def isproperty(obj: Any) -> bool: """Check if the object is property.""" return isinstance(obj, property) def safe_getattr(obj: Any, name: str, *defargs: Any) -> Any: """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: Any, predicate: Callable[[str], bool] = None, attr_getter: Callable = safe_getattr) -> List[Tuple[str, Any]]: """A version of inspect.getmembers() that uses safe_getattr().""" warnings.warn('safe_getmembers() is deprecated', RemovedInSphinx40Warning) results = [] # type: List[Tuple[str, 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: Any) -> str: """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: Any, attr_name: str) -> 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__', '')): return False return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls def signature(subject: Callable, bound_method: bool = False) -> inspect.Signature: """Return a Signature object for the given *subject*. :param bound_method: Specify *subject* is a bound method or not """ # 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)) try: signature = inspect.signature(subject) parameters = list(signature.parameters.values()) return_annotation = signature.return_annotation 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'): parameters = [] return_annotation = Parameter.empty else: raise try: # Update unresolved annotations using ``get_type_hints()``. annotations = typing.get_type_hints(subject) for i, param in enumerate(parameters): if isinstance(param.annotation, str) and param.name in annotations: parameters[i] = param.replace(annotation=annotations[param.name]) if 'return' in annotations: return_annotation = annotations['return'] except Exception: # ``get_type_hints()`` does not support some kind of objects like partial, # ForwardRef and so on. pass if bound_method: if inspect.ismethod(subject): # ``inspect.signature()`` considers the subject is a bound method and removes # first argument from signature. Therefore no skips are needed here. pass else: if len(parameters) > 0: parameters.pop(0) return inspect.Signature(parameters, return_annotation=return_annotation) def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, show_return_annotation: bool = True) -> str: """Stringify a Signature object. :param show_annotation: Show annotation in result """ args = [] last_kind = None for param in sig.parameters.values(): if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: # PEP-570: Separator for Positional Only Parameter: / args.append('/') if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY, None): # PEP-3102: Separator for Keyword Only Parameter: * args.append('*') arg = StringIO() if param.kind == param.VAR_POSITIONAL: arg.write('*' + param.name) elif param.kind == param.VAR_KEYWORD: arg.write('**' + param.name) else: arg.write(param.name) if show_annotation and param.annotation is not param.empty: arg.write(': ') arg.write(stringify_annotation(param.annotation)) if param.default is not param.empty: if show_annotation and param.annotation is not param.empty: arg.write(' = ') else: arg.write('=') arg.write(object_description(param.default)) args.append(arg.getvalue()) last_kind = param.kind if last_kind == Parameter.POSITIONAL_ONLY: # PEP-570: Separator for Positional Only Parameter: / args.append('/') if (sig.return_annotation is Parameter.empty or show_annotation is False or show_return_annotation is False): return '(%s)' % ', '.join(args) else: annotation = stringify_annotation(sig.return_annotation) return '(%s) -> %s' % (', '.join(args), annotation) def signature_from_str(signature: str) -> inspect.Signature: """Create a Signature object from string.""" module = ast.parse('def func' + signature + ': pass') definition = cast(ast.FunctionDef, module.body[0]) # type: ignore # parameters args = definition.args params = [] if hasattr(args, "posonlyargs"): for arg in args.posonlyargs: # type: ignore annotation = ast_unparse(arg.annotation) or Parameter.empty params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=annotation)) for i, arg in enumerate(args.args): if len(args.args) - i <= len(args.defaults): default = ast_unparse(args.defaults[-len(args.args) + i]) else: default = Parameter.empty annotation = ast_unparse(arg.annotation) or Parameter.empty params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD, default=default, annotation=annotation)) if args.vararg: annotation = ast_unparse(args.vararg.annotation) or Parameter.empty params.append(Parameter(args.vararg.arg, Parameter.VAR_POSITIONAL, annotation=annotation)) for i, arg in enumerate(args.kwonlyargs): default = ast_unparse(args.kw_defaults[i]) or Parameter.empty annotation = ast_unparse(arg.annotation) or Parameter.empty params.append(Parameter(arg.arg, Parameter.KEYWORD_ONLY, default=default, annotation=annotation)) if args.kwarg: annotation = ast_unparse(args.kwarg.annotation) or Parameter.empty params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD, annotation=annotation)) return_annotation = ast_unparse(definition.returns) or Parameter.empty return inspect.Signature(params, return_annotation=return_annotation) class Signature: """The Signature object represents the call signature of a callable object and its return annotation. """ empty = inspect.Signature.empty def __init__(self, subject: Callable, bound_method: bool = False, has_retval: bool = True) -> None: warnings.warn('sphinx.util.inspect.Signature() is deprecated', RemovedInSphinx40Warning) # 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: Optional[inspect.Signature] 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) -> Mapping: if self.partialmethod_with_noargs: return {} else: return self.signature.parameters @property def return_annotation(self) -> Any: if self.signature: if self.has_retval: return self.signature.return_annotation else: return Parameter.empty else: return None def format_args(self, show_annotation: bool = True) -> str: def get_annotation(param: Parameter) -> Any: if isinstance(param.annotation, str) and param.name in self.annotations: return self.annotations[param.name] else: return param.annotation 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 show_annotation and param.annotation is not param.empty: arg.write(': ') arg.write(stringify_annotation(get_annotation(param))) if param.default is not param.empty: if param.annotation is param.empty or show_annotation is False: 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) if show_annotation and param.annotation is not param.empty: arg.write(': ') arg.write(stringify_annotation(get_annotation(param))) elif param.kind == param.VAR_KEYWORD: arg.write('**') arg.write(param.name) if show_annotation and param.annotation is not param.empty: arg.write(': ') arg.write(stringify_annotation(get_annotation(param))) args.append(arg.getvalue()) last_kind = param.kind if self.return_annotation is Parameter.empty or show_annotation is False: return '(%s)' % ', '.join(args) else: if 'return' in self.annotations: annotation = stringify_annotation(self.annotations['return']) else: annotation = stringify_annotation(self.return_annotation) return '(%s) -> %s' % (', '.join(args), annotation) def format_annotation(self, annotation: Any) -> str: """Return formatted representation of a type annotation.""" return stringify_annotation(annotation) def format_annotation_new(self, annotation: Any) -> str: """format_annotation() for py37+""" return stringify_annotation(annotation) def format_annotation_old(self, annotation: Any) -> str: """format_annotation() for py36 or below""" return stringify_annotation(annotation) def getdoc(obj: Any, attrgetter: Callable = safe_getattr, allow_inherited: bool = False, cls: Any = None, name: str = None) -> str: """Get the docstring for the object. This tries to obtain the docstring for some kind of objects additionally: * partial functions * inherited docstring * inherited decorated methods """ 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) if doc is None and cls: # inspect.getdoc() does not support some kind of inherited and decorated methods. # This tries to obtain the docstring from super classes. for basecls in getattr(cls, '__mro__', []): meth = safe_getattr(basecls, name, None) if meth: doc = inspect.getdoc(meth) if doc: break return doc