Files

79 lines
2.6 KiB
Python

"""Version model shared by the docs-deploy helper scripts.
`BaseVersion` is the `(major, minor, patch)` triple; `Version` adds an optional
`-beta.N` prerelease number. Only `-beta.N` prereleases are recognised (that is
all Atuin ships); any other suffix fails to parse.
"""
from collections.abc import Iterator, Mapping
from dataclasses import dataclass
from functools import total_ordering
from typing import Self
import re
# Stable docs are published under a `major.minor` id (patch omitted); releases
# and prereleases carry the full `major.minor.patch[-beta.N]`.
_version_re = re.compile(r"^v?(\d+)\.(\d+)(?:\.(\d+))?(?:-beta\.(\d+))?$")
@dataclass(frozen=True, order=True)
class BaseVersion:
major: int
minor: int
patch: int
def __iter__(self) -> Iterator[int]:
return iter((self.major, self.minor, self.patch))
@total_ordering
@dataclass(frozen=True, eq=False)
class Version:
base: BaseVersion
# The `-beta.N` number, or None for a stable release.
pre_release: int | None
@classmethod
def from_str(cls, as_str: str) -> Self:
match = _version_re.match(as_str)
if match is None:
raise ValueError(f"{as_str!r} is not a supported version string")
major, minor, patch, pre = match.groups()
return cls(
base=BaseVersion(int(major), int(minor), int(patch) if patch else 0),
pre_release=int(pre) if pre is not None else None,
)
@classmethod
def from_mike(cls, entry: Mapping[str, object]) -> Self:
"""Build a Version from a `mike list --json` entry (a `{"version": ...}` mapping)."""
return cls.from_str(str(entry["version"]))
@property
def is_prerelease(self) -> bool:
return self.pre_release is not None
def __str__(self) -> str:
base = ".".join(map(str, self.base))
return base if self.pre_release is None else f"{base}-beta.{self.pre_release}"
def _key(self) -> tuple[int, int, int, float]:
# A stable release (None) sorts after every prerelease of the same base,
# matching semver (`18.18.0` > `18.18.0-beta.9`).
pre = self.pre_release if self.pre_release is not None else float("inf")
return (self.base.major, self.base.minor, self.base.patch, pre)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self._key() == other._key()
def __lt__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self._key() < other._key()
def __hash__(self) -> int:
return hash(self._key())