def __init__(self, atok, lino, node, origin_file): """ Initialize a new tree node instance. Arguments: atok -- asttokens object containing the tokens for the AST lino - asttokens LineNumber object containing the conversion for offsets to line numbers node -- Single raw node produced by the Python AST parser. origin_file {string} -- Relative path to the source file. """ self.node = node start, end = atok.get_text_range(node) start_line, start_char = LineNumbers.offset_to_line(lino, start) end_line, end_char = LineNumbers.offset_to_line(lino, end) self.origin = NodeOrigin(origin_file, start_line, end_line) # Check if this type of node can have docstring. can_have_docstring = node.__class__ in [ast.ClassDef, ast.FunctionDef] # HACK: Ignore useless context-related children. # This should greatly reduce the total number of nodes. self.children = [ TreeNode(atok, lino, n, origin_file) for n in ast.iter_child_nodes(node) if n.__class__ not in _IGNORE_CLASSES and # Ignore docstrings in class and function definitions. (not can_have_docstring or not isinstance(n, ast.Expr) or not isinstance(n.value, ast.Str)) ] self.weight = 1 + sum([c.weight for c in self.children]) # Name nodes are handled in a special way. if isinstance(node, ast.Name): self.value = f"Name('{node.id}')" self.names = [node.id] else: # Class name if the node has children, AST dump if it does not. self.value = node.__class__.__name__ if self.children else self.dump( ) self.names = [] for c in self.children: self.names.extend(c.names) # These values are set externally after all nodes are parsed # during the node tree flattening process. self.index = None self.parent_index = None self.child_indices = []
def _get_tree_node_from_file(file_path, repo_path): """ Parse a TreeNode representing the module in the specified file. Arguments: file_path {string} -- Path of file to parse the TreeNode from. Returns: TreeNode -- TreeNode parsed from the specified file. """ atok = asttokens.ASTTokens(_read_whole_file(file_path), True) root = atok.tree lino = LineNumbers(atok.get_text(root)) return TreeNode(atok, lino, root, relpath(file_path, repo_path))
def _parse_docstring( source: str, docstring: str, invalid_fields: Tuple, params: Optional[Tuple] = None, return_length: int = 0, ) -> dict: natspec: dict = {} if params is None: params = tuple() line_no = LineNumbers(source) start = source.index(docstring) translate_map = { "return": "returns", "dev": "details", "param": "params", } pattern = r"(?:^|\n)\s*@(\S+)\s*([\s\S]*?)(?=\n\s*@\S|\s*$)" for match in re.finditer(pattern, docstring): tag, value = match.groups() err_args = (source, *line_no.offset_to_line(start + match.start(1))) if tag not in SINGLE_FIELDS + PARAM_FIELDS: raise NatSpecSyntaxException(f"Unknown NatSpec field '@{tag}'", *err_args) if tag in invalid_fields: raise NatSpecSyntaxException( f"'@{tag}' is not a valid field for this docstring", *err_args) if not value or value.startswith("@"): raise NatSpecSyntaxException( f"No description given for tag '@{tag}'", *err_args) if tag not in PARAM_FIELDS: if tag in natspec: raise NatSpecSyntaxException( f"Duplicate NatSpec field '@{tag}'", *err_args) natspec[translate_map.get(tag, tag)] = " ".join(value.split()) continue tag = translate_map.get(tag, tag) natspec.setdefault(tag, {}) if tag == "params": try: key, value = value.split(maxsplit=1) except ValueError as exc: raise NatSpecSyntaxException( f"No description given for parameter '{value}'", *err_args) from exc if key not in params: raise NatSpecSyntaxException( f"Method has no parameter '{key}'", *err_args) elif tag == "returns": if not return_length: raise NatSpecSyntaxException( "Method does not return any values", *err_args) if len(natspec["returns"]) >= return_length: raise NatSpecSyntaxException( "Number of documented return values exceeds actual number", *err_args, ) key = f"_{len(natspec['returns'])}" if key in natspec[tag]: raise NatSpecSyntaxException( f"Parameter '{key}' documented more than once", *err_args) natspec[tag][key] = " ".join(value.split()) if not natspec: natspec["notice"] = " ".join(docstring.split()) elif not docstring.strip().startswith("@"): raise NatSpecSyntaxException( "NatSpec docstring opens with untagged comment", source, *line_no.offset_to_line(start), ) return natspec