mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
* Convert parsing logic to python
* Trigger job. (DROP ME BEFORE MERGE!)
* Missing EOF
* Add found modules to set
* Suggestion by @fanninpm
* Fix missing trailing slash
* Ensure no `.py` at module name
* Strip any file auffix
* Revert "Trigger job. (DROP ME BEFORE MERGE!)"
This reverts commit 8cf9651a24.
* Handle quoted args
464 lines
14 KiB
Python
464 lines
14 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Show dependency information for a module.
|
|
|
|
Usage:
|
|
python scripts/update_lib deps dis
|
|
python scripts/update_lib deps dataclasses
|
|
python scripts/update_lib deps dis --depth 2
|
|
python scripts/update_lib deps all # Show all modules' dependencies
|
|
"""
|
|
|
|
import argparse
|
|
import pathlib
|
|
import sys
|
|
|
|
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
|
|
|
|
|
|
def get_all_modules(cpython_prefix: str) -> list[str]:
|
|
"""Get all top-level module names from cpython/Lib/.
|
|
|
|
Includes private modules (_*) that are not hard_deps of other modules.
|
|
|
|
Returns:
|
|
Sorted list of module names (without .py extension)
|
|
"""
|
|
from update_lib.deps import resolve_hard_dep_parent
|
|
|
|
lib_dir = pathlib.Path(cpython_prefix) / "Lib"
|
|
if not lib_dir.exists():
|
|
return []
|
|
|
|
modules = set()
|
|
for entry in lib_dir.iterdir():
|
|
# Skip hidden files
|
|
if entry.name.startswith("."):
|
|
continue
|
|
# Skip test directory
|
|
if entry.name == "test":
|
|
continue
|
|
|
|
if entry.is_file() and entry.suffix == ".py":
|
|
name = entry.stem
|
|
elif entry.is_dir() and (entry / "__init__.py").exists():
|
|
name = entry.name
|
|
else:
|
|
continue
|
|
|
|
# Skip modules that are hard_deps of other modules
|
|
# e.g., _pydatetime is a hard_dep of datetime, pydoc_data is a hard_dep of pydoc
|
|
if resolve_hard_dep_parent(name, cpython_prefix) is not None:
|
|
continue
|
|
|
|
modules.add(name)
|
|
|
|
return sorted(modules)
|
|
|
|
|
|
def format_deps_tree(
|
|
cpython_prefix: str,
|
|
lib_prefix: str,
|
|
max_depth: int,
|
|
*,
|
|
name: str | None = None,
|
|
soft_deps: set[str] | None = None,
|
|
hard_deps: set[str] | None = None,
|
|
_depth: int = 0,
|
|
_visited: set[str] | None = None,
|
|
_indent: str = "",
|
|
) -> list[str]:
|
|
"""Format soft dependencies as a tree with up-to-date status.
|
|
|
|
Args:
|
|
cpython_prefix: CPython directory prefix
|
|
lib_prefix: Local Lib directory prefix
|
|
max_depth: Maximum recursion depth
|
|
name: Module name (used to compute deps if soft_deps not provided)
|
|
soft_deps: Pre-computed soft dependencies (optional)
|
|
hard_deps: Hard dependencies to show under the module (root level only)
|
|
_depth: Current depth (internal)
|
|
_visited: Already visited modules (internal)
|
|
_indent: Current indentation (internal)
|
|
|
|
Returns:
|
|
List of formatted lines
|
|
"""
|
|
from update_lib.deps import (
|
|
get_lib_paths,
|
|
get_rust_deps,
|
|
get_soft_deps,
|
|
is_up_to_date,
|
|
)
|
|
|
|
lines = []
|
|
|
|
if _visited is None:
|
|
_visited = set()
|
|
|
|
# Compute deps from name if not provided
|
|
if soft_deps is None:
|
|
soft_deps = get_soft_deps(name, cpython_prefix) if name else set()
|
|
|
|
soft_deps = sorted(soft_deps)
|
|
|
|
if not soft_deps and not hard_deps:
|
|
return lines
|
|
|
|
# Separate up-to-date and outdated modules
|
|
up_to_date_deps = []
|
|
outdated_deps = []
|
|
dup_deps = []
|
|
|
|
for dep in soft_deps:
|
|
# Skip if library doesn't exist in cpython
|
|
lib_paths = get_lib_paths(dep, cpython_prefix)
|
|
if not any(p.exists() for p in lib_paths):
|
|
continue
|
|
|
|
up_to_date = is_up_to_date(dep, cpython_prefix, lib_prefix)
|
|
if up_to_date:
|
|
# Up-to-date modules collected compactly, no dup tracking needed
|
|
up_to_date_deps.append(dep)
|
|
elif dep in _visited:
|
|
# Only track dup for outdated modules
|
|
dup_deps.append(dep)
|
|
else:
|
|
outdated_deps.append(dep)
|
|
|
|
# Show outdated modules with expansion
|
|
for dep in outdated_deps:
|
|
dep_native = get_rust_deps(dep, cpython_prefix)
|
|
native_suffix = (
|
|
f" (native: {', '.join(sorted(dep_native))})" if dep_native else ""
|
|
)
|
|
lines.append(f"{_indent}- [ ] {dep}{native_suffix}")
|
|
_visited.add(dep)
|
|
|
|
# Show hard_deps under this module (only at root level, i.e., when hard_deps is provided)
|
|
if hard_deps and dep in soft_deps:
|
|
for hd in sorted(hard_deps):
|
|
hd_up_to_date = is_up_to_date(hd, cpython_prefix, lib_prefix)
|
|
hd_marker = "[x]" if hd_up_to_date else "[ ]"
|
|
lines.append(f"{_indent} - {hd_marker} {hd}")
|
|
hard_deps = None # Only show once
|
|
|
|
# Recurse if within depth limit
|
|
if _depth < max_depth - 1:
|
|
lines.extend(
|
|
format_deps_tree(
|
|
cpython_prefix,
|
|
lib_prefix,
|
|
max_depth,
|
|
name=dep,
|
|
_depth=_depth + 1,
|
|
_visited=_visited,
|
|
_indent=_indent + " ",
|
|
)
|
|
)
|
|
|
|
# Show duplicates compactly (only for outdated)
|
|
if dup_deps:
|
|
lines.append(f"{_indent}- [ ] {', '.join(dup_deps)}")
|
|
|
|
# Show up-to-date modules compactly on one line
|
|
if up_to_date_deps:
|
|
lines.append(f"{_indent}- [x] {', '.join(up_to_date_deps)}")
|
|
|
|
return lines
|
|
|
|
|
|
def format_deps(
|
|
name: str,
|
|
cpython_prefix: str,
|
|
lib_prefix: str,
|
|
max_depth: int = 10,
|
|
_visited: set[str] | None = None,
|
|
) -> list[str]:
|
|
"""Format all dependency information for a module.
|
|
|
|
Args:
|
|
name: Module name
|
|
cpython_prefix: CPython directory prefix
|
|
lib_prefix: Local Lib directory prefix
|
|
max_depth: Maximum recursion depth
|
|
_visited: Shared visited set for deduplication across modules
|
|
|
|
Returns:
|
|
List of formatted lines
|
|
"""
|
|
from update_lib.deps import (
|
|
DEPENDENCIES,
|
|
count_test_todos,
|
|
find_dependent_tests_tree,
|
|
get_lib_paths,
|
|
get_test_paths,
|
|
is_path_synced,
|
|
is_test_up_to_date,
|
|
resolve_hard_dep_parent,
|
|
)
|
|
|
|
if _visited is None:
|
|
_visited = set()
|
|
|
|
lines = []
|
|
|
|
# Resolve test_ prefix to module (e.g., test_pydoc -> pydoc)
|
|
if name.startswith("test_"):
|
|
module_name = name[5:] # strip "test_"
|
|
lines.append(f"(redirecting {name} -> {module_name})")
|
|
name = module_name
|
|
|
|
# Resolve hard_dep to parent module (e.g., pydoc_data -> pydoc)
|
|
parent = resolve_hard_dep_parent(name, cpython_prefix)
|
|
if parent:
|
|
lines.append(f"(redirecting {name} -> {parent})")
|
|
name = parent
|
|
|
|
# lib paths (only show existing)
|
|
lib_paths = get_lib_paths(name, cpython_prefix)
|
|
existing_lib_paths = [p for p in lib_paths if p.exists()]
|
|
for p in existing_lib_paths:
|
|
synced = is_path_synced(p, cpython_prefix, lib_prefix)
|
|
marker = "[x]" if synced else "[ ]"
|
|
lines.append(f"{marker} lib: {p}")
|
|
|
|
# test paths (only show existing)
|
|
test_paths = get_test_paths(name, cpython_prefix)
|
|
existing_test_paths = [p for p in test_paths if p.exists()]
|
|
for p in existing_test_paths:
|
|
test_name = p.stem if p.is_file() else p.name
|
|
synced = is_test_up_to_date(test_name, cpython_prefix, lib_prefix)
|
|
marker = "[x]" if synced else "[ ]"
|
|
todo_count = count_test_todos(test_name, lib_prefix)
|
|
todo_suffix = f" (TODO: {todo_count})" if todo_count > 0 else ""
|
|
lines.append(f"{marker} test: {p}{todo_suffix}")
|
|
|
|
# If no lib or test paths exist, module doesn't exist
|
|
if not existing_lib_paths and not existing_test_paths:
|
|
lines.append(f"(module '{name}' not found)")
|
|
return lines
|
|
|
|
# Collect all hard_deps (explicit from DEPENDENCIES + implicit from lib_paths)
|
|
dep_info = DEPENDENCIES.get(name, {})
|
|
explicit_hard_deps = dep_info.get("hard_deps", [])
|
|
|
|
# Get implicit hard_deps from lib_paths (e.g., _pydecimal.py for decimal)
|
|
all_hard_deps = set()
|
|
for hd in explicit_hard_deps:
|
|
# Remove .py extension if present
|
|
all_hard_deps.add(hd[:-3] if hd.endswith(".py") else hd)
|
|
|
|
for p in existing_lib_paths:
|
|
dep_name = p.stem if p.is_file() else p.name
|
|
if dep_name != name: # Skip the main module itself
|
|
all_hard_deps.add(dep_name)
|
|
|
|
lines.append("\ndependencies:")
|
|
lines.extend(
|
|
format_deps_tree(
|
|
cpython_prefix,
|
|
lib_prefix,
|
|
max_depth,
|
|
soft_deps={name},
|
|
_visited=_visited,
|
|
hard_deps=all_hard_deps,
|
|
)
|
|
)
|
|
|
|
# Show dependent tests as tree (depth 2: module + direct importers + their importers)
|
|
tree = find_dependent_tests_tree(name, lib_prefix=lib_prefix, max_depth=2)
|
|
lines.extend(_format_dependent_tests_tree(tree, cpython_prefix, lib_prefix))
|
|
|
|
return lines
|
|
|
|
|
|
def _format_dependent_tests_tree(
|
|
tree: dict,
|
|
cpython_prefix: str,
|
|
lib_prefix: str,
|
|
indent: str = "",
|
|
) -> list[str]:
|
|
"""Format dependent tests tree for display."""
|
|
from update_lib.deps import is_up_to_date
|
|
|
|
lines = []
|
|
module = tree["module"]
|
|
tests = tree["tests"]
|
|
children = tree["children"]
|
|
|
|
if indent == "":
|
|
# Root level
|
|
# Count total tests in tree
|
|
def count_tests(t: dict) -> int:
|
|
total = len(t.get("tests", []))
|
|
for c in t.get("children", []):
|
|
total += count_tests(c)
|
|
return total
|
|
|
|
total = count_tests(tree)
|
|
if total == 0 and not children:
|
|
lines.append(f"\ndependent tests: (no tests depend on {module})")
|
|
return lines
|
|
lines.append(f"\ndependent tests: ({total} tests)")
|
|
|
|
# Check if module is up-to-date
|
|
synced = is_up_to_date(module.split(".")[0], cpython_prefix, lib_prefix)
|
|
marker = "[x]" if synced else "[ ]"
|
|
|
|
# Format this node
|
|
if tests:
|
|
test_str = " ".join(tests)
|
|
if indent == "":
|
|
lines.append(f"- {marker} {module}: {test_str}")
|
|
else:
|
|
lines.append(f"{indent}- {marker} {module}: {test_str}")
|
|
elif indent != "" and children:
|
|
# Has children but no direct tests
|
|
lines.append(f"{indent}- {marker} {module}:")
|
|
|
|
# Format children
|
|
child_indent = indent + " " if indent else " "
|
|
for child in children:
|
|
lines.extend(
|
|
_format_dependent_tests_tree(
|
|
child, cpython_prefix, lib_prefix, child_indent
|
|
)
|
|
)
|
|
|
|
return lines
|
|
|
|
|
|
def _resolve_module_name(
|
|
name: str,
|
|
cpython_prefix: str,
|
|
lib_prefix: str,
|
|
) -> list[str]:
|
|
"""Resolve module name through redirects.
|
|
|
|
Returns a list of module names (usually 1, but test support files may expand to multiple).
|
|
"""
|
|
import pathlib
|
|
|
|
from update_lib.deps import (
|
|
_build_test_import_graph,
|
|
get_lib_paths,
|
|
get_test_paths,
|
|
resolve_hard_dep_parent,
|
|
resolve_test_to_lib,
|
|
)
|
|
|
|
# Resolve test to library group (e.g., test_urllib2 -> urllib)
|
|
if name.startswith("test_"):
|
|
lib_group = resolve_test_to_lib(name)
|
|
if lib_group:
|
|
return [lib_group]
|
|
name = name[5:]
|
|
|
|
# Resolve hard_dep to parent
|
|
parent = resolve_hard_dep_parent(name, cpython_prefix)
|
|
if parent:
|
|
return [parent]
|
|
|
|
# Check if it's a valid module
|
|
lib_paths = get_lib_paths(name, cpython_prefix)
|
|
test_paths = get_test_paths(name, cpython_prefix)
|
|
if any(p.exists() for p in lib_paths) or any(p.exists() for p in test_paths):
|
|
return [name]
|
|
|
|
# Check for test support files (e.g., string_tests -> bytes, str, userstring)
|
|
test_support_path = pathlib.Path(cpython_prefix) / "Lib" / "test" / f"{name}.py"
|
|
if test_support_path.exists():
|
|
test_dir = pathlib.Path(lib_prefix) / "test"
|
|
if test_dir.exists():
|
|
import_graph, _ = _build_test_import_graph(test_dir)
|
|
importing_tests = []
|
|
for file_key, imports in import_graph.items():
|
|
if name in imports and file_key.startswith("test_"):
|
|
importing_tests.append(file_key)
|
|
if importing_tests:
|
|
# Resolve test names to module names (test_bytes -> bytes)
|
|
return sorted(set(t[5:] for t in importing_tests))
|
|
|
|
return [name]
|
|
|
|
|
|
def show_deps(
|
|
names: list[str],
|
|
cpython_prefix: str,
|
|
lib_prefix: str,
|
|
max_depth: int = 10,
|
|
) -> None:
|
|
"""Show all dependency information for modules."""
|
|
# Expand "all" to all module names
|
|
expanded_names = []
|
|
for name in set(names):
|
|
if name == "all":
|
|
expanded_names.extend(get_all_modules(cpython_prefix))
|
|
else:
|
|
expanded_names.append(name)
|
|
|
|
# Resolve and deduplicate names (preserving order)
|
|
seen: set[str] = set()
|
|
resolved_names: list[str] = []
|
|
for name in expanded_names:
|
|
for resolved in _resolve_module_name(name, cpython_prefix, lib_prefix):
|
|
if resolved not in seen:
|
|
seen.add(resolved)
|
|
resolved_names.append(resolved)
|
|
|
|
# Shared visited set across all modules
|
|
visited: set[str] = set()
|
|
|
|
for i, name in enumerate(resolved_names):
|
|
if i > 0:
|
|
print() # blank line between modules
|
|
for line in format_deps(name, cpython_prefix, lib_prefix, max_depth, visited):
|
|
print(line)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"names",
|
|
nargs="+",
|
|
help="Module names (e.g., dis, dataclasses) or 'all' for all modules",
|
|
)
|
|
parser.add_argument(
|
|
"--cpython",
|
|
default="cpython",
|
|
help="CPython directory prefix (default: cpython)",
|
|
)
|
|
parser.add_argument(
|
|
"--lib",
|
|
default="Lib",
|
|
help="Local Lib directory prefix (default: Lib)",
|
|
)
|
|
parser.add_argument(
|
|
"--depth",
|
|
type=int,
|
|
default=10,
|
|
help="Maximum recursion depth for soft_deps tree (default: 10)",
|
|
)
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
# When user does `./update_lib/ deps "foo bar" "baz"`
|
|
# We still want to get a list of
|
|
# ["foo", "bar", "baz"], not ["foo bar", "baz"]
|
|
args.names = [name for names in args.names for name in names.split()]
|
|
|
|
try:
|
|
show_deps(args.names, args.cpython, args.lib, args.depth)
|
|
return 0
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|