Skip to content

Commit 180c68e

Browse files
authored
improve deps CI formatting and name resolution (#6847)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Enhanced test module dependency tracking in automated checks to properly identify and report test-related dependencies. * Improved dependency resolution logic with better module name mapping and deduplication for clearer dependency reporting. * Refined formatting of dependency information and todo items for improved readability. * Dependency review comments now automatically refresh with the latest information when updated. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent af7bbfa commit 180c68e

File tree

4 files changed

+133
-17
lines changed

4 files changed

+133
-17
lines changed

.github/workflows/lib-deps-check.yaml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,20 @@ jobs:
4545
echo "Changed files:"
4646
echo "$changed"
4747
48-
# Extract unique module names (top-level only, skip test/)
48+
# Extract unique module names
4949
modules=""
5050
for file in $changed; do
51-
# Skip test files
5251
if [[ "$file" == Lib/test/* ]]; then
53-
continue
52+
# Test files: Lib/test/test_pydoc.py -> test_pydoc, Lib/test/test_pydoc/foo.py -> test_pydoc
53+
module=$(echo "$file" | sed -E 's|^Lib/test/||; s|\.py$||; s|/.*||')
54+
# Skip non-test files in test/ (e.g., support.py, __init__.py)
55+
if [[ ! "$module" == test_* ]]; then
56+
continue
57+
fi
58+
else
59+
# Lib files: Lib/foo.py -> foo, Lib/foo/__init__.py -> foo
60+
module=$(echo "$file" | sed -E 's|^Lib/||; s|/__init__\.py$||; s|\.py$||; s|/.*||')
5461
fi
55-
# Extract module name: Lib/foo.py -> foo, Lib/foo/__init__.py -> foo
56-
module=$(echo "$file" | sed -E 's|^Lib/||; s|/__init__\.py$||; s|\.py$||; s|/.*||')
5762
if [[ -n "$module" && ! " $modules " =~ " $module " ]]; then
5863
modules="$modules $module"
5964
fi
@@ -94,6 +99,7 @@ jobs:
9499
with:
95100
header: lib-deps-check
96101
number: ${{ github.event.pull_request.number }}
102+
recreate: true
97103
message: |
98104
## 📦 Library Dependencies
99105

scripts/update_lib/deps.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,32 @@ def clear_import_graph_caches() -> None:
101101
},
102102
}
103103

104+
def resolve_hard_dep_parent(name: str) -> str | None:
105+
"""Resolve a hard_dep name to its parent module.
106+
107+
If 'name' is listed as a hard_dep of another module, return that module's name.
108+
E.g., 'pydoc_data' -> 'pydoc', '_pydatetime' -> 'datetime'
109+
110+
Args:
111+
name: Module or file name (with or without .py extension)
112+
113+
Returns:
114+
Parent module name if found, None otherwise
115+
"""
116+
# Normalize: remove .py extension if present
117+
if name.endswith(".py"):
118+
name = name[:-3]
119+
120+
for module_name, dep_info in DEPENDENCIES.items():
121+
hard_deps = dep_info.get("hard_deps", [])
122+
for dep in hard_deps:
123+
# Normalize dep: remove .py extension
124+
dep_normalized = dep[:-3] if dep.endswith(".py") else dep
125+
if dep_normalized == name:
126+
return module_name
127+
return None
128+
129+
104130
# Test-specific dependencies (only when auto-detection isn't enough)
105131
# - hard_deps: files to migrate (tightly coupled, must be migrated together)
106132
# - data: directories to copy without migration
@@ -254,10 +280,11 @@ def _extract_top_level_code(content: str) -> str:
254280

255281
_FROM_TEST_IMPORT_RE = re.compile(r"^from test import (.+)", re.MULTILINE)
256282
_FROM_TEST_DOT_RE = re.compile(r"^from test\.(\w+)", re.MULTILINE)
283+
_IMPORT_TEST_DOT_RE = re.compile(r"^import test\.(\w+)", re.MULTILINE)
257284

258285

259286
def parse_test_imports(content: str) -> set[str]:
260-
"""Parse test file content and extract 'from test import ...' dependencies.
287+
"""Parse test file content and extract test package dependencies.
261288
262289
Uses regex for speed - only matches top-level imports.
263290
@@ -285,6 +312,12 @@ def parse_test_imports(content: str) -> set[str]:
285312
if dep not in ("support", "__init__"):
286313
imports.add(dep)
287314

315+
# Match "import test.foo" -> depends on foo
316+
for match in _IMPORT_TEST_DOT_RE.finditer(content):
317+
dep = match.group(1)
318+
if dep not in ("support", "__init__"):
319+
imports.add(dep)
320+
288321
return imports
289322

290323

scripts/update_lib/show_deps.py

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -163,32 +163,50 @@ def format_deps(
163163
find_dependent_tests_tree,
164164
get_lib_paths,
165165
get_test_paths,
166+
resolve_hard_dep_parent,
166167
)
167168

168169
if _visited is None:
169170
_visited = set()
170171

171172
lines = []
172173

174+
# Resolve test_ prefix to module (e.g., test_pydoc -> pydoc)
175+
if name.startswith("test_"):
176+
module_name = name[5:] # strip "test_"
177+
lines.append(f"(redirecting {name} -> {module_name})")
178+
name = module_name
179+
180+
# Resolve hard_dep to parent module (e.g., pydoc_data -> pydoc)
181+
parent = resolve_hard_dep_parent(name)
182+
if parent:
183+
lines.append(f"(redirecting {name} -> {parent})")
184+
name = parent
185+
173186
# lib paths (only show existing)
174187
lib_paths = get_lib_paths(name, cpython_prefix)
175-
for p in lib_paths:
176-
if p.exists():
177-
lines.append(f"[+] lib: {p}")
188+
existing_lib_paths = [p for p in lib_paths if p.exists()]
189+
for p in existing_lib_paths:
190+
lines.append(f"[+] lib: {p}")
178191

179192
# test paths (only show existing)
180193
test_paths = get_test_paths(name, cpython_prefix)
181-
for p in test_paths:
182-
if p.exists():
183-
lines.append(f"[+] test: {p}")
194+
existing_test_paths = [p for p in test_paths if p.exists()]
195+
for p in existing_test_paths:
196+
lines.append(f"[+] test: {p}")
197+
198+
# If no lib or test paths exist, module doesn't exist
199+
if not existing_lib_paths and not existing_test_paths:
200+
lines.append(f"(module '{name}' not found)")
201+
return lines
184202

185203
# hard_deps (from DEPENDENCIES table)
186204
dep_info = DEPENDENCIES.get(name, {})
187205
hard_deps = dep_info.get("hard_deps", [])
188206
if hard_deps:
189207
lines.append(f"packages: {hard_deps}")
190208

191-
lines.append("dependencies:")
209+
lines.append("\ndependencies:")
192210
lines.extend(
193211
format_deps_tree(
194212
cpython_prefix, lib_prefix, max_depth, soft_deps={name}, _visited=_visited
@@ -227,9 +245,9 @@ def count_tests(t: dict) -> int:
227245

228246
total = count_tests(tree)
229247
if total == 0 and not children:
230-
lines.append(f"dependent tests: (no tests depend on {module})")
248+
lines.append(f"\ndependent tests: (no tests depend on {module})")
231249
return lines
232-
lines.append(f"dependent tests: ({total} tests)")
250+
lines.append(f"\ndependent tests: ({total} tests)")
233251

234252
# Check if module is up-to-date
235253
synced = is_up_to_date(module.split(".")[0], cpython_prefix, lib_prefix)
@@ -258,6 +276,56 @@ def count_tests(t: dict) -> int:
258276
return lines
259277

260278

279+
def _resolve_module_name(
280+
name: str,
281+
cpython_prefix: str,
282+
lib_prefix: str,
283+
) -> list[str]:
284+
"""Resolve module name through redirects.
285+
286+
Returns a list of module names (usually 1, but test support files may expand to multiple).
287+
"""
288+
import pathlib
289+
290+
from update_lib.deps import (
291+
_build_test_import_graph,
292+
get_lib_paths,
293+
get_test_paths,
294+
resolve_hard_dep_parent,
295+
)
296+
297+
# Resolve test_ prefix
298+
if name.startswith("test_"):
299+
name = name[5:]
300+
301+
# Resolve hard_dep to parent
302+
parent = resolve_hard_dep_parent(name)
303+
if parent:
304+
return [parent]
305+
306+
# Check if it's a valid module
307+
lib_paths = get_lib_paths(name, cpython_prefix)
308+
test_paths = get_test_paths(name, cpython_prefix)
309+
if any(p.exists() for p in lib_paths) or any(p.exists() for p in test_paths):
310+
return [name]
311+
312+
# Check for test support files (e.g., string_tests -> bytes, str, userstring)
313+
test_support_path = pathlib.Path(cpython_prefix) / "Lib" / "test" / f"{name}.py"
314+
if test_support_path.exists():
315+
test_dir = pathlib.Path(lib_prefix) / "test"
316+
if test_dir.exists():
317+
import_graph, _ = _build_test_import_graph(test_dir)
318+
importing_tests = []
319+
for file_key, imports in import_graph.items():
320+
if name in imports and file_key.startswith("test_"):
321+
importing_tests.append(file_key)
322+
if importing_tests:
323+
# Resolve test names to module names (test_bytes -> bytes)
324+
return sorted(set(t[5:] for t in importing_tests))
325+
326+
return [name]
327+
328+
261329
def show_deps(
262330
names: list[str],
263331
cpython_prefix: str = "cpython",
@@ -273,10 +341,19 @@ def show_deps(
273341
else:
274342
expanded_names.append(name)
275343

344+
# Resolve and deduplicate names (preserving order)
345+
seen: set[str] = set()
346+
resolved_names: list[str] = []
347+
for name in expanded_names:
348+
for resolved in _resolve_module_name(name, cpython_prefix, lib_prefix):
349+
if resolved not in seen:
350+
seen.add(resolved)
351+
resolved_names.append(resolved)
352+
276353
# Shared visited set across all modules
277354
visited: set[str] = set()
278355

279-
for i, name in enumerate(expanded_names):
356+
for i, name in enumerate(resolved_names):
280357
if i > 0:
281358
print() # blank line between modules
282359
for line in format_deps(name, cpython_prefix, lib_prefix, max_depth, visited):

scripts/update_lib/show_todo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def format_todo_list(
143143

144144
rev_str = f"{rev_count} dependents" if rev_count else ""
145145

146-
parts = [done_mark, f"[{score_str}]", name]
146+
parts = ["-", done_mark, f"[{score_str}]", name]
147147
if rev_str:
148148
parts.append(f"({rev_str})")
149149

0 commit comments

Comments
 (0)