|
8 | 8 | """ |
9 | 9 |
|
10 | 10 | import ast |
| 11 | +import difflib |
11 | 12 | import functools |
12 | 13 | import pathlib |
13 | 14 | import re |
@@ -1011,6 +1012,119 @@ def is_up_to_date(name: str, cpython_prefix: str, lib_prefix: str) -> bool: |
1011 | 1012 | return found_any |
1012 | 1013 |
|
1013 | 1014 |
|
| 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 | + |
1014 | 1128 | def get_test_dependencies( |
1015 | 1129 | test_path: pathlib.Path, |
1016 | 1130 | ) -> dict[str, list[pathlib.Path]]: |
|
0 commit comments