mirror of
https://github.com/readthedocs/sphinx-autoapi
synced 2024-10-31 21:20:17 +00:00
628 lines
19 KiB
Python
628 lines
19 KiB
Python
import builtins
|
|
import itertools
|
|
import re
|
|
import sys
|
|
|
|
import astroid
|
|
import astroid.nodes
|
|
import sphinx.util.logging
|
|
|
|
_LOGGER = sphinx.util.logging.getLogger(__name__)
|
|
|
|
|
|
def resolve_import_alias(name, import_names):
|
|
"""Resolve a name from an aliased import to its original name.
|
|
|
|
:param name: The potentially aliased name to resolve.
|
|
:type name: str
|
|
:param import_names: The pairs of original names and aliases
|
|
from the import.
|
|
:type import_names: iterable(tuple(str, str or None))
|
|
|
|
:returns: The original name.
|
|
:rtype: str
|
|
"""
|
|
resolved_name = name
|
|
|
|
for import_name, imported_as in import_names:
|
|
if import_name == name:
|
|
break
|
|
if imported_as == name:
|
|
resolved_name = import_name
|
|
break
|
|
|
|
return resolved_name
|
|
|
|
|
|
def get_full_import_name(import_from, name):
|
|
"""Get the full path of a name from a ``from x import y`` statement.
|
|
|
|
:param import_from: The astroid node to resolve the name of.
|
|
:type import_from: astroid.nodes.ImportFrom
|
|
:param name:
|
|
:type name: str
|
|
|
|
:returns: The full import path of the name.
|
|
:rtype: str
|
|
"""
|
|
partial_basename = resolve_import_alias(name, import_from.names)
|
|
|
|
module_name = import_from.modname
|
|
if import_from.level:
|
|
module = import_from.root()
|
|
assert isinstance(module, astroid.nodes.Module)
|
|
module_name = module.relative_to_absolute_name(
|
|
import_from.modname, level=import_from.level
|
|
)
|
|
|
|
return "{}.{}".format(module_name, partial_basename)
|
|
|
|
|
|
def resolve_qualname(node, basename):
|
|
"""Resolve where a node is defined to get its fully qualified name.
|
|
|
|
:param node: The node representing the base name.
|
|
:type node: astroid.NodeNG
|
|
:param basename: The partial base name to resolve.
|
|
:type basename: str
|
|
|
|
:returns: The fully resolved base name.
|
|
:rtype: str
|
|
"""
|
|
full_basename = basename
|
|
|
|
top_level_name = re.sub(r"\(.*\)", "", basename).split(".", 1)[0]
|
|
lookup_node = (
|
|
node if isinstance(node, astroid.node_classes.LookupMixIn) else node.scope()
|
|
)
|
|
assigns = lookup_node.lookup(top_level_name)[1]
|
|
|
|
for assignment in assigns:
|
|
if isinstance(assignment, astroid.nodes.ImportFrom):
|
|
import_name = get_full_import_name(assignment, top_level_name)
|
|
full_basename = basename.replace(top_level_name, import_name, 1)
|
|
break
|
|
if isinstance(assignment, astroid.nodes.Import):
|
|
import_name = resolve_import_alias(top_level_name, assignment.names)
|
|
full_basename = basename.replace(top_level_name, import_name, 1)
|
|
break
|
|
if isinstance(assignment, astroid.nodes.ClassDef):
|
|
full_basename = assignment.qname()
|
|
break
|
|
if isinstance(assignment, astroid.nodes.AssignName):
|
|
full_basename = "{}.{}".format(assignment.scope().qname(), assignment.name)
|
|
|
|
if isinstance(node, astroid.nodes.Call):
|
|
full_basename = re.sub(r"\(.*\)", "()", full_basename)
|
|
|
|
if full_basename.startswith("builtins."):
|
|
return full_basename[len("builtins.") :]
|
|
|
|
if full_basename.startswith("__builtin__."):
|
|
return full_basename[len("__builtin__.") :]
|
|
|
|
return full_basename
|
|
|
|
|
|
def get_full_basenames(node):
|
|
"""Resolve the partial names of a class' bases to fully qualified names.
|
|
|
|
:param node: The class definition node to resolve the bases of.
|
|
:type: astroid.ClassDef
|
|
|
|
:returns: The full names.
|
|
:rtype: iterable(str)
|
|
"""
|
|
for base in node.bases:
|
|
yield _resolve_annotation(base)
|
|
|
|
|
|
def _get_const_values(node):
|
|
value = None
|
|
|
|
if isinstance(node, (astroid.nodes.List, astroid.nodes.Tuple)):
|
|
new_value = []
|
|
for element in node.elts:
|
|
if isinstance(element, astroid.nodes.Const):
|
|
new_value.append(element.value)
|
|
elif isinstance(element, (astroid.nodes.List, astroid.nodes.Tuple)):
|
|
new_value.append(_get_const_values(element))
|
|
else:
|
|
break
|
|
else:
|
|
value = new_value
|
|
elif isinstance(node, astroid.nodes.Const):
|
|
value = node.value
|
|
|
|
return value
|
|
|
|
|
|
def get_assign_value(node):
|
|
"""Get the name and value of the assignment of the given node.
|
|
|
|
Assignments to multiple names are ignored, as per PEP 257.
|
|
|
|
:param node: The node to get the assignment value from.
|
|
:type node: astroid.nodes.Assign or astroid.nodes.AnnAssign
|
|
|
|
:returns: The name that is assigned to,
|
|
and the value assigned to the name (if it can be converted).
|
|
:rtype: tuple(str, object or None) or None
|
|
"""
|
|
try:
|
|
targets = node.targets
|
|
except AttributeError:
|
|
targets = [node.target]
|
|
|
|
if len(targets) == 1:
|
|
target = targets[0]
|
|
if isinstance(target, astroid.nodes.AssignName):
|
|
name = target.name
|
|
elif isinstance(target, astroid.nodes.AssignAttr):
|
|
name = target.attrname
|
|
else:
|
|
return None
|
|
return (name, _get_const_values(node.value))
|
|
|
|
return None
|
|
|
|
|
|
def get_assign_annotation(node):
|
|
"""Get the type annotation of the assignment of the given node.
|
|
|
|
:param node: The node to get the annotation for.
|
|
:type node: astroid.nodes.Assign or astroid.nodes.AnnAssign
|
|
|
|
:returns: The type annotation as a string, or None if one does not exist.
|
|
:rtype: str or None
|
|
"""
|
|
annotation_node = None
|
|
try:
|
|
annotation_node = node.annotation
|
|
except AttributeError:
|
|
annotation_node = node.type_annotation
|
|
|
|
return format_annotation(annotation_node, node)
|
|
|
|
|
|
def is_decorated_with_property(node):
|
|
"""Check if the function is decorated as a property.
|
|
|
|
:param node: The node to check.
|
|
:type node: astroid.nodes.FunctionDef
|
|
|
|
:returns: True if the function is a property, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
if not node.decorators:
|
|
return False
|
|
|
|
for decorator in node.decorators.nodes:
|
|
if not isinstance(decorator, astroid.Name):
|
|
continue
|
|
|
|
try:
|
|
if _is_property_decorator(decorator):
|
|
return True
|
|
except astroid.InferenceError:
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
def _is_property_decorator(decorator):
|
|
def _is_property_class(class_node):
|
|
return (
|
|
class_node.name == "property"
|
|
and class_node.root().name == builtins.__name__
|
|
)
|
|
|
|
for inferred in decorator.infer():
|
|
if not isinstance(inferred, astroid.nodes.ClassDef):
|
|
continue
|
|
|
|
if _is_property_class(inferred):
|
|
return True
|
|
|
|
if any(_is_property_class(ancestor) for ancestor in inferred.ancestors()):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def is_decorated_with_property_setter(node):
|
|
"""Check if the function is decorated as a property setter.
|
|
|
|
:param node: The node to check.
|
|
:type node: astroid.nodes.FunctionDef
|
|
|
|
:returns: True if the function is a property setter, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
if not node.decorators:
|
|
return False
|
|
|
|
for decorator in node.decorators.nodes:
|
|
if (
|
|
isinstance(decorator, astroid.nodes.Attribute)
|
|
and decorator.attrname == "setter"
|
|
):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def is_decorated_with_overload(node):
|
|
"""Check if the function is decorated as an overload definition.
|
|
|
|
:param node: The node to check.
|
|
:type node: astroid.nodes.FunctionDef
|
|
|
|
:returns: True if the function is an overload definition, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
if not node.decorators:
|
|
return False
|
|
|
|
for decorator in node.decorators.nodes:
|
|
if not isinstance(decorator, (astroid.Name, astroid.Attribute)):
|
|
continue
|
|
|
|
try:
|
|
if _is_overload_decorator(decorator):
|
|
return True
|
|
except astroid.InferenceError:
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
def _is_overload_decorator(decorator):
|
|
for inferred in decorator.infer():
|
|
if not isinstance(inferred, astroid.nodes.FunctionDef):
|
|
continue
|
|
|
|
if inferred.name == "overload" and inferred.root().name == "typing":
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def is_constructor(node):
|
|
"""Check if the function is a constructor.
|
|
|
|
:param node: The node to check.
|
|
:type node: astroid.nodes.FunctionDef
|
|
|
|
:returns: True if the function is a constructor, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
return (
|
|
node.parent
|
|
and isinstance(node.parent.scope(), astroid.nodes.ClassDef)
|
|
and node.name == "__init__"
|
|
)
|
|
|
|
|
|
def is_exception(node):
|
|
"""Check if a class is an exception.
|
|
|
|
:param node: The node to check.
|
|
:type node: astroid.nodes.ClassDef
|
|
|
|
:returns: True if the class is an exception, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
if node.name in ("Exception", "BaseException") and node.root().name == "builtins":
|
|
return True
|
|
|
|
if not hasattr(node, "ancestors"):
|
|
return False
|
|
|
|
return any(is_exception(parent) for parent in node.ancestors(recurs=True))
|
|
|
|
|
|
def is_local_import_from(node, package_name):
|
|
"""Check if a node is an import from the local package.
|
|
|
|
:param node: The node to check.
|
|
:type node: astroid.node.NodeNG
|
|
|
|
:param package_name: The name of the local package.
|
|
:type package_name: str
|
|
|
|
:returns: True if the node is an import from the local package,
|
|
False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
if not isinstance(node, astroid.ImportFrom):
|
|
return False
|
|
|
|
return (
|
|
node.level
|
|
or node.modname == package_name
|
|
or node.modname.startswith(package_name + ".")
|
|
)
|
|
|
|
|
|
def get_module_all(node):
|
|
"""Get the contents of the ``__all__`` variable from a module.
|
|
|
|
:param node: The module to get ``__all__`` from.
|
|
:type node: astroid.nodes.Module
|
|
|
|
:returns: The contents of ``__all__`` if defined. Otherwise None.
|
|
:rtype: list(str) or None
|
|
"""
|
|
all_ = None
|
|
|
|
if "__all__" in node.locals:
|
|
assigned = next(node.igetattr("__all__"))
|
|
if assigned is not astroid.Uninferable:
|
|
all_ = []
|
|
for elt in getattr(assigned, "elts", ()):
|
|
try:
|
|
elt_name = next(elt.infer())
|
|
except astroid.InferenceError:
|
|
continue
|
|
|
|
if elt_name is astroid.Uninferable:
|
|
continue
|
|
|
|
if isinstance(elt_name, astroid.Const) and isinstance(
|
|
elt_name.value, str
|
|
):
|
|
all_.append(elt_name.value)
|
|
|
|
return all_
|
|
|
|
|
|
def _is_ellipsis(node):
|
|
if sys.version_info < (3, 8):
|
|
return isinstance(node, astroid.Ellipsis)
|
|
|
|
return isinstance(node, astroid.Const) and node.value == Ellipsis
|
|
|
|
|
|
def merge_annotations(annotations, comment_annotations):
|
|
for ann, comment_ann in itertools.zip_longest(annotations, comment_annotations):
|
|
if ann and not _is_ellipsis(ann):
|
|
yield ann
|
|
elif comment_ann and not _is_ellipsis(comment_ann):
|
|
yield comment_ann
|
|
else:
|
|
yield None
|
|
|
|
|
|
def _resolve_annotation(annotation):
|
|
resolved = None
|
|
|
|
if isinstance(annotation, astroid.Const):
|
|
resolved = resolve_qualname(annotation, str(annotation.value))
|
|
elif isinstance(annotation, astroid.Name):
|
|
resolved = resolve_qualname(annotation, annotation.name)
|
|
elif isinstance(annotation, astroid.Attribute):
|
|
resolved = resolve_qualname(annotation, annotation.as_string())
|
|
elif isinstance(annotation, astroid.Subscript):
|
|
value = _resolve_annotation(annotation.value)
|
|
slice_node = annotation.slice
|
|
if isinstance(slice_node, astroid.Index):
|
|
slice_node = slice_node.value
|
|
if isinstance(slice_node, astroid.Tuple):
|
|
slice_ = ", ".join(_resolve_annotation(elt) for elt in slice_node.elts)
|
|
else:
|
|
slice_ = _resolve_annotation(slice_node)
|
|
resolved = f"{value}[{slice_}]"
|
|
elif isinstance(annotation, astroid.Tuple):
|
|
resolved = (
|
|
"(" + ", ".join(_resolve_annotation(elt) for elt in annotation.elts) + ")"
|
|
)
|
|
elif isinstance(annotation, astroid.List):
|
|
resolved = (
|
|
"[" + ", ".join(_resolve_annotation(elt) for elt in annotation.elts) + "]"
|
|
)
|
|
else:
|
|
resolved = annotation.as_string()
|
|
|
|
if resolved.startswith("typing."):
|
|
return resolved[len("typing.") :]
|
|
|
|
# Sphinx is capable of linking anything in the same module
|
|
# without needing a fully qualified path.
|
|
module_prefix = annotation.root().name + "."
|
|
if resolved.startswith(module_prefix):
|
|
return resolved[len(module_prefix) :]
|
|
|
|
return resolved
|
|
|
|
|
|
def format_annotation(annotation, parent):
|
|
if annotation:
|
|
# Workaround https://github.com/PyCQA/astroid/issues/851
|
|
if annotation.parent and not isinstance(
|
|
annotation.parent, astroid.node_classes.NodeNG
|
|
):
|
|
annotation.parent = parent
|
|
|
|
return _resolve_annotation(annotation)
|
|
|
|
return annotation
|
|
|
|
|
|
def _iter_args(args, annotations, defaults):
|
|
default_offset = len(args) - len(defaults)
|
|
packed = itertools.zip_longest(args, annotations)
|
|
for i, (arg, annotation) in enumerate(packed):
|
|
default = None
|
|
if defaults is not None and i >= default_offset:
|
|
if defaults[i - default_offset] is not None:
|
|
default = defaults[i - default_offset].as_string()
|
|
|
|
name = arg.name
|
|
if isinstance(arg, astroid.Tuple):
|
|
name = "({})".format(", ".join(x.name for x in arg.elts))
|
|
|
|
yield (name, format_annotation(annotation, arg.parent), default)
|
|
|
|
|
|
def get_args_info(args_node): # pylint: disable=too-many-branches,too-many-statements
|
|
result = []
|
|
positional_only_defaults = []
|
|
positional_or_keyword_defaults = args_node.defaults
|
|
if args_node.defaults:
|
|
args = args_node.args or []
|
|
positional_or_keyword_defaults = args_node.defaults[-len(args) :]
|
|
positional_only_defaults = args_node.defaults[
|
|
: len(args_node.defaults) - len(args)
|
|
]
|
|
|
|
plain_annotations = args_node.annotations or ()
|
|
func_comment_annotations = args_node.parent.type_comment_args or ()
|
|
comment_annotations = args_node.type_comment_posonlyargs
|
|
comment_annotations += args_node.type_comment_args or []
|
|
comment_annotations += args_node.type_comment_kwonlyargs
|
|
annotations = list(
|
|
merge_annotations(
|
|
plain_annotations,
|
|
merge_annotations(func_comment_annotations, comment_annotations),
|
|
)
|
|
)
|
|
annotation_offset = 0
|
|
|
|
if args_node.posonlyargs:
|
|
posonlyargs_annotations = args_node.posonlyargs_annotations
|
|
if not any(args_node.posonlyargs_annotations):
|
|
num_args = len(args_node.posonlyargs)
|
|
posonlyargs_annotations = annotations[
|
|
annotation_offset : annotation_offset + num_args
|
|
]
|
|
|
|
for arg, annotation, default in _iter_args(
|
|
args_node.posonlyargs, posonlyargs_annotations, positional_only_defaults
|
|
):
|
|
result.append((None, arg, annotation, default))
|
|
|
|
result.append(("/", None, None, None))
|
|
|
|
if not any(args_node.posonlyargs_annotations):
|
|
annotation_offset += num_args
|
|
|
|
if args_node.args:
|
|
num_args = len(args_node.args)
|
|
for arg, annotation, default in _iter_args(
|
|
args_node.args,
|
|
annotations[annotation_offset : annotation_offset + num_args],
|
|
positional_or_keyword_defaults,
|
|
):
|
|
result.append((None, arg, annotation, default))
|
|
|
|
annotation_offset += num_args
|
|
|
|
if args_node.vararg:
|
|
annotation = None
|
|
if args_node.varargannotation:
|
|
annotation = format_annotation(args_node.varargannotation, args_node.parent)
|
|
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
|
|
annotation = format_annotation(
|
|
annotations[annotation_offset], args_node.parent
|
|
)
|
|
annotation_offset += 1
|
|
result.append(("*", args_node.vararg, annotation, None))
|
|
|
|
if args_node.kwonlyargs:
|
|
if not args_node.vararg:
|
|
result.append(("*", None, None, None))
|
|
|
|
kwonlyargs_annotations = args_node.kwonlyargs_annotations
|
|
if not any(args_node.kwonlyargs_annotations):
|
|
num_args = len(args_node.kwonlyargs)
|
|
kwonlyargs_annotations = annotations[
|
|
annotation_offset : annotation_offset + num_args
|
|
]
|
|
|
|
for arg, annotation, default in _iter_args(
|
|
args_node.kwonlyargs,
|
|
kwonlyargs_annotations,
|
|
args_node.kw_defaults,
|
|
):
|
|
result.append((None, arg, annotation, default))
|
|
|
|
if not any(args_node.kwonlyargs_annotations):
|
|
annotation_offset += num_args
|
|
|
|
if args_node.kwarg:
|
|
annotation = None
|
|
if args_node.kwargannotation:
|
|
annotation = format_annotation(args_node.kwargannotation, args_node.parent)
|
|
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
|
|
annotation = format_annotation(
|
|
annotations[annotation_offset], args_node.parent
|
|
)
|
|
annotation_offset += 1
|
|
result.append(("**", args_node.kwarg, annotation, None))
|
|
|
|
return result
|
|
|
|
|
|
def get_return_annotation(node):
|
|
"""Get the return annotation of a node.
|
|
:type node: astroid.nodes.FunctionDef
|
|
"""
|
|
return_annotation = None
|
|
|
|
if node.returns:
|
|
return_annotation = format_annotation(node.returns, node)
|
|
elif node.type_comment_returns:
|
|
return_annotation = format_annotation(node.type_comment_returns, node)
|
|
|
|
return return_annotation
|
|
|
|
|
|
def get_func_docstring(node):
|
|
"""Get the docstring of a node, using a parent docstring if needed.
|
|
|
|
:param node: The node to get a docstring for.
|
|
:type node: astroid.nodes.FunctionDef
|
|
"""
|
|
doc = node.doc
|
|
|
|
if doc is None and isinstance(node.parent, astroid.nodes.ClassDef):
|
|
for base in node.parent.ancestors():
|
|
if node.name in ("__init__", "__new__") and base.qname() in (
|
|
"__builtins__.object",
|
|
"builtins.object",
|
|
"builtins.type",
|
|
):
|
|
continue
|
|
for child in base.get_children():
|
|
if (
|
|
isinstance(child, node.__class__)
|
|
and child.name == node.name
|
|
and child.doc is not None
|
|
):
|
|
return child.doc
|
|
|
|
return doc or ""
|
|
|
|
|
|
def get_class_docstring(node):
|
|
"""Get the docstring of a node, using a parent docstring if needed.
|
|
|
|
:param node: The node to get a docstring for.
|
|
:type node: astroid.nodes.ClassDef
|
|
"""
|
|
doc = node.doc
|
|
|
|
if doc is None:
|
|
for base in node.ancestors():
|
|
if base.qname() in (
|
|
"__builtins__.object",
|
|
"builtins.object",
|
|
"builtins.type",
|
|
):
|
|
continue
|
|
if base.doc is not None:
|
|
return base.doc
|
|
|
|
return doc or ""
|