sphinx-autoapi/autoapi/mappers/python/astroid_utils.py

468 lines
14 KiB
Python
Raw Normal View History

2017-09-12 17:59:21 +00:00
try:
import builtins
except ImportError:
import __builtin__ as builtins
2019-08-08 06:23:21 +00:00
import itertools
2017-09-12 17:59:21 +00:00
import re
import sys
2017-09-12 17:59:21 +00:00
import astroid
import astroid.nodes
if sys.version_info < (3,):
_EXCEPTIONS_MODULE = "exceptions"
2018-08-20 18:01:07 +00:00
# getattr to keep linter happy
2019-01-27 05:20:45 +00:00
_STRING_TYPES = getattr(builtins, "basestring")
2019-10-05 22:11:23 +00:00
_zip_longest = itertools.izip_longest # pylint: disable=invalid-name,no-member
else:
_EXCEPTIONS_MODULE = "builtins"
2019-10-05 22:11:23 +00:00
_STRING_TYPES = (str,)
_zip_longest = itertools.zip_longest # pylint: disable=invalid-name
2017-09-12 17:59:21 +00:00
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.
2017-09-12 17:59:21 +00:00
: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(
2019-01-27 05:20:45 +00:00
import_from.modname, level=import_from.level
2017-09-12 17:59:21 +00:00
)
2019-01-27 05:20:45 +00:00
return "{}.{}".format(module_name, partial_basename)
2017-09-12 17:59:21 +00:00
def get_full_basename(node, basename):
"""Resolve a partial base name to the full path.
: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
2019-01-27 05:20:45 +00:00
top_level_name = re.sub(r"\(.*\)", "", basename).split(".", 1)[0]
2017-09-12 17:59:21 +00:00
lookup_node = node
2019-01-27 05:20:45 +00:00
while not hasattr(lookup_node, "lookup"):
2017-09-12 17:59:21 +00:00
lookup_node = lookup_node.parent
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
2019-10-05 22:11:23 +00:00
if isinstance(assignment, astroid.nodes.Import):
2017-09-12 17:59:21 +00:00
import_name = resolve_import_alias(top_level_name, assignment.names)
full_basename = basename.replace(top_level_name, import_name, 1)
break
2019-10-05 22:11:23 +00:00
if isinstance(assignment, astroid.nodes.ClassDef):
2019-01-27 05:20:45 +00:00
full_basename = "{}.{}".format(assignment.root().name, assignment.name)
break
2017-09-12 17:59:21 +00:00
if isinstance(node, astroid.nodes.Call):
2019-01-27 05:20:45 +00:00
full_basename = re.sub(r"\(.*\)", "()", full_basename)
2017-09-12 17:59:21 +00:00
2019-01-27 05:20:45 +00:00
if full_basename.startswith("builtins."):
return full_basename[len("builtins.") :]
2018-08-14 00:26:54 +00:00
2019-01-27 05:20:45 +00:00
if full_basename.startswith("__builtin__."):
return full_basename[len("__builtin__.") :]
2018-08-14 00:26:54 +00:00
2017-09-12 17:59:21 +00:00
return full_basename
def get_full_basenames(bases, basenames):
"""Resolve the base nodes and partial names of a class to full names.
:param bases: The astroid node representing something that a class
inherits from.
:type bases: iterable(astroid.NodeNG)
:param basenames: The partial name of something that a class inherits from.
:type basenames: iterable(str)
:returns: The full names.
:rtype: iterable(str)
"""
for base, basename in zip(bases, basenames):
yield get_full_basename(base, basename)
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
2017-09-12 17:59:21 +00:00
: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]
2017-09-12 17:59:21 +00:00
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.
2019-10-05 22:11:23 +00:00
:rtype: str or None
"""
annotation = None
annotation_node = None
try:
annotation_node = node.annotation
except AttributeError:
# Python 2 has no support for type annotations, so use getattr
annotation_node = getattr(node, "type_annotation", None)
if annotation_node:
if isinstance(annotation_node, astroid.nodes.Const):
annotation = node.value
else:
annotation = annotation_node.as_string()
return annotation
2017-09-12 17:59:21 +00:00
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 (
2019-01-27 05:20:45 +00:00
class_node.name == "property"
2017-09-12 17:59:21 +00:00
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:
2019-01-27 05:20:45 +00:00
if (
isinstance(decorator, astroid.nodes.Attribute)
and decorator.attrname == "setter"
):
2017-09-12 17:59:21 +00:00
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 contructor, False otherwise.
:rtype: bool
"""
return (
node.parent
and isinstance(node.parent.scope(), astroid.nodes.ClassDef)
2019-01-27 05:20:45 +00:00
and node.name == "__init__"
2017-09-12 17:59:21 +00:00
)
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
"""
2019-01-27 05:20:45 +00:00
if (
node.name in ("Exception", "BaseException")
and node.root().name == _EXCEPTIONS_MODULE
):
return True
2019-01-27 05:20:45 +00:00
if not hasattr(node, "ancestors"):
return False
2019-01-27 05:20:45 +00:00
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
2019-01-27 05:20:45 +00:00
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
2019-01-27 05:20:45 +00:00
if "__all__" in node.locals:
assigned = next(node.igetattr("__all__"))
if assigned is not astroid.Uninferable:
all_ = []
2019-01-27 05:20:45 +00:00
for elt in getattr(assigned, "elts", ()):
try:
elt_name = next(elt.infer())
except astroid.InferenceError:
continue
if elt_name is astroid.Uninferable:
continue
2019-01-27 05:20:45 +00:00
if isinstance(elt_name, astroid.Const) and isinstance(
elt_name.value, _STRING_TYPES
):
all_.append(elt_name.value)
return all_
2019-08-08 06:23:21 +00:00
def merge_annotations(annotations, comment_annotations):
for ann, comment_ann in _zip_longest(annotations, comment_annotations):
if not ann or isinstance(ann, astroid.Ellipsis):
yield comment_ann
else:
yield ann
def _format_args(args, defaults=None, annotations=None):
values = []
if args is None:
return ""
if annotations is None:
annotations = []
if defaults is not None:
default_offset = len(args) - len(defaults)
packed = _zip_longest(args, annotations)
for i, (arg, annotation) in enumerate(packed):
if isinstance(arg, astroid.Tuple):
values.append("({})".format(_format_args(arg.elts)))
else:
argname = arg.name
default_sep = "="
if annotation is not None:
ann_str = annotation.as_string()
if isinstance(annotation, astroid.Const):
ann_str = annotation.value
argname = "{}: {}".format(argname, ann_str)
default_sep = " = "
values.append(argname)
if defaults is not None and i >= default_offset:
if defaults[i - default_offset] is not None:
values[-1] += default_sep + defaults[i - default_offset].as_string()
return ", ".join(values)
def format_args(args_node):
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 = getattr(args_node, "annotations", ()) or ()
func_comment_annotations = getattr(args_node.parent, "type_comment_args", ()) or ()
comment_annotations = getattr(args_node, "type_comment_args", ()) or ()
annotations = list(
merge_annotations(
plain_annotations,
merge_annotations(func_comment_annotations, comment_annotations),
)
)
if getattr(args_node, "posonlyargs", None):
result.append(_format_args(args_node.posonlyargs, positional_only_defaults))
result.append("/")
if args_node.args:
result.append(
_format_args(args_node.args, positional_or_keyword_defaults, annotations)
)
if args_node.vararg:
vararg_result = "*{}".format(args_node.vararg)
if getattr(args_node, "varargannotation", None):
vararg_result = "{}: {}".format(
vararg_result, args_node.varargannotation.as_string()
)
result.append(vararg_result)
if getattr(args_node, "kwonlyargs", None):
if not args_node.vararg:
result.append("*")
result.append(
_format_args(
args_node.kwonlyargs,
args_node.kw_defaults,
args_node.kwonlyargs_annotations,
)
)
if args_node.kwarg:
kwarg_result = "**{}".format(args_node.kwarg)
if getattr(args_node, "kwargannotation", None):
kwarg_result = "{}: {}".format(
kwarg_result, args_node.kwargannotation.as_string()
)
result.append(kwarg_result)
return ", ".join(result)