Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

194 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Update task index comments at the top of each tasks.py file.
Reads task metadata from TaskRegistry and generates a structured comment
block at the top of each suite's tasks.py, making it easy to see all tasks
at a glance without scrolling or running external commands.
Usage:
python scripts/bench/audit/update_task_index.py # Update all suites
python scripts/bench/audit/update_task_index.py wechat alipay # Update specific suites
python scripts/bench/audit/update_task_index.py --check # Dry-run, report stale
"""
from __future__ import annotations
import ast
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT))
from bench_env.task.registry import TaskRegistry, _SUITE_MODULES
from bench_env.task.base import BaseTask
MARKER_START = "# -- Task Index (auto-generated, do not edit) --"
MARKER_END = "# -- End Task Index --"
MARKER_PREFIX = "# -- Task Index"
def _resolve_tasks_path(suite: str) -> Path | None:
"""Return the tasks.py path for a suite, or None if it's defs/-only."""
module_path = _SUITE_MODULES.get(suite)
if module_path is None:
return None
return ROOT / Path(module_path.replace(".", "/")).with_suffix(".py")
def _get_class_order(filepath: Path) -> list[str]:
"""Get class names in source-file order via AST."""
tree = ast.parse(filepath.read_text(encoding="utf-8"))
return [
node.name
for node in ast.iter_child_nodes(tree)
if isinstance(node, ast.ClassDef)
]
def _generate_index(
tasks: dict[str, type[BaseTask]],
source_order: list[str],
) -> str:
"""Generate the task index comment block."""
diff_counts: dict[str, int] = {}
for cls in tasks.values():
d = getattr(cls, "difficulty", "?")
diff_counts[d] = diff_counts.get(d, 0) + 1
diff_summary = " ".join(
f"{d}\u00d7{diff_counts[d]}" for d in sorted(diff_counts)
)
order_map = {name: i for i, name in enumerate(source_order)}
sorted_names = sorted(
tasks.keys(),
key=lambda n: order_map.get(n, 9999),
)
max_name_len = max(len(n) for n in tasks) if tasks else 0
pad = max(max_name_len + 2, 24)
lines = [
MARKER_START,
f"# {len(tasks)} tasks | {diff_summary}",
"#",
]
for name in sorted_names:
cls = tasks[name]
difficulty = getattr(cls, "difficulty", "?")
templates = getattr(cls, "templates", [])
if isinstance(templates, str):
templates = [templates]
tpl = templates[0].strip() if templates else ""
tpl_commented = tpl.replace("\n", "\n# ")
lines.append(f"# [{difficulty}] {name:<{pad}s}{tpl_commented}")
lines.append(MARKER_END)
return "\n".join(lines)
def _find_insertion_point(content: str) -> int:
"""Find where to insert the index block (after module docstring)."""
stripped = content.lstrip()
if stripped.startswith('"""') or stripped.startswith("'''"):
quote = stripped[:3]
offset = content.index(quote)
closing = content.index(quote, offset + 3) + 3
if closing < len(content) and content[closing] == "\n":
closing += 1
return closing
return 0
def _update_file(filepath: Path, index_block: str, *, check: bool = False) -> bool:
"""Insert or replace the task index in a file. Returns True if changed."""
content = filepath.read_text(encoding="utf-8")
marker_start = None
if MARKER_START in content and MARKER_END in content:
marker_start = MARKER_START
else:
for line in content.splitlines():
if line.startswith(MARKER_PREFIX):
marker_start = line
break
if marker_start and MARKER_END in content:
start = content.index(marker_start)
end = content.index(MARKER_END, start) + len(MARKER_END)
if end < len(content) and content[end] == "\n":
end += 1
old_block = content[start:end].rstrip("\n")
if old_block == index_block:
return False
if check:
return True
new_content = content[:start] + index_block + "\n" + content[end:]
else:
if check:
return True
insertion_point = _find_insertion_point(content)
new_content = (
content[:insertion_point]
+ index_block
+ "\n\n"
+ content[insertion_point:]
)
filepath.write_text(new_content, encoding="utf-8")
return True
def main():
check = "--check" in sys.argv
suites = [a for a in sys.argv[1:] if not a.startswith("-")]
registry = TaskRegistry()
if not suites:
suites = registry.list_suites(include_generated=False)
changed = 0
for suite in suites:
tasks = registry._load_suite_tasks(suite)
if not tasks:
print(f" [SKIP] {suite}: no tasks found")
continue
filepath = _resolve_tasks_path(suite)
if filepath is None:
print(f" [SKIP] {suite}: no tasks.py (defs/-only suite)")
continue
if not filepath.exists():
print(f" [SKIP] {suite}: {filepath} not found")
continue
source_order = _get_class_order(filepath)
index_block = _generate_index(tasks, source_order)
if _update_file(filepath, index_block, check=check):
status = "STALE" if check else "UPDATED"
print(f" [{status}] {suite}: {filepath.relative_to(ROOT)}")
changed += 1
else:
print(f" [OK] {suite}: up to date")
if check and changed:
print(
f"\n{changed} file(s) have stale task indexes."
" Run without --check to update."
)
sys.exit(1)
elif not check:
print(f"\n{changed} file(s) updated.")
if __name__ == "__main__":
main()