signal-bot/signal_bot/versioning.py
Krzysztof kuhy Rudnicki b7b1f8f1c4
Some checks are pending
CI / check (push) Waiting to run
release: 0.0.4 «Ewaryst»
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 23:41:31 +02:00

104 lines
3.2 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Release versions named after the popes, in order.
Release *n* is pope *n*: 0.0.0 is Święty Piotr Apostoł, and the version is a
pure function of the ordinal rather than a judgement about the size of a
change. The one fixed point is Jan Paweł II at 2.13.7, which is what picks
the two constants below -- eight patches per minor, and majors opening at
ordinals 33 and 153:
(264 - 153) = 111 = 13 * 8 + 7 -> 2.13.7
Both earlier majors happen to divide evenly by eight as well (32 = 4 * 8,
120 = 15 * 8), so no bracket needs a fudge factor. The anchors are not
arbitrary either: 33 is Sylwester I, pope through the Edict of Milan and
Nicaea, and 153 is Wiktor II, the first pope elected after the 1054 schism.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Final
# Ordinals at which a new major version opens. Ascending, first entry 1.
MAJOR_STARTS: Final = (1, 33, 153)
PATCHES_PER_MINOR: Final = 8
# Which release this build is. Bump by one per release; everything else,
# including the version string in pyproject.toml, follows from it.
RELEASE_ORDINAL: Final = 5
_POPES_FILE: Final = Path(__file__).with_name("popes.json")
@dataclass(frozen=True, slots=True)
class Pope:
"""One entry of the papal list, as the version ladder needs it."""
ordinal: int
name: str
reign: str
portrait: str
def _load() -> tuple[Pope, ...]:
"""Read popes.json once, at import."""
document: dict[str, Any] = json.loads(_POPES_FILE.read_text(encoding="utf-8"))
return tuple(
Pope(
ordinal=int(entry["ordinal"]),
name=str(entry["name"]),
reign=str(entry["reign"]),
portrait=str(entry["portrait"]),
)
for entry in document["popes"]
)
POPES: Final = _load()
def version_for(ordinal: int) -> str:
"""Return the dotted version string for the given papal ordinal.
Raises:
ValueError: if the ordinal is outside the known list.
"""
if not 1 <= ordinal <= len(POPES):
message = f"no pope with ordinal {ordinal}; the list has {len(POPES)}"
raise ValueError(message)
major = max(index for index, start in enumerate(MAJOR_STARTS) if ordinal >= start)
offset = ordinal - MAJOR_STARTS[major]
return f"{major}.{offset // PATCHES_PER_MINOR}.{offset % PATCHES_PER_MINOR}"
def pope_for(ordinal: int) -> Pope:
"""Return the pope with the given ordinal.
Raises:
ValueError: if the ordinal is outside the known list.
"""
if not 1 <= ordinal <= len(POPES):
message = f"no pope with ordinal {ordinal}; the list has {len(POPES)}"
raise ValueError(message)
return POPES[ordinal - 1]
def ordinal_for(version: str) -> int:
"""Return the papal ordinal a version string denotes.
Raises:
ValueError: if no release carries that version.
"""
for pope in POPES:
if version_for(pope.ordinal) == version:
return pope.ordinal
message = f"no release carries version {version!r}"
raise ValueError(message)
__version__: Final = version_for(RELEASE_ORDINAL)
POPE: Final = pope_for(RELEASE_ORDINAL)