ClickUp Operator

"""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 ast_constant_attr = { ast.Constant: "value", # python <= 3.7: ast.NameConstant: "value", ast.Num: "n", ast.Str: "s", } 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, ast_constant_attr[node.__class__]) 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.Str, ast.Num, ast.NameConstant, 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, ast.Str)) 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)