Coverage for archdocs/syntax_tree.py: 100%
26 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-09 14:41 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-09 14:41 +0000
1"""Reading a source through its syntax tree instead of through a regex.
3The tree answers what a pattern can only guess at: whether a name is a call or a word in a
4comment, which keyword belongs to which call, where a string starts and ends. The price is that a
5file the running interpreter cannot parse gives no tree at all, so a feature read this way loses
6that file, where a regex would still have matched something in it.
7"""
9import ast
10import typing
11import warnings
14# The parse runs on every file of somebody's project, and an invalid escape in a regex literal is
15# a `SyntaxWarning` printed into their logs on each scan: the filter is narrowed to the pseudo
16# file name below, so warnings of the service's own code are left as they are.
17_PARSED_FILE_NAME: typing.Final = "<archdocs>"
18warnings.filterwarnings("ignore", category=SyntaxWarning, module=_PARSED_FILE_NAME)
21def parse_syntax_tree(raw_source: str, /) -> ast.Module | None:
22 try:
23 return ast.parse(raw_source, filename=_PARSED_FILE_NAME)
24 except (SyntaxError, ValueError, RecursionError):
25 return None
28# `ast.walk` goes breadth first, and a feature that takes the first match has to take the one
29# written first, the way a regex search does.
30def list_nodes_in_source_order(syntax_tree: ast.Module, /) -> list[ast.AST]:
31 return sorted(
32 ast.walk(syntax_tree),
33 key=lambda one_node: (getattr(one_node, "lineno", 0), getattr(one_node, "col_offset", 0)),
34 )
37def read_callee_name(one_call: ast.Call, /) -> str:
38 match one_call.func:
39 case ast.Name(id=callee_name) | ast.Attribute(attr=callee_name):
40 return callee_name
41 case _:
42 return ""
45def read_imported_modules(one_node: ast.AST, /) -> tuple[str, ...]:
46 match one_node:
47 case ast.ImportFrom(module=str() as module_name):
48 return (module_name,)
49 case ast.Import(names=every_alias):
50 return tuple(one_alias.name for one_alias in every_alias)
51 case _:
52 return ()