1111import difflib
1212import functools
1313import pathlib
14- import re
1514import shelve
1615import subprocess
1716
3231# === Import parsing utilities ===
3332
3433
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 " )
34+ class ImportVisitor (ast .NodeVisitor ):
35+ def __init__ (self ) -> None :
36+ self .__imports = set ()
3937
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 " )
38+ @property
39+ def test_imports (self ) -> frozenset [str ]:
40+ imports = set ()
41+ for module in self .__imports :
42+ if not module .startswith ("test." ):
43+ continue
44+ name = module .removeprefix ("test." )
4445
46+ if name == "support" or name .startswith ("support." ):
47+ continue
4548
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 )
49+ imports .add (name )
4950
51+ return frozenset (imports )
5052
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 ()
53+ @property
54+ def lib_imports (self ) -> frozenset [str ]:
55+ return frozenset (
56+ # module.split(".", 1)[0]
57+ module
58+ for module in self .__imports
59+ if not module .startswith ("test." )
60+ )
5561
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 )
62+ def visit_Import (self , node ):
63+ for alias in node .names :
64+ self .__imports .add (alias .name )
6265
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 )
66+ def visit_ImportFrom (self , node ):
67+ try :
68+ module = node .module
69+ except AttributeError :
70+ # Ignore `from . import my_internal_module`
71+ return
6772
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 )
73+ if module is None : # Ignore `from . import my_internal_module`
74+ return
7275
73- return imports
76+ for alias in node .names :
77+ # We only care about what we import if it was from the "test" module
78+ if module == "test" :
79+ name = f"{ module } .{ alias .name } "
80+ else :
81+ name = module
7482
83+ self .__imports .add (name )
7584
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 )
85+ def visit_Call (self , node ) -> None :
86+ """
87+ In test files, there's sometimes use of:
7888
89+ ```python
90+ import test.support
91+ from test.support import script_helper
7992
80- def parse_lib_imports (content : str ) -> set [str ]:
81- """Parse library file and extract all imported module names."""
82- imports = set ()
93+ script = support.findfile("_test_atexit.py")
94+ script_helper.run_test_script(script)
95+ ```
96+
97+ This imports "_test_atexit.py" but does not show as an import node.
98+ """
99+ func = node .func
100+ if not isinstance (func , ast .Attribute ):
101+ return
102+
103+ value = func .value
104+ if not isinstance (value , ast .Name ):
105+ return
106+
107+ if (value .id != "support" ) or (func .attr != "findfile" ):
108+ return
109+
110+ arg = node .args [0 ]
111+ if not isinstance (arg , ast .Constant ):
112+ return
83113
84- for match in _IMPORT_RE .finditer (content ):
85- imports .add (match .group (1 ))
114+ target = arg .value
115+ if not target .endswith (".py" ):
116+ return
86117
87- for match in _FROM_IMPORT_RE . finditer ( content ):
88- imports . add (match . group ( 1 ) )
118+ target = target . removesuffix ( ".py" )
119+ self . __imports . add (f"test. { target } " )
89120
90- return imports
121+
122+ def parse_test_imports (content : str ) -> set [str ]:
123+ """Parse test file content and extract test package dependencies."""
124+ if not (tree := safe_parse_ast (content )):
125+ return set ()
126+
127+ visitor = ImportVisitor ()
128+ visitor .visit (tree )
129+ return visitor .test_imports
130+
131+
132+ def parse_lib_imports (content : str ) -> set [str ]:
133+ """Parse library file and extract all imported module names."""
134+ if not (tree := safe_parse_ast (content )):
135+ return set ()
136+
137+ visitor = ImportVisitor ()
138+ visitor .visit (tree )
139+ return visitor .lib_imports
91140
92141
93142# === TODO marker utilities ===
@@ -104,7 +153,7 @@ def filter_rustpython_todo(content: str) -> str:
104153
105154def count_rustpython_todo (content : str ) -> int :
106155 """Count lines containing RustPython TODO markers."""
107- return sum ( 1 for line in content .splitlines () if TODO_MARKER in line )
156+ return content .count ( TODO_MARKER )
108157
109158
110159def count_todo_in_path (path : pathlib .Path ) -> int :
@@ -113,10 +162,7 @@ def count_todo_in_path(path: pathlib.Path) -> int:
113162 content = safe_read_text (path )
114163 return count_rustpython_todo (content ) if content else 0
115164
116- total = 0
117- for _ , content in read_python_files (path ):
118- total += count_rustpython_todo (content )
119- return total
165+ return sum (count_rustpython_todo (content ) for _ , content in read_python_files (path ))
120166
121167
122168# === Test utilities ===
0 commit comments