diff options
Diffstat (limited to 'sphinx/util/inspect.py')
-rw-r--r-- | sphinx/util/inspect.py | 259 |
1 files changed, 214 insertions, 45 deletions
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 0f3f47562..202e170c1 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -4,11 +4,12 @@ Helpers for inspecting Python modules. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import builtins +import contextlib import enum import inspect import re @@ -17,30 +18,28 @@ import types import typing import warnings from functools import partial, partialmethod -from inspect import ( # NOQA - Parameter, isclass, ismethod, ismethoddescriptor -) +from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA from io import StringIO -from typing import Any, Callable, Mapping, List, Optional, Tuple -from typing import cast +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, 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 ForwardRef from sphinx.util.typing import stringify as stringify_annotation if sys.version_info > (3, 7): - from types import ( - ClassMethodDescriptorType, - MethodDescriptorType, - WrapperDescriptorType - ) + from types import ClassMethodDescriptorType, MethodDescriptorType, WrapperDescriptorType else: ClassMethodDescriptorType = type(object.__init__) MethodDescriptorType = type(str.join) WrapperDescriptorType = type(dict.__dict__['fromkeys']) +if False: + # For type annotation + from typing import Type # NOQA + logger = logging.getLogger(__name__) memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE) @@ -60,14 +59,6 @@ def getargspec(func: Callable) -> Any: methods.""" warnings.warn('sphinx.ext.inspect.getargspec() is deprecated', RemovedInSphinx50Warning, stacklevel=2) - # 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) @@ -120,7 +111,11 @@ def getargspec(func: Callable) -> Any: def unwrap(obj: Any) -> Any: """Get an original object from wrapped object (wrapped functions).""" try: - return inspect.unwrap(obj) + if hasattr(obj, '__sphinx_mock__'): + # Skip unwrapping mock object to avoid RecursionError + return obj + else: + return inspect.unwrap(obj) except ValueError: # might be a mock object return obj @@ -146,6 +141,81 @@ def unwrap_all(obj: Any, *, stop: Callable = None) -> Any: return obj +def getall(obj: Any) -> Optional[Sequence[str]]: + """Get __all__ attribute of the module as dict. + + Return None if given *obj* does not have __all__. + Raises AttributeError if given *obj* raises an error on accessing __all__. + Raises ValueError if given *obj* have invalid __all__. + """ + __all__ = safe_getattr(obj, '__all__', None) + if __all__ is None: + return None + else: + if (isinstance(__all__, (list, tuple)) and all(isinstance(e, str) for e in __all__)): + return __all__ + else: + raise ValueError(__all__) + + +def getannotations(obj: Any) -> Mapping[str, Any]: + """Get __annotations__ from given *obj* safely. + + Raises AttributeError if given *obj* raises an error on accessing __attribute__. + """ + __annotations__ = safe_getattr(obj, '__annotations__', None) + if isinstance(__annotations__, Mapping): + return __annotations__ + else: + return {} + + +def getmro(obj: Any) -> Tuple["Type", ...]: + """Get __mro__ from given *obj* safely. + + Raises AttributeError if given *obj* raises an error on accessing __mro__. + """ + __mro__ = safe_getattr(obj, '__mro__', None) + if isinstance(__mro__, tuple): + return __mro__ + else: + return tuple() + + +def getslots(obj: Any) -> Optional[Dict]: + """Get __slots__ attribute of the class as dict. + + Return None if gienv *obj* does not have __slots__. + Raises AttributeError if given *obj* raises an error on accessing __slots__. + Raises TypeError if given *obj* is not a class. + Raises ValueError if given *obj* have invalid __slots__. + """ + if not inspect.isclass(obj): + raise TypeError + + __slots__ = safe_getattr(obj, '__slots__', None) + if __slots__ is None: + return None + elif isinstance(__slots__, dict): + return __slots__ + elif isinstance(__slots__, str): + return {__slots__: None} + elif isinstance(__slots__, (list, tuple)): + return {e: None for e in __slots__} + else: + raise ValueError + + +def isNewType(obj: Any) -> bool: + """Check the if object is a kind of NewType.""" + __module__ = safe_getattr(obj, '__module__', None) + __qualname__ = safe_getattr(obj, '__qualname__', None) + if __module__ == 'typing' and __qualname__ == 'NewType.<locals>.new_type': + return True + else: + return False + + def isenumclass(x: Any) -> bool: """Check if the object is subclass of enum.""" return inspect.isclass(x) and issubclass(x, enum.Enum) @@ -302,6 +372,11 @@ def iscoroutinefunction(obj: Any) -> bool: def isproperty(obj: Any) -> bool: """Check if the object is property.""" + if sys.version_info >= (3, 8): + from functools import cached_property # cached_property is available since py3.8 + if isinstance(obj, cached_property): + return True + return isinstance(obj, property) @@ -313,6 +388,9 @@ def isgenericalias(obj: Any) -> bool: elif (hasattr(types, 'GenericAlias') and # only for py39+ isinstance(obj, types.GenericAlias)): # type: ignore return True + elif (hasattr(typing, '_SpecialGenericAlias') and # for py39+ + isinstance(obj, typing._SpecialGenericAlias)): # type: ignore + return True else: return False @@ -321,7 +399,7 @@ 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: + except Exception as exc: # sometimes accessing a property raises an exception (e.g. # NotImplementedError), so let's try to read the attribute directly try: @@ -336,7 +414,7 @@ def safe_getattr(obj: Any, name: str, *defargs: Any) -> Any: if defargs: return defargs[0] - raise AttributeError(name) + raise AttributeError(name) from exc def safe_getmembers(object: Any, predicate: Callable[[str], bool] = None, @@ -385,8 +463,8 @@ def object_description(object: Any) -> str: for x in sorted_values) try: s = repr(object) - except Exception: - raise ValueError + except Exception as exc: + raise ValueError from exc # Strip non-deterministic memory addresses such as # ``<__main__.A at 0x7f68cb685710>`` s = memory_address_re.sub('', s) @@ -421,17 +499,50 @@ def is_builtin_class_method(obj: Any, attr_name: str) -> bool: return getattr(builtins, name, None) is cls -def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False - ) -> inspect.Signature: +class DefaultValue: + """A simple wrapper for default value of the parameters of overload functions.""" + + def __init__(self, value: str) -> None: + self.value = value + + def __eq__(self, other: object) -> bool: + return self.value == other + + def __repr__(self) -> str: + return self.value + + +def _should_unwrap(subject: Callable) -> bool: + """Check the function should be unwrapped on getting signature.""" + if (safe_getattr(subject, '__globals__', None) and + subject.__globals__.get('__name__') == 'contextlib' and # type: ignore + subject.__globals__.get('__file__') == contextlib.__file__): # type: ignore + # contextmanger should be unwrapped + return True + + return False + + +def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = None, + type_aliases: Dict = {}) -> inspect.Signature: """Return a Signature object for the given *subject*. :param bound_method: Specify *subject* is a bound method or not :param follow_wrapped: Same as ``inspect.signature()``. - Defaults to ``False`` (get a signature of *subject*). """ + + if follow_wrapped is None: + follow_wrapped = True + else: + warnings.warn('The follow_wrapped argument of sphinx.util.inspect.signature() is ' + 'deprecated', RemovedInSphinx50Warning, stacklevel=2) + try: try: - signature = inspect.signature(subject, follow_wrapped=follow_wrapped) + if _should_unwrap(subject): + signature = inspect.signature(subject) + else: + signature = inspect.signature(subject, follow_wrapped=follow_wrapped) except ValueError: # follow built-in wrappers up (ex. functools.lru_cache) signature = inspect.signature(subject) @@ -448,10 +559,10 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo raise try: - # Update unresolved annotations using ``get_type_hints()``. - annotations = typing.get_type_hints(subject) + # Resolve annotations using ``get_type_hints()`` and type_aliases. + annotations = typing.get_type_hints(subject, None, type_aliases) for i, param in enumerate(parameters): - if isinstance(param.annotation, str) and param.name in annotations: + if param.name in annotations: parameters[i] = param.replace(annotation=annotations[param.name]) if 'return' in annotations: return_annotation = annotations['return'] @@ -469,7 +580,60 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo if len(parameters) > 0: parameters.pop(0) - return inspect.Signature(parameters, return_annotation=return_annotation) + # To allow to create signature object correctly for pure python functions, + # pass an internal parameter __validate_parameters__=False to Signature + # + # For example, this helps a function having a default value `inspect._empty`. + # refs: https://github.com/sphinx-doc/sphinx/issues/7935 + return inspect.Signature(parameters, return_annotation=return_annotation, # type: ignore + __validate_parameters__=False) + + +def evaluate_signature(sig: inspect.Signature, globalns: Dict = None, localns: Dict = None + ) -> inspect.Signature: + """Evaluate unresolved type annotations in a signature object.""" + def evaluate_forwardref(ref: ForwardRef, globalns: Dict, localns: Dict) -> Any: + """Evaluate a forward reference.""" + if sys.version_info > (3, 9): + return ref._evaluate(globalns, localns, frozenset()) + else: + return ref._evaluate(globalns, localns) + + def evaluate(annotation: Any, globalns: Dict, localns: Dict) -> Any: + """Evaluate unresolved type annotation.""" + try: + if isinstance(annotation, str): + ref = ForwardRef(annotation, True) + annotation = evaluate_forwardref(ref, globalns, localns) + + if isinstance(annotation, ForwardRef): + annotation = evaluate_forwardref(ref, globalns, localns) + elif isinstance(annotation, str): + # might be a ForwardRef'ed annotation in overloaded functions + ref = ForwardRef(annotation, True) + annotation = evaluate_forwardref(ref, globalns, localns) + except (NameError, TypeError): + # failed to evaluate type. skipped. + pass + + return annotation + + if globalns is None: + globalns = {} + if localns is None: + localns = globalns + + parameters = list(sig.parameters.values()) + for i, param in enumerate(parameters): + if param.annotation: + annotation = evaluate(param.annotation, globalns, localns) + parameters[i] = param.replace(annotation=annotation) + + return_annotation = sig.return_annotation + if return_annotation: + return_annotation = evaluate(return_annotation, globalns, localns) + + return sig.replace(parameters=parameters, return_annotation=return_annotation) def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, @@ -526,11 +690,16 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, 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 + code = 'def func' + signature + ': pass' + module = ast.parse(code) + function = cast(ast.FunctionDef, module.body[0]) # type: ignore + + return signature_from_ast(function, code) + - # parameters - args = definition.args +def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signature: + """Create a Signature object from AST *node*.""" + args = node.args defaults = list(args.defaults) params = [] if hasattr(args, "posonlyargs"): @@ -548,9 +717,9 @@ def signature_from_str(signature: str) -> inspect.Signature: if defaults[i] is Parameter.empty: default = Parameter.empty else: - default = ast_unparse(defaults[i]) + default = DefaultValue(ast_unparse(defaults[i], code)) - annotation = ast_unparse(arg.annotation) or Parameter.empty + annotation = ast_unparse(arg.annotation, code) or Parameter.empty params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY, default=default, annotation=annotation)) @@ -558,29 +727,29 @@ def signature_from_str(signature: str) -> inspect.Signature: if defaults[i + posonlyargs] is Parameter.empty: default = Parameter.empty else: - default = ast_unparse(defaults[i + posonlyargs]) + default = DefaultValue(ast_unparse(defaults[i + posonlyargs], code)) - annotation = ast_unparse(arg.annotation) or Parameter.empty + annotation = ast_unparse(arg.annotation, code) 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 + annotation = ast_unparse(args.vararg.annotation, code) 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 + default = ast_unparse(args.kw_defaults[i], code) or Parameter.empty + annotation = ast_unparse(arg.annotation, code) 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 + annotation = ast_unparse(args.kwarg.annotation, code) or Parameter.empty params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD, annotation=annotation)) - return_annotation = ast_unparse(definition.returns) or Parameter.empty + return_annotation = ast_unparse(node.returns, code) or Parameter.empty return inspect.Signature(params, return_annotation=return_annotation) |