Files
RustPython/scripts/update_lib/quick.py
T
2026-01-20 11:38:26 +09:00

366 lines
10 KiB
Python

#!/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(
"--no-copy",
action="store_true",
help="Skip copy-lib step (implied if path is a test path)",
)
parser.add_argument(
"--no-migrate",
action="store_true",
help="Skip migration step (implied if path starts with Lib/)",
)
parser.add_argument(
"--no-auto-mark",
action="store_true",
help="Skip auto-mark step",
)
parser.add_argument(
"--mark-failure",
action="store_true",
help="Add @expectedFailure to failing tests",
)
parser.add_argument(
"--no-commit",
action="store_true",
help="Skip git commit step",
)
parser.add_argument(
"--skip-build",
action="store_true",
help="Skip cargo build, use pre-built binary",
)
args = parser.parse_args(argv)
try:
src_path = args.path
no_copy = args.no_copy
# 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 not no_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=args.no_migrate,
no_auto_mark=args.no_auto_mark,
mark_failure=args.mark_failure,
skip_build=args.skip_build,
)
# Step 3: Git commit
if not args.no_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())