Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
285 changes: 282 additions & 3 deletions scripts/update_lib/show_todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,230 @@ def compute_todo_list(
return result


def get_all_tests(cpython_prefix: str = "cpython") -> list[str]:
"""Get all test module names from cpython/Lib/test/.

Returns:
Sorted list of test names (e.g., ["test_abc", "test_dis", ...])
"""
test_dir = pathlib.Path(cpython_prefix) / "Lib" / "test"
if not test_dir.exists():
return []

tests = set()
for entry in test_dir.iterdir():
# Skip private/internal and special directories
if entry.name.startswith(("_", ".")):
continue
# Skip non-test items
if not entry.name.startswith("test_"):
continue

if entry.is_file() and entry.suffix == ".py":
tests.add(entry.stem)
elif entry.is_dir() and (entry / "__init__.py").exists():
tests.add(entry.name)

return sorted(tests)


def _filter_rustpython_todo(content: str) -> str:
"""Remove lines containing 'TODO: RUSTPYTHON' from content."""
lines = content.splitlines(keepends=True)
filtered = [line for line in lines if "TODO: RUSTPYTHON" not in line]
return "".join(filtered)


def _count_rustpython_todo(content: str) -> int:
"""Count lines containing 'TODO: RUSTPYTHON' in content."""
return sum(1 for line in content.splitlines() if "TODO: RUSTPYTHON" in line)


def _compare_file_ignoring_todo(
cpython_path: pathlib.Path, local_path: pathlib.Path
) -> bool:
"""Compare two files, ignoring TODO: RUSTPYTHON lines in local file."""
try:
cpython_content = cpython_path.read_text(encoding="utf-8")
local_content = local_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return False

local_filtered = _filter_rustpython_todo(local_content)
return cpython_content == local_filtered


def _compare_dir_ignoring_todo(
cpython_path: pathlib.Path, local_path: pathlib.Path
) -> bool:
"""Compare two directories, ignoring TODO: RUSTPYTHON lines in local files."""
# Get all .py files in both directories
cpython_files = {f.relative_to(cpython_path) for f in cpython_path.rglob("*.py")}
local_files = {f.relative_to(local_path) for f in local_path.rglob("*.py")}

# Check for missing or extra files
if cpython_files != local_files:
return False

# Compare each file
for rel_path in cpython_files:
if not _compare_file_ignoring_todo(
cpython_path / rel_path, local_path / rel_path
):
return False

return True


def count_test_todos(test_name: str, lib_prefix: str = "Lib") -> int:
"""Count TODO: RUSTPYTHON lines in a test file/directory."""
local_dir = pathlib.Path(lib_prefix) / "test" / test_name
local_file = pathlib.Path(lib_prefix) / "test" / f"{test_name}.py"

if local_dir.exists():
local_path = local_dir
elif local_file.exists():
local_path = local_file
else:
return 0

total = 0
if local_path.is_file():
try:
content = local_path.read_text(encoding="utf-8")
total = _count_rustpython_todo(content)
except (OSError, UnicodeDecodeError):
pass
else:
for py_file in local_path.rglob("*.py"):
try:
content = py_file.read_text(encoding="utf-8")
total += _count_rustpython_todo(content)
except (OSError, UnicodeDecodeError):
pass

return total


def is_test_up_to_date(
test_name: str, cpython_prefix: str = "cpython", lib_prefix: str = "Lib"
) -> bool:
"""Check if a test is up-to-date by comparing files.

Ignores lines containing 'TODO: RUSTPYTHON' in local files.
"""
# Try directory first, then file
cpython_dir = pathlib.Path(cpython_prefix) / "Lib" / "test" / test_name
cpython_file = pathlib.Path(cpython_prefix) / "Lib" / "test" / f"{test_name}.py"

if cpython_dir.exists():
cpython_path = cpython_dir
elif cpython_file.exists():
cpython_path = cpython_file
else:
return True # No cpython test, consider up-to-date

local_path = pathlib.Path(lib_prefix) / "test" / cpython_path.name

if not local_path.exists():
return False

if cpython_path.is_file():
return _compare_file_ignoring_todo(cpython_path, local_path)
else:
return _compare_dir_ignoring_todo(cpython_path, local_path)


def compute_test_todo_list(
cpython_prefix: str = "cpython",
lib_prefix: str = "Lib",
include_done: bool = False,
lib_status: dict[str, bool] | None = None,
) -> list[dict]:
"""Compute prioritized list of tests to update.

Scoring:
- If corresponding lib is up-to-date: score = 0 (ready)
- If corresponding lib is NOT up-to-date: score = 1 (wait for lib)
- If no corresponding lib: score = -1 (independent)
Comment on lines +316 to +319
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Docstring does not match implementation.

The docstring states scores of 0, 1, and -1, but the implementation uses 0, 2, and 1:

  • score = 0: lib is up-to-date (matches)
  • score = 2: lib NOT up-to-date (docstring says 1)
  • score = 1: no corresponding lib (docstring says -1)
📝 Proposed fix
     """Compute prioritized list of tests to update.

     Scoring:
-        - If corresponding lib is up-to-date: score = 0 (ready)
-        - If corresponding lib is NOT up-to-date: score = 1 (wait for lib)
-        - If no corresponding lib: score = -1 (independent)
+        - If corresponding lib is up-to-date: score = 0 (ready to update)
+        - If no corresponding lib: score = 1 (independent test)
+        - If corresponding lib is NOT up-to-date: score = 2 (wait for lib first)

     Returns:
         List of dicts with test info, sorted by priority
     """
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Scoring:
- If corresponding lib is up-to-date: score = 0 (ready)
- If corresponding lib is NOT up-to-date: score = 1 (wait for lib)
- If no corresponding lib: score = -1 (independent)
Scoring:
- If corresponding lib is up-to-date: score = 0 (ready to update)
- If no corresponding lib: score = 1 (independent test)
- If corresponding lib is NOT up-to-date: score = 2 (wait for lib first)
🤖 Prompt for AI Agents
In `@scripts/update_lib/show_todo.py` around lines 253 - 256, The docstring and
implementation disagree on the scoring values; update the logic that assigns the
variable score in this module so it matches the docstring: set score = 0 when
the corresponding lib is up-to-date, set score = 1 when the corresponding lib
exists but is NOT up-to-date, and set score = -1 when there is no corresponding
lib (ensure any places that check or return score use these values consistently,
e.g., the score assignment sites and any functions that consume score).


Returns:
List of dicts with test info, sorted by priority
"""
all_tests = get_all_tests(cpython_prefix)

result = []
for test_name in all_tests:
up_to_date = is_test_up_to_date(test_name, cpython_prefix, lib_prefix)

if up_to_date and not include_done:
continue

# Extract lib name from test name (test_foo -> foo)
lib_name = test_name[5:] if test_name.startswith("test_") else test_name

# Check if corresponding lib is up-to-date
# Scoring: 0 = lib ready (highest priority), 1 = no lib, 2 = lib pending
if lib_status and lib_name in lib_status:
lib_up_to_date = lib_status[lib_name]
if lib_up_to_date:
score = 0 # Lib is ready, can update test
else:
score = 2 # Wait for lib first
else:
score = 1 # No corresponding lib (independent test)

todo_count = count_test_todos(test_name, lib_prefix)

result.append(
{
"name": test_name,
"lib_name": lib_name,
"score": score,
"up_to_date": up_to_date,
"todo_count": todo_count,
}
)

# Sort by score (ascending)
result.sort(key=lambda x: x["score"])

return result


def format_test_todo_list(
todo_list: list[dict],
limit: int | None = None,
) -> list[str]:
"""Format test todo list for display."""
lines = []

if limit:
todo_list = todo_list[:limit]

for item in todo_list:
name = item["name"]
done_mark = "[x]" if item["up_to_date"] else "[ ]"
todo_count = item.get("todo_count", 0)
if todo_count > 0:
lines.append(f"- {done_mark} {name} ({todo_count} TODO)")
else:
lines.append(f"- {done_mark} {name}")

return lines


def format_todo_list(
todo_list: list[dict],
test_by_lib: dict[str, dict] | None = None,
limit: int | None = None,
verbose: bool = False,
) -> list[str]:
"""Format todo list for display.

Args:
todo_list: List from compute_todo_list()
test_by_lib: Dict mapping lib_name -> test info (optional)
limit: Maximum number of items to show
verbose: Show detailed dependency information

Expand Down Expand Up @@ -149,6 +364,18 @@ def format_todo_list(

lines.append(" ".join(parts))

# Show corresponding test if exists
if test_by_lib and name in test_by_lib:
test_info = test_by_lib[name]
test_done_mark = "[x]" if test_info["up_to_date"] else "[ ]"
todo_count = test_info.get("todo_count", 0)
if todo_count > 0:
lines.append(
f" - {test_done_mark} {test_info['name']} ({todo_count} TODO)"
)
else:
lines.append(f" - {test_done_mark} {test_info['name']}")

# Verbose mode: show detailed dependency info
if verbose:
if item["reverse_deps"]:
Expand All @@ -161,16 +388,68 @@ def format_todo_list(
return lines


def format_all_todo(
cpython_prefix: str = "cpython",
lib_prefix: str = "Lib",
limit: int | None = None,
include_done: bool = False,
verbose: bool = False,
) -> list[str]:
"""Format prioritized list of modules and tests to update.

Returns:
List of formatted lines
"""
from update_lib.deps import is_up_to_date
from update_lib.show_deps import get_all_modules

lines = []

# Compute lib todo
lib_todo = compute_todo_list(cpython_prefix, lib_prefix, include_done)

# Build lib status map for test scoring
lib_status = {}
for name in get_all_modules(cpython_prefix):
lib_status[name] = is_up_to_date(name, cpython_prefix, lib_prefix)

# Compute test todo
test_todo = compute_test_todo_list(
cpython_prefix, lib_prefix, include_done, lib_status
)

# Build test_by_lib map (only for tests with corresponding lib)
test_by_lib = {}
no_lib_tests = []
for test in test_todo:
if test["score"] == 1: # no lib
no_lib_tests.append(test)
else:
test_by_lib[test["lib_name"]] = test

# Format lib todo with embedded tests
lines.extend(format_todo_list(lib_todo, test_by_lib, limit, verbose))

# Format "no lib" tests separately if any
if no_lib_tests:
lines.append("")
lines.append("## Standalone Tests")
lines.extend(format_test_todo_list(no_lib_tests, limit))

return lines


def show_todo(
cpython_prefix: str = "cpython",
lib_prefix: str = "Lib",
limit: int | None = None,
include_done: bool = False,
verbose: bool = False,
) -> None:
"""Show prioritized list of modules to update."""
todo_list = compute_todo_list(cpython_prefix, lib_prefix, include_done)
for line in format_todo_list(todo_list, limit, verbose):
"""Show prioritized list of modules and tests to update."""
for line in format_all_todo(
cpython_prefix, lib_prefix, limit, include_done, verbose
):
print(line)


Expand Down
Loading