#!/usr/bin/env python3 """Update or verify ferro-ta version strings across release files. Usage ----- python3 scripts/bump_version.py 1.0.3 python3 scripts/bump_version.py --check python3 scripts/bump_version.py --show """ from __future__ import annotations import argparse import re from dataclasses import dataclass from pathlib import Path ROOT = Path(__file__).resolve().parent.parent SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$") @dataclass(frozen=True) class VersionCarrier: label: str path: Path pattern: str replacement: str def read(self) -> str: text = self.path.read_text(encoding="utf-8") match = re.search(self.pattern, text, flags=re.MULTILINE) if not match: raise ValueError(f"Could not find version for {self.label} in {self.path}") return match.group(2) def write(self, version: str) -> bool: text = self.path.read_text(encoding="utf-8") updated, count = re.subn( self.pattern, rf"\g<1>{version}\g<3>", text, count=1, flags=re.MULTILINE, ) if count != 1: raise ValueError(f"Could not update {self.label} in {self.path}") changed = updated != text if changed: self.path.write_text(updated, encoding="utf-8") return changed CARRIERS = [ VersionCarrier( "cargo_root", ROOT / "Cargo.toml", r'(?m)^(version = ")([^"]+)(")$', r"\g<1>{version}\g<3>", ), VersionCarrier( "cargo_core_dep", ROOT / "Cargo.toml", r'(ferro_ta_core = \{ path = "crates/ferro_ta_core", version = ")([^"]+)("[^}]*\})', r"\g<1>{version}\g<3>", ), VersionCarrier( "cargo_core_crate", ROOT / "crates" / "ferro_ta_core" / "Cargo.toml", r'(?m)^(version = ")([^"]+)(")$', r"\g<1>{version}\g<3>", ), VersionCarrier( "cargo_core_readme", ROOT / "crates" / "ferro_ta_core" / "README.md", r'(ferro_ta_core = ")([^"]+)(")', r"\g<1>{version}\g<3>", ), VersionCarrier( "pyproject", ROOT / "pyproject.toml", r'(?m)^(version = ")([^"]+)(")$', r"\g<1>{version}\g<3>", ), VersionCarrier( "wasm_cargo", ROOT / "wasm" / "Cargo.toml", r'(?m)^(version = ")([^"]+)(")$', r"\g<1>{version}\g<3>", ), VersionCarrier( "wasm_package", ROOT / "wasm" / "package.json", r'("version": ")([^"]+)(")', r"\g<1>{version}\g<3>", ), VersionCarrier( "conda", ROOT / "conda" / "meta.yaml", r'({% set version = ")([^"]+)(" %})', r"\g<1>{version}\g<3>", ), VersionCarrier( "docs_changelog", ROOT / "docs" / "changelog.rst", r"(These docs track package version ``)([^`]+)(``\.)", r"\g<1>{version}\g<3>", ), VersionCarrier( "docs_support_matrix", ROOT / "docs" / "support_matrix.rst", r"(These docs track package version ``)([^`]+)(``\.)", r"\g<1>{version}\g<3>", ), ] def _read_versions() -> dict[str, str]: return {carrier.label: carrier.read() for carrier in CARRIERS} def _print_versions(versions: dict[str, str]) -> None: for label, version in versions.items(): print(f"{label:20} {version}") def _check_versions() -> int: versions = _read_versions() unique = sorted(set(versions.values())) _print_versions(versions) if len(unique) != 1: print() print(f"ERROR: version mismatch detected: {', '.join(unique)}") return 1 print() print(f"OK: all tracked versions match {unique[0]}") return 0 def _set_version(version: str) -> int: if not SEMVER_RE.match(version): print(f"ERROR: expected MAJOR.MINOR.PATCH, got {version!r}") return 1 changed_paths: list[Path] = [] for carrier in CARRIERS: if carrier.write(version): changed_paths.append(carrier.path) if changed_paths: print(f"Updated version to {version}:") for path in sorted(set(changed_paths)): print(f" - {path.relative_to(ROOT)}") else: print(f"No changes needed. All tracked files already use {version}.") return 0 def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("version", nargs="?", help="New version to write") parser.add_argument( "--check", action="store_true", help="Fail if tracked version strings do not match", ) parser.add_argument( "--show", action="store_true", help="Print tracked version strings without modifying files", ) args = parser.parse_args() if args.check: return _check_versions() if args.show: _print_versions(_read_versions()) return 0 if args.version: return _set_version(args.version) parser.print_help() return 1 if __name__ == "__main__": raise SystemExit(main())