#!/usr/bin/env python """ Quick update for test files from CPython. Usage: # Library + test: copy lib, then patch + auto-mark test + commit python scripts/update_lib quick cpython/Lib/dataclasses.py # Shortcut: just the module name python scripts/update_lib quick dataclasses # Test file: patch + auto-mark python scripts/update_lib quick cpython/Lib/test/test_foo.py # Test file: migrate only python scripts/update_lib quick cpython/Lib/test/test_foo.py --no-auto-mark # Test file: auto-mark only (Lib/ path implies --no-migrate) python scripts/update_lib quick Lib/test/test_foo.py # Directory: patch all + auto-mark all python scripts/update_lib quick cpython/Lib/test/test_dataclasses/ # Skip git commit python scripts/update_lib quick dataclasses --no-commit """ import argparse import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).parent.parent)) from update_lib.path import ( get_test_files, is_lib_path, is_test_path, lib_to_test_path, parse_lib_path, ) def collect_original_methods( lib_path: pathlib.Path, ) -> set[tuple[str, str]] | dict[pathlib.Path, set[tuple[str, str]]] | None: """ Collect original test methods from lib path before patching. Returns: - For file: set of (class_name, method_name) or None if file doesn't exist - For directory: dict mapping file path to set of methods, or None if dir doesn't exist """ from update_lib.auto_mark import extract_test_methods if not lib_path.exists(): return None if lib_path.is_file(): return extract_test_methods(lib_path.read_text()) else: result = {} for lib_file in get_test_files(lib_path): result[lib_file.resolve()] = extract_test_methods(lib_file.read_text()) return result def quick( src_path: pathlib.Path, no_migrate: bool = False, no_auto_mark: bool = False, mark_failure: bool = False, verbose: bool = True, skip_build: bool = False, ) -> None: """ Process a file or directory: migrate + auto-mark. Args: src_path: Source path (file or directory) no_migrate: Skip migration step no_auto_mark: Skip auto-mark step mark_failure: Add @expectedFailure to ALL failing tests verbose: Print progress messages skip_build: Skip cargo build, use pre-built binary """ from update_lib.auto_mark import auto_mark_directory, auto_mark_file from update_lib.migrate import patch_directory, patch_file # Determine lib_path and whether to migrate if is_lib_path(src_path): no_migrate = True lib_path = src_path else: lib_path = parse_lib_path(src_path) is_dir = src_path.is_dir() # Capture original test methods before migration (for smart auto-mark) original_methods = collect_original_methods(lib_path) # Step 1: Migrate if not no_migrate: if is_dir: patch_directory(src_path, lib_path, verbose=verbose) else: patch_file(src_path, lib_path, verbose=verbose) # Step 2: Auto-mark if not no_auto_mark: if not lib_path.exists(): raise FileNotFoundError(f"Path not found: {lib_path}") if is_dir: num_added, num_removed, _ = auto_mark_directory( lib_path, mark_failure=mark_failure, verbose=verbose, original_methods_per_file=original_methods, skip_build=skip_build, ) else: num_added, num_removed, _ = auto_mark_file( lib_path, mark_failure=mark_failure, verbose=verbose, original_methods=original_methods, skip_build=skip_build, ) if verbose: if num_added: print(f"Added expectedFailure to {num_added} tests") print(f"Removed expectedFailure from {num_removed} tests") def get_cpython_dir(src_path: pathlib.Path) -> pathlib.Path: """Extract cpython directory from source path. Example: cpython/Lib/dataclasses.py -> cpython /some/path/cpython/Lib/foo.py -> /some/path/cpython """ path_str = str(src_path).replace("\\", "/") lib_marker = "/Lib/" if lib_marker in path_str: idx = path_str.index(lib_marker) return pathlib.Path(path_str[:idx]) # Shortcut case: assume "cpython" return pathlib.Path("cpython") def get_cpython_version(cpython_dir: pathlib.Path) -> str: """Get CPython version from git tag.""" import subprocess result = subprocess.run( ["git", "describe", "--tags"], cwd=cpython_dir, capture_output=True, text=True, check=True, ) return result.stdout.strip() def git_commit( name: str, lib_path: pathlib.Path | None, test_path: pathlib.Path | None, cpython_dir: pathlib.Path, verbose: bool = True, ) -> bool: """Commit changes with CPython author. Args: name: Module name (e.g., "dataclasses") lib_path: Path to library file/directory (or None) test_path: Path to test file/directory (or None) cpython_dir: Path to cpython directory verbose: Print progress messages Returns: True if commit was created, False otherwise """ import subprocess # Stage changes paths_to_add = [] if lib_path and lib_path.exists(): paths_to_add.append(str(lib_path)) if test_path and test_path.exists(): paths_to_add.append(str(test_path)) if not paths_to_add: return False version = get_cpython_version(cpython_dir) subprocess.run(["git", "add"] + paths_to_add, check=True) # Check if there are staged changes result = subprocess.run( ["git", "diff", "--cached", "--quiet"], capture_output=True, ) if result.returncode == 0: if verbose: print("No changes to commit") return False # Commit with CPython author message = f"Update {name} from {version}" subprocess.run( [ "git", "commit", "--author", "CPython Developers <>", "-m", message, ], check=True, ) if verbose: print(f"Committed: {message}") return True def _expand_shortcut(path: pathlib.Path) -> pathlib.Path: """Expand simple name to cpython/Lib path if it exists. Examples: dataclasses -> cpython/Lib/dataclasses.py (if exists) json -> cpython/Lib/json/ (if exists) test_types -> cpython/Lib/test/test_types.py (if exists) """ # Only expand if it's a simple name (no path separators) and doesn't exist if "/" in str(path) or path.exists(): return path name = str(path) # Test shortcut: test_foo -> cpython/Lib/test/test_foo if name.startswith("test_"): dir_path = pathlib.Path(f"cpython/Lib/test/{name}") if dir_path.exists(): return dir_path file_path = pathlib.Path(f"cpython/Lib/test/{name}.py") if file_path.exists(): return file_path # Library shortcut: foo -> cpython/Lib/foo file_path = pathlib.Path(f"cpython/Lib/{name}.py") if file_path.exists(): return file_path dir_path = pathlib.Path(f"cpython/Lib/{name}") if dir_path.exists(): return dir_path # Return original (will likely fail later with a clear error) return path def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "path", type=pathlib.Path, help="Source path (file or directory)", ) parser.add_argument( "--copy", action=argparse.BooleanOptionalAction, default=True, help="Copy library file (default: enabled, implied disabled if test path)", ) parser.add_argument( "--migrate", action=argparse.BooleanOptionalAction, default=True, help="Migrate test file (default: enabled, implied disabled if Lib/ path)", ) parser.add_argument( "--auto-mark", action=argparse.BooleanOptionalAction, default=True, help="Auto-mark test failures (default: enabled)", ) parser.add_argument( "--mark-failure", action="store_true", help="Add @expectedFailure to failing tests", ) parser.add_argument( "--commit", action=argparse.BooleanOptionalAction, default=True, help="Create git commit (default: enabled)", ) parser.add_argument( "--build", action=argparse.BooleanOptionalAction, default=True, help="Build with cargo (default: enabled)", ) args = parser.parse_args(argv) try: src_path = args.path # Shortcut: expand simple name to cpython/Lib path src_path = _expand_shortcut(src_path) original_src = src_path # Keep for commit # Track library path for commit lib_file_path = None test_path = None # If it's a library path (not test path), do copy_lib first if not is_test_path(src_path): # Get library destination path for commit lib_file_path = parse_lib_path(src_path) if args.copy: from update_lib.copy_lib import copy_lib copy_lib(src_path) # Convert to test path src_path = lib_to_test_path(original_src) if not src_path.exists(): print(f"Test path does not exist: {src_path}") return 1 test_path = parse_lib_path(src_path) if not is_lib_path(src_path) else src_path # Process the test path quick( src_path, no_migrate=not args.migrate, no_auto_mark=not args.auto_mark, mark_failure=args.mark_failure, skip_build=not args.build, ) # Step 3: Git commit if args.commit: # Extract module name from path name = original_src.stem if name == "__init__": name = original_src.parent.name cpython_dir = get_cpython_dir(original_src) git_commit(name, lib_file_path, test_path, cpython_dir) return 0 except ValueError as e: print(f"Error: {e}", file=sys.stderr) return 1 except FileNotFoundError as e: print(f"Error: {e}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())