You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sphinx-autoapi/autoapi/_parser.py

345 lines
11 KiB
Python

import collections
import itertools
import os
import astroid
import astroid.builder
import sphinx.util.docstrings
from . import _astroid_utils
def _prepare_docstring(doc):
return "\n".join(sphinx.util.docstrings.prepare_docstring(doc))
class Parser:
def __init__(self):
self._qual_name_stack = []
self._full_name_stack = []
self._encoding = None
def _get_qual_name(self, name):
return ".".join(self._qual_name_stack + [name])
def _get_full_name(self, name):
return ".".join(self._full_name_stack + [name])
def _parse_file(self, file_path, condition):
directory, filename = os.path.split(file_path)
module_parts = []
if filename != "__init__.py" and filename != "__init__.pyi":
module_part = os.path.splitext(filename)[0]
module_parts = [module_part]
module_parts = collections.deque(module_parts)
while directory and condition(directory):
directory, module_part = os.path.split(directory)
if module_part:
module_parts.appendleft(module_part)
module_name = ".".join(module_parts)
node = astroid.builder.AstroidBuilder().file_build(file_path, module_name)
return self.parse(node)
def parse_file(self, file_path):
return self._parse_file(
file_path,
lambda directory: (
os.path.isfile(os.path.join(directory, "__init__.py"))
or os.path.isfile(os.path.join(directory, "__init__.pyi"))
),
)
def parse_file_in_namespace(self, file_path, dir_root):
return self._parse_file(
file_path, lambda directory: os.path.abspath(directory) != dir_root
)
def parse_annassign(self, node):
# Don't document module level assignments to class attributes
if isinstance(node.target, astroid.nodes.AssignAttr):
return []
return self._parse_assign(node)
def parse_assign(self, node):
# Don't document module level assignments to class attributes
if any(isinstance(target, astroid.nodes.AssignAttr) for target in node.targets):
return []
return self._parse_assign(node)
def _parse_assign(self, node):
doc = ""
doc_node = node.next_sibling()
if isinstance(doc_node, astroid.nodes.Expr) and isinstance(
doc_node.value, astroid.nodes.Const
):
doc = doc_node.value.value
type_ = "data"
if isinstance(
node.scope(), astroid.nodes.ClassDef
) or _astroid_utils.is_constructor(node.scope()):
type_ = "attribute"
assign_value = _astroid_utils.get_assign_value(node)
if not assign_value:
return []
target = assign_value[0]
value = assign_value[1]
annotation = _astroid_utils.get_assign_annotation(node)
if annotation in ("TypeAlias", "typing.TypeAlias"):
value = node.value.as_string()
data = {
"type": type_,
"name": target,
"qual_name": self._get_qual_name(target),
"full_name": self._get_full_name(target),
"doc": _prepare_docstring(doc),
"value": value,
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"annotation": annotation,
}
return [data]
def parse_classdef(self, node, data=None):
type_ = "class"
if _astroid_utils.is_exception(node):
type_ = "exception"
basenames = list(_astroid_utils.get_full_basenames(node))
data = {
"type": type_,
"name": node.name,
"qual_name": self._get_qual_name(node.name),
"full_name": self._get_full_name(node.name),
"bases": basenames,
"doc": _prepare_docstring(_astroid_utils.get_class_docstring(node)),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"children": [],
}
self._qual_name_stack.append(node.name)
self._full_name_stack.append(node.name)
overridden = set()
overloads = {}
for base in itertools.chain(iter((node,)), node.ancestors()):
seen = set()
if base.qname() in (
"__builtins__.object",
"builtins.object",
"builtins.type",
):
continue
for child in base.get_children():
name = getattr(child, "name", None)
if isinstance(child, (astroid.Assign, astroid.AnnAssign)):
assign_value = _astroid_utils.get_assign_value(child)
if not assign_value:
continue
name = assign_value[0]
if not name or name in overridden:
continue
seen.add(name)
child_data = self.parse(child)
data["children"].extend(
_parse_child(node, child_data, overloads, base, name)
)
overridden.update(seen)
self._qual_name_stack.pop()
self._full_name_stack.pop()
return [data]
def parse_asyncfunctiondef(self, node):
return self.parse_functiondef(node)
def parse_functiondef(self, node):
if _astroid_utils.is_decorated_with_property_setter(node):
return []
type_ = "method"
properties = []
if node.type == "function":
type_ = "function"
if isinstance(node, astroid.AsyncFunctionDef):
properties.append("async")
elif _astroid_utils.is_decorated_with_property(node):
type_ = "property"
if node.type == "classmethod":
properties.append(node.type)
if node.is_abstract(pass_is_abstract=False):
properties.append("abstractmethod")
else:
# "__new__" method is implicit classmethod
if node.type in ("staticmethod", "classmethod") and node.name != "__new__":
properties.append(node.type)
if node.is_abstract(pass_is_abstract=False):
properties.append("abstractmethod")
if isinstance(node, astroid.AsyncFunctionDef):
properties.append("async")
data = {
"type": type_,
"name": node.name,
"qual_name": self._get_qual_name(node.name),
"full_name": self._get_full_name(node.name),
"args": _astroid_utils.get_args_info(node.args),
"doc": _prepare_docstring(_astroid_utils.get_func_docstring(node)),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"return_annotation": _astroid_utils.get_return_annotation(node),
"properties": properties,
"is_overload": _astroid_utils.is_decorated_with_overload(node),
"overloads": [],
}
result = [data]
if node.name == "__init__":
for child in node.get_children():
if isinstance(child, (astroid.nodes.Assign, astroid.nodes.AnnAssign)):
child_data = self._parse_assign(child)
result.extend(data for data in child_data if data["doc"])
return result
def _parse_local_import_from(self, node):
result = []
for import_name, alias in node.names:
is_wildcard = (alias or import_name) == "*"
original_path = _astroid_utils.get_full_import_name(
node, alias or import_name
)
name = original_path if is_wildcard else (alias or import_name)
qual_name = self._get_qual_name(alias or import_name)
full_name = self._get_full_name(alias or import_name)
data = {
"type": "placeholder",
"name": name,
"qual_name": qual_name,
"full_name": full_name,
"original_path": original_path,
}
result.append(data)
return result
def parse_module(self, node):
path = node.path
if isinstance(node.path, list):
path = node.path[0] if node.path else None
type_ = "module"
if node.package:
type_ = "package"
self._full_name_stack = [node.name]
self._encoding = node.file_encoding
data = {
"type": type_,
"name": node.name,
"qual_name": node.name,
"full_name": node.name,
"doc": _prepare_docstring(node.doc_node.value if node.doc_node else ""),
"children": [],
"file_path": path,
"encoding": node.file_encoding,
"all": _astroid_utils.get_module_all(node),
}
overloads = {}
top_name = node.name.split(".", 1)[0]
for child in node.get_children():
if _astroid_utils.is_local_import_from(child, top_name):
child_data = self._parse_local_import_from(child)
else:
child_data = self.parse(child)
data["children"].extend(_parse_child(node, child_data, overloads))
return data
def parse_typealias(self, node):
doc = ""
doc_node = node.next_sibling()
if isinstance(doc_node, astroid.nodes.Expr) and isinstance(
doc_node.value, astroid.nodes.Const
):
doc = doc_node.value.value
if isinstance(node.name, astroid.nodes.AssignName):
name = node.name.name
elif isinstance(node.name, astroid.nodes.AssignAttr):
name = node.name.attrname
else:
return []
data = {
"type": "data",
"name": name,
"qual_name": self._get_qual_name(name),
"full_name": self._get_full_name(name),
"doc": _prepare_docstring(doc),
"value": node.value.as_string(),
"from_line_no": node.fromlineno,
"to_line_no": node.tolineno,
"annotation": "TypeAlias",
}
return [data]
def parse(self, node):
data = {}
node_type = node.__class__.__name__.lower()
parse_func = getattr(self, "parse_" + node_type, None)
if parse_func:
data = parse_func(node)
else:
for child in node.get_children():
data = self.parse(child)
if data:
break
return data
def _parse_child(node, child_data, overloads, base=None, name=None):
result = []
for single_data in child_data:
if single_data["type"] in ("function", "method", "property"):
if name is None:
name = single_data["name"]
if name in overloads:
grouped = overloads[name]
grouped["doc"] = single_data["doc"]
if single_data["is_overload"]:
grouped["overloads"].append(
(single_data["args"], single_data["return_annotation"])
)
continue
if single_data["is_overload"] and name not in overloads:
overloads[name] = single_data
if base:
single_data["inherited"] = base is not node
result.append(single_data)
return result