Path: blob/main/singlestoredb/docstring/attrdoc.py
469 views
"""Attribute docstrings parsing.12.. seealso:: https://peps.python.org/pep-0257/#what-is-a-docstring3"""4import ast5import inspect6import textwrap7import typing as T8from types import ModuleType910from .common import Docstring11from .common import DocstringParam121314def ast_get_constant_value(node: ast.AST) -> T.Any:15"""Return the constant's value if the given node is a constant."""16return getattr(node, 'value')171819def ast_unparse(node: ast.AST) -> T.Optional[str]:20"""Convert the AST node to source code as a string."""21if hasattr(ast, 'unparse'):22return ast.unparse(node)23# Support simple cases in Python < 3.924if isinstance(node, ast.Constant):25return str(ast_get_constant_value(node))26if isinstance(node, ast.Name):27return node.id28return None293031def ast_is_literal_str(node: ast.AST) -> bool:32"""Return True if the given node is a literal string."""33return (34isinstance(node, ast.Expr)35and isinstance(node.value, ast.Constant)36and isinstance(ast_get_constant_value(node.value), str)37)383940def ast_get_attribute(41node: ast.AST,42) -> T.Optional[T.Tuple[str, T.Optional[str], T.Optional[str]]]:43"""Return name, type and default if the given node is an attribute."""44if isinstance(node, (ast.Assign, ast.AnnAssign)):45target = (46node.targets[0] if isinstance(node, ast.Assign) else node.target47)48if isinstance(target, ast.Name):49type_str = None50if isinstance(node, ast.AnnAssign):51type_str = ast_unparse(node.annotation)52default = None53if node.value:54default = ast_unparse(node.value)55return target.id, type_str, default56return None575859class AttributeDocstrings(ast.NodeVisitor):60"""An ast.NodeVisitor that collects attribute docstrings."""6162attr_docs: T.Dict[str, T.Tuple[str, T.Optional[str], T.Optional[str]]] = {}63prev_attr = None6465def visit(self, node: T.Any) -> None:66if self.prev_attr and ast_is_literal_str(node):67attr_name, attr_type, attr_default = self.prev_attr68self.attr_docs[attr_name] = (69ast_get_constant_value(node.value),70attr_type,71attr_default,72)73self.prev_attr = ast_get_attribute(node)74if isinstance(node, (ast.ClassDef, ast.Module)):75self.generic_visit(node)7677def get_attr_docs(78self, component: T.Any,79) -> T.Dict[str, T.Tuple[str, T.Optional[str], T.Optional[str]]]:80"""Get attribute docstrings from the given component.8182:param component: component to process (class or module)83:returns: for each attribute docstring, a tuple with (description,84type, default)85"""86self.attr_docs = {}87self.prev_attr = None88try:89source = textwrap.dedent(inspect.getsource(component))90except OSError:91pass92else:93tree = ast.parse(source)94if inspect.ismodule(component):95self.visit(tree)96elif isinstance(tree, ast.Module) and isinstance(97tree.body[0], ast.ClassDef,98):99self.visit(tree.body[0])100return self.attr_docs101102103def add_attribute_docstrings(104obj: T.Union[type, ModuleType], docstring: Docstring,105) -> None:106"""Add attribute docstrings found in the object's source code.107108:param obj: object from which to parse attribute docstrings109:param docstring: Docstring object where found attributes are added110:returns: list with names of added attributes111"""112params = set(p.arg_name for p in docstring.params)113for arg_name, (description, type_name, default) in (114AttributeDocstrings().get_attr_docs(obj).items()115):116if arg_name not in params:117param = DocstringParam(118args=['attribute', arg_name],119description=description,120arg_name=arg_name,121type_name=type_name,122is_optional=default is not None,123default=default,124)125docstring.meta.append(param)126127128