Skip to content

Commit d2a9937

Browse files
authored
[update_lib] todo shows last updated date (RustPython#7053)
1 parent 4daac23 commit d2a9937

2 files changed

Lines changed: 160 additions & 4 deletions

File tree

scripts/update_lib/cmd_todo.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
from update_lib.deps import (
1717
count_test_todos,
18+
get_module_diff_stat,
19+
get_module_last_updated,
20+
get_test_last_updated,
1821
is_test_tracked,
1922
is_test_up_to_date,
2023
)
@@ -368,6 +371,18 @@ def compute_test_todo_list(
368371
return result
369372

370373

374+
def _format_meta_suffix(item: dict) -> str:
375+
"""Format metadata suffix (last updated date and diff count)."""
376+
parts = []
377+
last_updated = item.get("last_updated")
378+
diff_lines = item.get("diff_lines", 0)
379+
if last_updated:
380+
parts.append(last_updated)
381+
if diff_lines > 0:
382+
parts.append(f"Δ{diff_lines}")
383+
return f" | {' '.join(parts)}" if parts else ""
384+
385+
371386
def _format_test_suffix(item: dict) -> str:
372387
"""Format suffix for test item (TODO count or untracked)."""
373388
tracked = item.get("tracked", True)
@@ -410,13 +425,15 @@ def format_test_todo_list(
410425
primary = tests[0]
411426
done_mark = "[x]" if primary["up_to_date"] else "[ ]"
412427
suffix = _format_test_suffix(primary)
413-
lines.append(f"- {done_mark} {primary['name']}{suffix}")
428+
meta = _format_meta_suffix(primary)
429+
lines.append(f"- {done_mark} {primary['name']}{suffix}{meta}")
414430

415431
# Rest are indented
416432
for item in tests[1:]:
417433
done_mark = "[x]" if item["up_to_date"] else "[ ]"
418434
suffix = _format_test_suffix(item)
419-
lines.append(f" - {done_mark} {item['name']}{suffix}")
435+
meta = _format_meta_suffix(item)
436+
lines.append(f" - {done_mark} {item['name']}{suffix}{meta}")
420437

421438
return lines
422439

@@ -462,7 +479,8 @@ def format_todo_list(
462479
if rev_str:
463480
parts.append(f"({rev_str})")
464481

465-
lines.append(" ".join(parts))
482+
line = " ".join(parts) + _format_meta_suffix(item)
483+
lines.append(line)
466484

467485
# Show hard_deps:
468486
# - Normal mode: only show if lib is up-to-date but hard_deps are not
@@ -482,7 +500,8 @@ def format_todo_list(
482500
for test_info in test_by_lib[name]:
483501
test_done_mark = "[x]" if test_info["up_to_date"] else "[ ]"
484502
suffix = _format_test_suffix(test_info)
485-
lines.append(f" - {test_done_mark} {test_info['name']}{suffix}")
503+
meta = _format_meta_suffix(test_info)
504+
lines.append(f" - {test_done_mark} {test_info['name']}{suffix}{meta}")
486505

487506
# Verbose mode: show detailed dependency info
488507
if verbose:
@@ -556,6 +575,29 @@ def format_all_todo(
556575
if include_done or lib_not_done or has_pending_test:
557576
lib_todo.append(item)
558577

578+
# Add metadata (last updated date and diff stat) to lib items
579+
for item in lib_todo:
580+
item["last_updated"] = get_module_last_updated(
581+
item["name"], cpython_prefix, lib_prefix
582+
)
583+
item["diff_lines"] = (
584+
0
585+
if item["up_to_date"]
586+
else get_module_diff_stat(item["name"], cpython_prefix, lib_prefix)
587+
)
588+
589+
# Add last_updated to displayed test items (verbose only - slow)
590+
if verbose:
591+
for tests in test_by_lib.values():
592+
for test in tests:
593+
test["last_updated"] = get_test_last_updated(
594+
test["name"], cpython_prefix, lib_prefix
595+
)
596+
for test in no_lib_tests:
597+
test["last_updated"] = get_test_last_updated(
598+
test["name"], cpython_prefix, lib_prefix
599+
)
600+
559601
# Format lib todo with embedded tests
560602
lines.extend(format_todo_list(lib_todo, test_by_lib, limit, verbose))
561603

scripts/update_lib/deps.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import ast
11+
import difflib
1112
import functools
1213
import pathlib
1314
import re
@@ -1011,6 +1012,119 @@ def is_up_to_date(name: str, cpython_prefix: str, lib_prefix: str) -> bool:
10111012
return found_any
10121013

10131014

1015+
def _count_file_diff(file_a: pathlib.Path, file_b: pathlib.Path) -> int:
1016+
"""Count changed lines between two text files using difflib."""
1017+
a_content = safe_read_text(file_a)
1018+
b_content = safe_read_text(file_b)
1019+
if a_content is None or b_content is None:
1020+
return 0
1021+
if a_content == b_content:
1022+
return 0
1023+
a_lines = a_content.splitlines()
1024+
b_lines = b_content.splitlines()
1025+
count = 0
1026+
for line in difflib.unified_diff(a_lines, b_lines, lineterm=""):
1027+
if (line.startswith("+") and not line.startswith("+++")) or (
1028+
line.startswith("-") and not line.startswith("---")
1029+
):
1030+
count += 1
1031+
return count
1032+
1033+
1034+
def _count_path_diff(path_a: pathlib.Path, path_b: pathlib.Path) -> int:
1035+
"""Count changed lines between two paths (file or directory, *.py only)."""
1036+
if path_a.is_file() and path_b.is_file():
1037+
return _count_file_diff(path_a, path_b)
1038+
if path_a.is_dir() and path_b.is_dir():
1039+
total = 0
1040+
a_files = {f.relative_to(path_a) for f in path_a.rglob("*.py")}
1041+
b_files = {f.relative_to(path_b) for f in path_b.rglob("*.py")}
1042+
for rel in a_files & b_files:
1043+
total += _count_file_diff(path_a / rel, path_b / rel)
1044+
for rel in a_files - b_files:
1045+
content = safe_read_text(path_a / rel)
1046+
if content:
1047+
total += len(content.splitlines())
1048+
for rel in b_files - a_files:
1049+
content = safe_read_text(path_b / rel)
1050+
if content:
1051+
total += len(content.splitlines())
1052+
return total
1053+
return 0
1054+
1055+
1056+
def get_module_last_updated(
1057+
name: str, cpython_prefix: str, lib_prefix: str
1058+
) -> str | None:
1059+
"""Get the last git commit date for a module's Lib files."""
1060+
local_paths = []
1061+
for cpython_path in get_lib_paths(name, cpython_prefix):
1062+
if not cpython_path.exists():
1063+
continue
1064+
try:
1065+
rel_path = cpython_path.relative_to(cpython_prefix)
1066+
local_path = pathlib.Path(lib_prefix) / rel_path.relative_to("Lib")
1067+
if local_path.exists():
1068+
local_paths.append(str(local_path))
1069+
except ValueError:
1070+
continue
1071+
if not local_paths:
1072+
return None
1073+
try:
1074+
result = subprocess.run(
1075+
["git", "log", "-1", "--format=%cd", "--date=short", "--"] + local_paths,
1076+
capture_output=True,
1077+
text=True,
1078+
timeout=10,
1079+
)
1080+
if result.returncode == 0 and result.stdout.strip():
1081+
return result.stdout.strip()
1082+
except Exception:
1083+
pass
1084+
return None
1085+
1086+
1087+
def get_module_diff_stat(name: str, cpython_prefix: str, lib_prefix: str) -> int:
1088+
"""Count differing lines between cpython and local Lib for a module."""
1089+
total = 0
1090+
for cpython_path in get_lib_paths(name, cpython_prefix):
1091+
if not cpython_path.exists():
1092+
continue
1093+
try:
1094+
rel_path = cpython_path.relative_to(cpython_prefix)
1095+
local_path = pathlib.Path(lib_prefix) / rel_path.relative_to("Lib")
1096+
except ValueError:
1097+
continue
1098+
if not local_path.exists():
1099+
continue
1100+
total += _count_path_diff(cpython_path, local_path)
1101+
return total
1102+
1103+
1104+
def get_test_last_updated(
1105+
test_name: str, cpython_prefix: str, lib_prefix: str
1106+
) -> str | None:
1107+
"""Get the last git commit date for a test's files."""
1108+
cpython_path = _get_cpython_test_path(test_name, cpython_prefix)
1109+
if cpython_path is None:
1110+
return None
1111+
local_path = _get_local_test_path(cpython_path, lib_prefix)
1112+
if not local_path.exists():
1113+
return None
1114+
try:
1115+
result = subprocess.run(
1116+
["git", "log", "-1", "--format=%cd", "--date=short", "--", str(local_path)],
1117+
capture_output=True,
1118+
text=True,
1119+
timeout=10,
1120+
)
1121+
if result.returncode == 0 and result.stdout.strip():
1122+
return result.stdout.strip()
1123+
except Exception:
1124+
pass
1125+
return None
1126+
1127+
10141128
def get_test_dependencies(
10151129
test_path: pathlib.Path,
10161130
) -> dict[str, list[pathlib.Path]]:

0 commit comments

Comments
 (0)