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 . test_imports = set ( )
37+ self . lib_imports = set ( )
3938
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 " )
39+ def add_import (self , name : str ) -> None :
40+ """
41+ Add an `import` to its correct slot (`test_imports` or `lib_imports`).
4442
43+ Parameters
44+ ----------
45+ name : str
46+ Module name.
47+ """
48+ if name .startswith ("test.support" ):
49+ return
4550
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 )
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 )
4956
57+ def visit_Import (self , node ):
58+ for alias in node .names :
59+ self .add_import (alias .name )
5060
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 ()
61+ def visit_ImportFrom (self , node ):
62+ try :
63+ module = node .module
64+ except AttributeError :
65+ # Ignore `from . import my_internal_module`
66+ return
5567
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 )
68+ for name in node .names :
69+ self .add_import (f"{ module } .{ name } " )
6270
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 )
71+ def visit_Call (self , node ) -> None :
72+ """
73+ In test files, there's sometimes use of:
6774
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 )
75+ ```python
76+ import test.support
77+ from test.support import script_helper
7278
73- return imports
79+ script = support.findfile("_test_atexit.py")
80+ script_helper.run_test_script(script)
81+ ```
7482
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
7588
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 )
89+ value = func .value
90+ if not isinstance (value , ast .Name ):
91+ return
7892
93+ if (value .id != "support" ) or (func .attr != "findfile" ):
94+ return
7995
80- def parse_lib_imports (content : str ) -> set [str ]:
81- """Parse library file and extract all imported module names."""
82- imports = set ()
96+ arg = node .args [0 ]
97+ if not isinstance (arg , ast .Constant ):
98+ return
99+
100+ target = arg .value
101+ if not target .endswith (".py" ):
102+ return
83103
84- for match in _IMPORT_RE . finditer ( content ):
85- imports . add ( match . group ( 1 ) )
104+ target = target . removesuffix ( ".py" )
105+ self . add_import ( f"test. { target } " )
86106
87- for match in _FROM_IMPORT_RE .finditer (content ):
88- imports .add (match .group (1 ))
89107
90- return imports
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 ()
112+
113+ visitor = ImportVisitor ()
114+ visitor .visit (tree )
115+ return visitor .test_imports
116+
117+
118+ def parse_lib_imports (content : str ) -> set [str ]:
119+ """Parse library file and extract all imported module names."""
120+ if not (tree := safe_parse_ast (content )):
121+ return set ()
122+
123+ visitor = ImportVisitor ()
124+ visitor .visit (tree )
125+ return visitor .lib_imports
91126
92127
93128# === TODO marker utilities ===
@@ -104,7 +139,7 @@ def filter_rustpython_todo(content: str) -> str:
104139
105140def count_rustpython_todo (content : str ) -> int :
106141 """Count lines containing RustPython TODO markers."""
107- return sum ( 1 for line in content .splitlines () if TODO_MARKER in line )
142+ return content .count ( TODO_MARKER )
108143
109144
110145def count_todo_in_path (path : pathlib .Path ) -> int :
@@ -113,10 +148,7 @@ def count_todo_in_path(path: pathlib.Path) -> int:
113148 content = safe_read_text (path )
114149 return count_rustpython_todo (content ) if content else 0
115150
116- total = 0
117- for _ , content in read_python_files (path ):
118- total += count_rustpython_todo (content )
119- return total
151+ return sum (count_rustpython_todo (content ) for _ , content in read_python_files (path ))
120152
121153
122154# === Test utilities ===
0 commit comments