Skip to content

Commit e0eb6d5

Browse files
authored
Reapply "ast.NodeVisitor for import tracking" (#7241)
* Reapply "`ast.NodeVisitor` for import tracking (#7229)" (#7230) This reverts commit a47572c.
1 parent 4092346 commit e0eb6d5

File tree

2 files changed

+93
-47
lines changed

2 files changed

+93
-47
lines changed

scripts/update_lib/cmd_todo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ def compute_test_todo_list(
334334
test_order = lib_test_order[lib_name].index(test_name)
335335
else:
336336
# Extract lib name from test name (test_foo -> foo)
337-
lib_name = test_name.removeprefix("test_")
337+
lib_name = test_name.removeprefix("test_").removeprefix("_test")
338338
test_order = 0 # Default order for tests not in DEPENDENCIES
339339

340340
# Check if corresponding lib is up-to-date

scripts/update_lib/deps.py

Lines changed: 92 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import difflib
1212
import functools
1313
import pathlib
14-
import re
1514
import shelve
1615
import subprocess
1716

@@ -32,62 +31,112 @@
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("\ndef ")
38-
class_idx = content.find("\nclass ")
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

105154
def 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

110159
def 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

Comments
 (0)