|
| 1 | +#!/usr/bin/env python |
| 2 | +""" |
| 3 | +Show prioritized list of modules to update. |
| 4 | +
|
| 5 | +Usage: |
| 6 | + python scripts/update_lib todo |
| 7 | + python scripts/update_lib todo --limit 20 |
| 8 | +""" |
| 9 | + |
| 10 | +import argparse |
| 11 | +import pathlib |
| 12 | +import sys |
| 13 | + |
| 14 | +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) |
| 15 | + |
| 16 | + |
| 17 | +def compute_todo_list( |
| 18 | + cpython_prefix: str = "cpython", |
| 19 | + lib_prefix: str = "Lib", |
| 20 | + include_done: bool = False, |
| 21 | +) -> list[dict]: |
| 22 | + """Compute prioritized list of modules to update. |
| 23 | +
|
| 24 | + Scoring: |
| 25 | + - Modules with no pylib dependencies: score = -1 |
| 26 | + - Modules with pylib dependencies: score = count of NOT up-to-date deps |
| 27 | +
|
| 28 | + Sorting (ascending by score): |
| 29 | + 1. More reverse dependencies (modules depending on this) = higher priority |
| 30 | + 2. Fewer native dependencies = higher priority |
| 31 | +
|
| 32 | + Returns: |
| 33 | + List of dicts with module info, sorted by priority |
| 34 | + """ |
| 35 | + from update_lib.deps import get_rust_deps, get_soft_deps, is_up_to_date |
| 36 | + from update_lib.show_deps import get_all_modules |
| 37 | + |
| 38 | + all_modules = get_all_modules(cpython_prefix) |
| 39 | + |
| 40 | + # Build dependency data for all modules |
| 41 | + module_data = {} |
| 42 | + for name in all_modules: |
| 43 | + soft_deps = get_soft_deps(name, cpython_prefix) |
| 44 | + native_deps = get_rust_deps(name, cpython_prefix) |
| 45 | + up_to_date = is_up_to_date(name, cpython_prefix, lib_prefix) |
| 46 | + |
| 47 | + module_data[name] = { |
| 48 | + "name": name, |
| 49 | + "soft_deps": soft_deps, |
| 50 | + "native_deps": native_deps, |
| 51 | + "up_to_date": up_to_date, |
| 52 | + } |
| 53 | + |
| 54 | + # Build reverse dependency map: who depends on this module |
| 55 | + reverse_deps: dict[str, set[str]] = {name: set() for name in all_modules} |
| 56 | + for name, data in module_data.items(): |
| 57 | + for dep in data["soft_deps"]: |
| 58 | + if dep in reverse_deps: |
| 59 | + reverse_deps[dep].add(name) |
| 60 | + |
| 61 | + # Compute scores and filter |
| 62 | + result = [] |
| 63 | + for name, data in module_data.items(): |
| 64 | + # Skip already up-to-date modules (unless --done) |
| 65 | + if data["up_to_date"] and not include_done: |
| 66 | + continue |
| 67 | + |
| 68 | + soft_deps = data["soft_deps"] |
| 69 | + if not soft_deps: |
| 70 | + # No pylib dependencies |
| 71 | + score = -1 |
| 72 | + total_deps = 0 |
| 73 | + else: |
| 74 | + # Count NOT up-to-date dependencies |
| 75 | + score = sum( |
| 76 | + 1 |
| 77 | + for dep in soft_deps |
| 78 | + if dep in module_data and not module_data[dep]["up_to_date"] |
| 79 | + ) |
| 80 | + total_deps = len(soft_deps) |
| 81 | + |
| 82 | + result.append( |
| 83 | + { |
| 84 | + "name": name, |
| 85 | + "score": score, |
| 86 | + "total_deps": total_deps, |
| 87 | + "reverse_deps": reverse_deps[name], |
| 88 | + "reverse_deps_count": len(reverse_deps[name]), |
| 89 | + "native_deps_count": len(data["native_deps"]), |
| 90 | + "native_deps": data["native_deps"], |
| 91 | + "soft_deps": soft_deps, |
| 92 | + "up_to_date": data["up_to_date"], |
| 93 | + } |
| 94 | + ) |
| 95 | + |
| 96 | + # Sort by: |
| 97 | + # 1. score (ascending) - fewer outstanding deps first |
| 98 | + # 2. reverse_deps_count (descending) - more dependents first |
| 99 | + # 3. native_deps_count (ascending) - fewer native deps first |
| 100 | + result.sort( |
| 101 | + key=lambda x: ( |
| 102 | + x["score"], |
| 103 | + -x["reverse_deps_count"], |
| 104 | + x["native_deps_count"], |
| 105 | + ) |
| 106 | + ) |
| 107 | + |
| 108 | + return result |
| 109 | + |
| 110 | + |
| 111 | +def format_todo_list( |
| 112 | + todo_list: list[dict], |
| 113 | + limit: int | None = None, |
| 114 | + verbose: bool = False, |
| 115 | +) -> list[str]: |
| 116 | + """Format todo list for display. |
| 117 | +
|
| 118 | + Args: |
| 119 | + todo_list: List from compute_todo_list() |
| 120 | + limit: Maximum number of items to show |
| 121 | + verbose: Show detailed dependency information |
| 122 | +
|
| 123 | + Returns: |
| 124 | + List of formatted lines |
| 125 | + """ |
| 126 | + lines = [] |
| 127 | + |
| 128 | + if limit: |
| 129 | + todo_list = todo_list[:limit] |
| 130 | + |
| 131 | + for item in todo_list: |
| 132 | + name = item["name"] |
| 133 | + score = item["score"] |
| 134 | + total_deps = item["total_deps"] |
| 135 | + rev_count = item["reverse_deps_count"] |
| 136 | + |
| 137 | + done_mark = "[x]" if item["up_to_date"] else "[ ]" |
| 138 | + |
| 139 | + if score == -1: |
| 140 | + score_str = "no deps" |
| 141 | + else: |
| 142 | + score_str = f"{score}/{total_deps} deps" |
| 143 | + |
| 144 | + rev_str = f"{rev_count} dependents" if rev_count else "" |
| 145 | + |
| 146 | + parts = [done_mark, f"[{score_str}]", name] |
| 147 | + if rev_str: |
| 148 | + parts.append(f"({rev_str})") |
| 149 | + |
| 150 | + lines.append(" ".join(filter(None, parts))) |
| 151 | + |
| 152 | + # Verbose mode: show detailed dependency info |
| 153 | + if verbose: |
| 154 | + if item["reverse_deps"]: |
| 155 | + lines.append(f" dependents: {', '.join(sorted(item['reverse_deps']))}") |
| 156 | + if item["soft_deps"]: |
| 157 | + lines.append(f" python: {', '.join(sorted(item['soft_deps']))}") |
| 158 | + if item["native_deps"]: |
| 159 | + lines.append(f" native: {', '.join(sorted(item['native_deps']))}") |
| 160 | + |
| 161 | + return lines |
| 162 | + |
| 163 | + |
| 164 | +def show_todo( |
| 165 | + cpython_prefix: str = "cpython", |
| 166 | + lib_prefix: str = "Lib", |
| 167 | + limit: int | None = None, |
| 168 | + include_done: bool = False, |
| 169 | + verbose: bool = False, |
| 170 | +) -> None: |
| 171 | + """Show prioritized list of modules to update.""" |
| 172 | + todo_list = compute_todo_list(cpython_prefix, lib_prefix, include_done) |
| 173 | + for line in format_todo_list(todo_list, limit, verbose): |
| 174 | + print(line) |
| 175 | + |
| 176 | + |
| 177 | +def main(argv: list[str] | None = None) -> int: |
| 178 | + parser = argparse.ArgumentParser( |
| 179 | + description=__doc__, |
| 180 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 181 | + ) |
| 182 | + parser.add_argument( |
| 183 | + "--cpython", |
| 184 | + default="cpython", |
| 185 | + help="CPython directory prefix (default: cpython)", |
| 186 | + ) |
| 187 | + parser.add_argument( |
| 188 | + "--lib", |
| 189 | + default="Lib", |
| 190 | + help="Local Lib directory prefix (default: Lib)", |
| 191 | + ) |
| 192 | + parser.add_argument( |
| 193 | + "--limit", |
| 194 | + type=int, |
| 195 | + default=None, |
| 196 | + help="Maximum number of items to show", |
| 197 | + ) |
| 198 | + parser.add_argument( |
| 199 | + "--done", |
| 200 | + action="store_true", |
| 201 | + help="Include already up-to-date modules", |
| 202 | + ) |
| 203 | + parser.add_argument( |
| 204 | + "--verbose", |
| 205 | + "-v", |
| 206 | + action="store_true", |
| 207 | + help="Show detailed dependency information", |
| 208 | + ) |
| 209 | + |
| 210 | + args = parser.parse_args(argv) |
| 211 | + |
| 212 | + try: |
| 213 | + show_todo(args.cpython, args.lib, args.limit, args.done, args.verbose) |
| 214 | + return 0 |
| 215 | + except Exception as e: |
| 216 | + print(f"Error: {e}", file=sys.stderr) |
| 217 | + return 1 |
| 218 | + |
| 219 | + |
| 220 | +if __name__ == "__main__": |
| 221 | + sys.exit(main()) |
0 commit comments