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.py259
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)