127 lines
4.0 KiB
Python
127 lines
4.0 KiB
Python
"""Attribute docstrings parsing.
|
|
|
|
.. seealso:: https://peps.python.org/pep-0257/#what-is-a-docstring
|
|
"""
|
|
|
|
import ast
|
|
import inspect
|
|
import textwrap
|
|
import typing as T
|
|
from types import ModuleType
|
|
|
|
from .common import Docstring, DocstringParam
|
|
|
|
|
|
def ast_get_constant_value(node: ast.AST) -> T.Any:
|
|
"""Return the constant's value if the given node is a constant."""
|
|
return getattr(node, "value")
|
|
|
|
|
|
def ast_unparse(node: ast.AST) -> T.Optional[str]:
|
|
"""Convert the AST node to source code as a string."""
|
|
if hasattr(ast, "unparse"):
|
|
return ast.unparse(node)
|
|
# Support simple cases in Python < 3.9
|
|
if isinstance(node, ast.Constant):
|
|
return str(ast_get_constant_value(node))
|
|
if isinstance(node, ast.Name):
|
|
return node.id
|
|
return None
|
|
|
|
|
|
def ast_is_literal_str(node: ast.AST) -> bool:
|
|
"""Return True if the given node is a literal string."""
|
|
return (
|
|
isinstance(node, ast.Expr)
|
|
and isinstance(node.value, ast.Constant)
|
|
and isinstance(ast_get_constant_value(node.value), str)
|
|
)
|
|
|
|
|
|
def ast_get_attribute(
|
|
node: ast.AST,
|
|
) -> T.Optional[T.Tuple[str, T.Optional[str], T.Optional[str]]]:
|
|
"""Return name, type and default if the given node is an attribute."""
|
|
if isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
target = (
|
|
node.targets[0] if isinstance(node, ast.Assign) else node.target
|
|
)
|
|
if isinstance(target, ast.Name):
|
|
type_str = None
|
|
if isinstance(node, ast.AnnAssign):
|
|
type_str = ast_unparse(node.annotation)
|
|
default = None
|
|
if node.value:
|
|
default = ast_unparse(node.value)
|
|
return target.id, type_str, default
|
|
return None
|
|
|
|
|
|
class AttributeDocstrings(ast.NodeVisitor):
|
|
"""An ast.NodeVisitor that collects attribute docstrings."""
|
|
|
|
attr_docs = None
|
|
prev_attr = None
|
|
|
|
def visit(self, node):
|
|
if self.prev_attr and ast_is_literal_str(node):
|
|
attr_name, attr_type, attr_default = self.prev_attr
|
|
self.attr_docs[attr_name] = (
|
|
ast_get_constant_value(node.value),
|
|
attr_type,
|
|
attr_default,
|
|
)
|
|
self.prev_attr = ast_get_attribute(node)
|
|
if isinstance(node, (ast.ClassDef, ast.Module)):
|
|
self.generic_visit(node)
|
|
|
|
def get_attr_docs(
|
|
self, component: T.Any
|
|
) -> T.Dict[str, T.Tuple[str, T.Optional[str], T.Optional[str]]]:
|
|
"""Get attribute docstrings from the given component.
|
|
|
|
:param component: component to process (class or module)
|
|
:returns: for each attribute docstring, a tuple with (description,
|
|
type, default)
|
|
"""
|
|
self.attr_docs = {}
|
|
self.prev_attr = None
|
|
try:
|
|
source = textwrap.dedent(inspect.getsource(component))
|
|
except OSError:
|
|
pass
|
|
else:
|
|
tree = ast.parse(source)
|
|
if inspect.ismodule(component):
|
|
self.visit(tree)
|
|
elif isinstance(tree, ast.Module) and isinstance(
|
|
tree.body[0], ast.ClassDef
|
|
):
|
|
self.visit(tree.body[0])
|
|
return self.attr_docs
|
|
|
|
|
|
def add_attribute_docstrings(
|
|
obj: T.Union[type, ModuleType], docstring: Docstring
|
|
) -> None:
|
|
"""Add attribute docstrings found in the object's source code.
|
|
|
|
:param obj: object from which to parse attribute docstrings
|
|
:param docstring: Docstring object where found attributes are added
|
|
:returns: list with names of added attributes
|
|
"""
|
|
params = set(p.arg_name for p in docstring.params)
|
|
for arg_name, (description, type_name, default) in (
|
|
AttributeDocstrings().get_attr_docs(obj).items()
|
|
):
|
|
if arg_name not in params:
|
|
param = DocstringParam(
|
|
args=["attribute", arg_name],
|
|
description=description,
|
|
arg_name=arg_name,
|
|
type_name=type_name,
|
|
is_optional=default is not None,
|
|
default=default,
|
|
)
|
|
docstring.meta.append(param)
|