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

1"""Reading a source through its syntax tree instead of through a regex. 

2 

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""" 

8 

9import ast 

10import typing 

11import warnings 

12 

13 

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) 

19 

20 

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 

26 

27 

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 ) 

35 

36 

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 "" 

43 

44 

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 ()