1111import difflib
1212import functools
1313import pathlib
14+ import re
1415import shelve
1516import subprocess
1617
3132# === Import parsing utilities ===
3233
3334
34- class ImportVisitor ( ast . NodeVisitor ) :
35- def __init__ ( self ) -> None :
36- self . test_imports = set ( )
37- self . lib_imports = set ( )
35+ def _extract_top_level_code ( content : str ) -> str :
36+ """Extract only top-level code from Python content for faster parsing."""
37+ def_idx = content . find ( " \n def " )
38+ class_idx = content . find ( " \n class " )
3839
39- def add_import (self , name : str ) -> None :
40- """
41- Add an `import` to its correct slot (`test_imports` or `lib_imports`).
40+ indices = [i for i in (def_idx , class_idx ) if i != - 1 ]
41+ if indices :
42+ content = content [: min (indices )]
43+ return content .rstrip ("\n " )
4244
43- Parameters
44- ----------
45- name : str
46- Module name.
47- """
48- if name .startswith ("test.support" ):
49- return
5045
51- real_name = name .split ("." , 1 )[- 1 ]
52- if name .startswith ("test." ):
53- self .test_imports .add (real_name )
54- else :
55- self .lib_imports .add (real_name )
56-
57- def visit_Import (self , node ):
58- for alias in node .names :
59- self .add_import (alias .name )
60-
61- def visit_ImportFrom (self , node ):
62- try :
63- module = node .module
64- except AttributeError :
65- # Ignore `from . import my_internal_module`
66- return
67-
68- for name in node .names :
69- self .add_import (f"{ module } .{ name } " )
70-
71- def visit_Call (self , node ) -> None :
72- """
73- In test files, there's sometimes use of:
74-
75- ```python
76- import test.support
77- from test.support import script_helper
78-
79- script = support.findfile("_test_atexit.py")
80- script_helper.run_test_script(script)
81- ```
82-
83- This imports "_test_atexit.py" but does not show as an import node.
84- """
85- func = node .func
86- if not isinstance (func , ast .Attribute ):
87- return
46+ _FROM_TEST_IMPORT_RE = re .compile (r"^from test import (.+)" , re .MULTILINE )
47+ _FROM_TEST_DOT_RE = re .compile (r"^from test\.(\w+)" , re .MULTILINE )
48+ _IMPORT_TEST_DOT_RE = re .compile (r"^import test\.(\w+)" , re .MULTILINE )
8849
89- value = func .value
90- if not isinstance (value , ast .Name ):
91- return
9250
93- if (value .id != "support" ) or (func .attr != "findfile" ):
94- return
51+ def parse_test_imports (content : str ) -> set [str ]:
52+ """Parse test file content and extract test package dependencies."""
53+ content = _extract_top_level_code (content )
54+ imports = set ()
9555
96- arg = node .args [0 ]
97- if not isinstance (arg , ast .Constant ):
98- return
56+ for match in _FROM_TEST_IMPORT_RE .finditer (content ):
57+ import_list = match .group (1 )
58+ for part in import_list .split ("," ):
59+ name = part .split ()[0 ].strip ()
60+ if name and name not in ("support" , "__init__" ):
61+ imports .add (name )
9962
100- target = arg .value
101- if not target .endswith (".py" ):
102- return
63+ for match in _FROM_TEST_DOT_RE .finditer (content ):
64+ dep = match .group (1 )
65+ if dep not in ("support" , "__init__" ):
66+ imports .add (dep )
10367
104- target = target .removesuffix (".py" )
105- self .add_import (f"test.{ target } " )
68+ for match in _IMPORT_TEST_DOT_RE .finditer (content ):
69+ dep = match .group (1 )
70+ if dep not in ("support" , "__init__" ):
71+ imports .add (dep )
10672
73+ return imports
10774
108- def parse_test_imports (content : str ) -> set [str ]:
109- """Parse test file content and extract test package dependencies."""
110- if not (tree := safe_parse_ast (content )):
111- return set ()
11275
113- visitor = ImportVisitor ()
114- visitor .visit (tree )
115- return visitor .test_imports
76+ _IMPORT_RE = re .compile (r"^import\s+(\w[\w.]*)" , re .MULTILINE )
77+ _FROM_IMPORT_RE = re .compile (r"^from\s+(\w[\w.]*)\s+import" , re .MULTILINE )
11678
11779
11880def parse_lib_imports (content : str ) -> set [str ]:
11981 """Parse library file and extract all imported module names."""
120- if not (tree := safe_parse_ast (content )):
121- return set ()
82+ imports = set ()
12283
123- visitor = ImportVisitor ()
124- visitor .visit (tree )
125- return visitor .lib_imports
84+ for match in _IMPORT_RE .finditer (content ):
85+ imports .add (match .group (1 ))
86+
87+ for match in _FROM_IMPORT_RE .finditer (content ):
88+ imports .add (match .group (1 ))
89+
90+ return imports
12691
12792
12893# === TODO marker utilities ===
@@ -139,7 +104,7 @@ def filter_rustpython_todo(content: str) -> str:
139104
140105def count_rustpython_todo (content : str ) -> int :
141106 """Count lines containing RustPython TODO markers."""
142- return content .count ( TODO_MARKER )
107+ return sum ( 1 for line in content .splitlines () if TODO_MARKER in line )
143108
144109
145110def count_todo_in_path (path : pathlib .Path ) -> int :
@@ -148,7 +113,10 @@ def count_todo_in_path(path: pathlib.Path) -> int:
148113 content = safe_read_text (path )
149114 return count_rustpython_todo (content ) if content else 0
150115
151- return sum (count_rustpython_todo (content ) for _ , content in read_python_files (path ))
116+ total = 0
117+ for _ , content in read_python_files (path ):
118+ total += count_rustpython_todo (content )
119+ return total
152120
153121
154122# === Test utilities ===
0 commit comments