Skip to content

Commit eee2932

Browse files
committed
smarter module resolution
1 parent 0d38949 commit eee2932

File tree

2 files changed

+118
-8
lines changed

2 files changed

+118
-8
lines changed

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: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,24 +163,42 @@ 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, {})
@@ -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):

0 commit comments

Comments
 (0)