from __future__ import annotations from dataclasses import dataclass from pathlib import Path import re _VERSION_PATTERN = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$") @dataclass(frozen=True, order=True) class SoftwareVersion: major: int minor: int patch: int @classmethod def parse(cls, value: str) -> "SoftwareVersion": if not isinstance(value, str): raise ValueError("software version must be a string") match = _VERSION_PATTERN.fullmatch(value.strip()) if match is None: raise ValueError("software version must use strict MAJOR.MINOR.PATCH digits") return cls(*(int(part) for part in match.groups())) def __str__(self) -> str: return f"{self.major}.{self.minor}.{self.patch}" def read_software_version(code_root: Path) -> SoftwareVersion: path = Path(code_root) / "VERSION" try: return SoftwareVersion.parse(path.read_text(encoding="utf-8")) except OSError as exc: raise RuntimeError(f"software VERSION file is unreadable: {path}") from exc