diff --git a/cli/python/base_release/engine.py b/cli/python/base_release/engine.py index c1a1358..923f06e 100644 --- a/cli/python/base_release/engine.py +++ b/cli/python/base_release/engine.py @@ -2,9 +2,6 @@ import subprocess # pylint: disable=unused-import import sys -from dataclasses import dataclass -from pathlib import Path -from typing import TextIO import base_cli from base_setup import process # pylint: disable=unused-import @@ -12,6 +9,10 @@ from base_setup.manifest import read_manifest from .release_model import ReleaseContext, ReleaseError, ReleaseFinding +from .release_parser import ReleaseArguments +from .release_parser import ReleaseUsageError +from .release_parser import parse_release_args +from .release_parser import print_usage # pylint: disable=unused-import from .release_publish import RELEASE_STEP_TIMEOUT_SECONDS from .release_publish import release_publish_recovery_guidance, require_interactive_publish_confirmation @@ -31,27 +32,6 @@ ) -class ReleaseUsageError(RuntimeError): - pass - - -@dataclass(frozen=True) -class ReleaseArguments: - command: str - version: str - manifest_path: Path | None - dry_run: bool = False - yes: bool = False - - -@dataclass -class ReleaseOptionState: - version: str | None = None - manifest_path: Path | None = None - dry_run: bool = False - yes: bool = False - - def main(argv: list[str] | None = None) -> int: return base_cli.run_app(app, argv) @@ -95,87 +75,6 @@ def print_error(exc: ManifestError | ReleaseError) -> None: print(guidance, file=sys.stderr) -def parse_release_args(arguments: tuple[str, ...]) -> ReleaseArguments: - if not arguments or arguments[0] in ("-h", "--help", "help"): - print_usage() - raise SystemExit(0) - - command = arguments[0] - if command not in ("check", "plan", "notes", "publish"): - raise ReleaseUsageError(f"Unknown release command '{command}'.") - - state = ReleaseOptionState() - remaining = list(arguments[1:]) - index = 0 - while index < len(remaining): - index = parse_release_option(command, remaining, index, state) - - if state.version is None: - raise ReleaseUsageError(f"The 'release {command}' command requires --version.") - return ReleaseArguments( - command=command, - version=state.version, - manifest_path=state.manifest_path, - dry_run=state.dry_run, - yes=state.yes, - ) - - -def parse_release_option( - command: str, - arguments: list[str], - index: int, - state: ReleaseOptionState, -) -> int: - arg = arguments[index] - if arg in ("-h", "--help"): - print_usage() - raise SystemExit(0) - if arg == "--version": - state.version = read_release_option_value(arguments, index, "--version") - return index + 2 - if arg == "--manifest": - state.manifest_path = Path(read_release_option_value(arguments, index, "--manifest")).expanduser() - return index + 2 - if arg == "--dry-run": - require_publish_option(command, "--dry-run") - state.dry_run = True - return index + 1 - if arg == "--yes": - require_publish_option(command, "--yes") - state.yes = True - return index + 1 - raise ReleaseUsageError(f"Unknown release {command} option '{arg}'.") - - -def read_release_option_value(arguments: list[str], index: int, option_name: str) -> str: - value_index = index + 1 - if value_index >= len(arguments) or not arguments[value_index]: - raise ReleaseUsageError(f"Option '{option_name}' requires an argument.") - return arguments[value_index] - - -def require_publish_option(command: str, option_name: str) -> None: - if command != "publish": - raise ReleaseUsageError(f"Option '{option_name}' is only supported by release publish.") - - -def print_usage(file: TextIO = sys.stdout) -> None: - command = base_cli.delegated_display_command("base_release") - print( - f"""Usage: - {command} check --version [--manifest ] - {command} plan --version [--manifest ] - {command} notes --version [--manifest ] - {command} publish --version [--manifest ] [--dry-run] [--yes] - -Purpose: - Inspect release readiness and guarded GitHub publishing for a Base-managed - project. Homebrew tap changes remain a manual handoff.""", - file=file, - ) - - def build_release_context(ctx: base_cli.Context, args: ReleaseArguments) -> ReleaseContext: manifest_path = args.manifest_path or ctx.manifest_path if manifest_path is None: diff --git a/cli/python/base_release/release_parser.py b/cli/python/base_release/release_parser.py new file mode 100644 index 0000000..4627a16 --- /dev/null +++ b/cli/python/base_release/release_parser.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import TextIO + +import base_cli + + +class ReleaseUsageError(RuntimeError): + pass + + +@dataclass(frozen=True) +class ReleaseArguments: + command: str + version: str + manifest_path: Path | None + dry_run: bool = False + yes: bool = False + + +@dataclass +class ReleaseOptionState: + version: str | None = None + manifest_path: Path | None = None + dry_run: bool = False + yes: bool = False + + +def parse_release_args(arguments: tuple[str, ...]) -> ReleaseArguments: + if not arguments or arguments[0] in ("-h", "--help", "help"): + print_usage() + raise SystemExit(0) + + command = arguments[0] + if command not in ("check", "plan", "notes", "publish"): + raise ReleaseUsageError(f"Unknown release command '{command}'.") + + state = ReleaseOptionState() + remaining = list(arguments[1:]) + index = 0 + while index < len(remaining): + index = parse_release_option(command, remaining, index, state) + + if state.version is None: + raise ReleaseUsageError(f"The 'release {command}' command requires --version.") + return ReleaseArguments( + command=command, + version=state.version, + manifest_path=state.manifest_path, + dry_run=state.dry_run, + yes=state.yes, + ) + + +def parse_release_option( + command: str, + arguments: list[str], + index: int, + state: ReleaseOptionState, +) -> int: + arg = arguments[index] + if arg in ("-h", "--help"): + print_usage() + raise SystemExit(0) + if arg == "--version": + state.version = read_release_option_value(arguments, index, "--version") + return index + 2 + if arg == "--manifest": + state.manifest_path = Path(read_release_option_value(arguments, index, "--manifest")).expanduser() + return index + 2 + if arg == "--dry-run": + require_publish_option(command, "--dry-run") + state.dry_run = True + return index + 1 + if arg == "--yes": + require_publish_option(command, "--yes") + state.yes = True + return index + 1 + raise ReleaseUsageError(f"Unknown release {command} option '{arg}'.") + + +def read_release_option_value(arguments: list[str], index: int, option_name: str) -> str: + value_index = index + 1 + if value_index >= len(arguments) or not arguments[value_index]: + raise ReleaseUsageError(f"Option '{option_name}' requires an argument.") + return arguments[value_index] + + +def require_publish_option(command: str, option_name: str) -> None: + if command != "publish": + raise ReleaseUsageError(f"Option '{option_name}' is only supported by release publish.") + + +def print_usage(file: TextIO = sys.stdout) -> None: + command = base_cli.delegated_display_command("base_release") + print( + f"""Usage: + {command} check --version [--manifest ] + {command} plan --version [--manifest ] + {command} notes --version [--manifest ] + {command} publish --version [--manifest ] [--dry-run] [--yes] + +Purpose: + Inspect release readiness and guarded GitHub publishing for a Base-managed + project. Homebrew tap changes remain a manual handoff.""", + file=file, + ) diff --git a/cli/python/base_release/tests/test_engine_structure.py b/cli/python/base_release/tests/test_engine_structure.py index becfdb2..b7d3a9d 100644 --- a/cli/python/base_release/tests/test_engine_structure.py +++ b/cli/python/base_release/tests/test_engine_structure.py @@ -21,3 +21,12 @@ def test_release_publish_helpers_are_split_from_engine() -> None: assert publish_path.exists() assert "def run_release_step" in publish_path.read_text(encoding="utf-8") assert "def run_release_step" not in engine_path.read_text(encoding="utf-8") + + +def test_release_parser_helpers_are_split_from_engine() -> None: + engine_path = Path(engine.__file__) + parser_path = engine_path.with_name("release_parser.py") + + assert parser_path.exists() + assert "def parse_release_args" in parser_path.read_text(encoding="utf-8") + assert "def parse_release_args" not in engine_path.read_text(encoding="utf-8")