#!/usr/bin/env python3 from __future__ import annotations import argparse import re import sys from pathlib import Path VERSION_PATTERN = re.compile(r'^(\s*__version__\s*=\s*")(\d+)\.(\d+)\.(\d+)(".*)$') def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Bump hytop version in src/hytop/__init__.py" ) parser.add_argument( "action", choices=["patch", "minor", "major", "set"], help="Bump strategy or set an explicit version", ) parser.add_argument( "version", nargs="?", help="Target version when action is 'set' (format: X.Y.Z)", ) parser.add_argument( "--dry-run", action="store_true", help="Print the next version without writing files", ) return parser.parse_args() def parse_semver(version_text: str) -> tuple[int, int, int]: match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", version_text) if not match: raise ValueError(f"Invalid version: {version_text!r}. Expected format: X.Y.Z") major, minor, patch = (int(part) for part in match.groups()) return major, minor, patch def bump_version(current: tuple[int, int, int], action: str) -> tuple[int, int, int]: major, minor, patch = current if action == "patch": return major, minor, patch + 1 if action == "minor": return major, minor + 1, 0 if action == "major": return major + 1, 0, 0 raise ValueError(f"Unsupported bump action: {action}") def main() -> int: args = parse_args() root = Path(__file__).resolve().parents[1] init_file = root / "src" / "hytop" / "__init__.py" if not init_file.exists(): print(f"Cannot find version file: {init_file}", file=sys.stderr) return 1 lines = init_file.read_text(encoding="utf-8").splitlines(keepends=True) matched_index = -1 matched_groups: tuple[str, str, str, str, str] | None = None for i, line in enumerate(lines): match = VERSION_PATTERN.match(line.rstrip("\r\n")) if match: if matched_index != -1: print("Found multiple __version__ lines; aborting.", file=sys.stderr) return 1 matched_index = i matched_groups = match.groups() if matched_index == -1 or matched_groups is None: print("Cannot find __version__ declaration in __init__.py", file=sys.stderr) return 1 prefix, major_text, minor_text, patch_text, suffix = matched_groups current_tuple = (int(major_text), int(minor_text), int(patch_text)) current_version = ".".join((major_text, minor_text, patch_text)) if args.action == "set": if not args.version: print("Action 'set' requires a version argument (X.Y.Z).", file=sys.stderr) return 1 try: next_tuple = parse_semver(args.version) except ValueError as exc: print(str(exc), file=sys.stderr) return 1 else: if args.version: print("Version argument is only allowed with action 'set'.", file=sys.stderr) return 1 next_tuple = bump_version(current_tuple, args.action) next_version = ".".join(str(part) for part in next_tuple) if next_version == current_version: print(f"Version unchanged: {current_version}") return 0 new_line = f"{prefix}{next_version}{suffix}" line_ending = "\n" if lines[matched_index].endswith("\r\n"): line_ending = "\r\n" lines[matched_index] = f"{new_line}{line_ending}" if args.dry_run: print(f"{current_version} -> {next_version} (dry-run)") return 0 init_file.write_text("".join(lines), encoding="utf-8") print(f"{current_version} -> {next_version}") return 0 if __name__ == "__main__": raise SystemExit(main())