Source code for vibesketch.codenames

"""Release codename catalogue.

vibe-sketch names releases after an organic chemist with a named reaction
and a piece of laboratory equipment. The maintainer selected Dötz’s Flask
for 0.1.2 and later 0.1.x patches (``docs/codenames.md``). An explicit patch
entry starts a new name within its minor line without renaming older versions.
Registration accompanies the release version commit.

This module is the **single source of truth**. Three surfaces read it:

* ``vibe-sketch --version`` (:mod:`vibesketch.cli`)
* the server's ``/api/version`` resource, which the browser's About box and
  the Electron desktop shell display
* the documentation site's ``{{codename}}`` substitution (``docs/conf.py``
  loads this file standalone, so it must keep importing nothing beyond the
  standard library)

``tests/test_codenames.py`` checks that every surface agrees with the
catalogue.

Programmatic API
----------------

``codename_for_version(version)``
    Resolve a version string to its codename, or ``None``.

``codename_for_current()``
    Convenience: the codename of the running ``vibesketch.__version__``.

``version_label()``
    The ``"0.1.2 — Dötz’s Flask"`` string the user-facing surfaces print,
    or the bare version while no codename is registered.
"""

from __future__ import annotations

import re

__all__ = [
    "RELEASE_CODENAMES",
    "codename_for_current",
    "codename_for_version",
    "version_label",
]


# --- Catalogue -------------------------------------------------------------
#
# Keyed by the first ``X.Y.Z`` version to use a name. Later patches inherit
# the most recent entry in their minor line; entries never rename earlier
# versions. Dev, alpha, beta and rc builds inherit the name of their target.
#
# When selecting a new release name, add the entry here in the same commit
# that bumps ``pyproject.toml`` and ``src/vibesketch/__init__.py``, with a
# comment that says what the release is about and why the name fits.
RELEASE_CODENAMES: dict[str, str] = {
    # Historical candidates keep the name assigned before the scheme changed.
    "0.1.0": "Couper's Coati",
    # Corrected desktop delivery starts the organic chemist/equipment scheme.
    # Karl Heinz Dötz: Dötz benzannulation; see docs/codenames.md.
    "0.1.2": "Dötz’s Flask",
    # Otto Diels: the Diels–Alder reaction; predictable geometry and scheme layout.
    "0.3.0": "Diels’s Burette",
    # Georg Wittig: the Wittig reaction, which interchanges a carbonyl for an alkene;
    # this release interchanges drawings with QVF, CDXML and LaTeX.
    "0.4.0": "Wittig’s Pipette",
    # Richard Heck: traceable reaction calculations and their energy tables.
    "0.5.0": "Heck’s Beaker",
    # Staged workflows funnel verified artifacts into the release-paper baseline.
    "0.6.0": "Negishi’s Funnel",
}


_DEV_SUFFIX = re.compile(
    r"""
    (?:\.dev\d+ | a\d+ | b\d+ | rc\d+)   # PEP-440 pre-release / dev segments
    (?:\+.*)?$                            # ... and any local version label
    """,
    re.VERBOSE,
)
_LOCAL_SUFFIX = re.compile(r"\+.*$")


def _strip_dev_suffix(version: str) -> str:
    """Reduce a PEP-440 version to the ``X.Y.Z`` release it belongs to.

    ``0.2.0.dev3`` -> ``0.2.0``, ``0.2.0rc1`` -> ``0.2.0``,
    ``0.1.2+local.1`` -> ``0.1.2``.
    """
    return _DEV_SUFFIX.sub("", _LOCAL_SUFFIX.sub("", version.strip()))


[docs] def codename_for_version(version: str) -> str | None: """Return the codename for a vibe-sketch release version, or ``None``. Resolution order: 1. Strip any PEP-440 ``.devN`` / ``aN`` / ``bN`` / ``rcN`` suffix and local version label, so a dev build inherits the codename of the release it leads up to. 2. Direct lookup of the resulting ``X.Y.Z`` in :data:`RELEASE_CODENAMES`. 3. Use the most recent named version no later than ``X.Y.Z`` in the same minor line, so subsequent patches inherit a patch-level change. 4. ``None`` when no codename is registered for that minor. """ stripped = _strip_dev_suffix(version) if stripped in RELEASE_CODENAMES: return RELEASE_CODENAMES[stripped] parts = stripped.split(".") if len(parts) >= 2: anchor = f"{parts[0]}.{parts[1]}.0" if len(parts) == 3 and all(part.isdigit() for part in parts): prefix = f"{parts[0]}.{parts[1]}." patch = int(parts[2]) eligible = [ (int(key[len(prefix) :]), name) for key, name in RELEASE_CODENAMES.items() if key.startswith(prefix) and key[len(prefix) :].isdigit() and int(key[len(prefix) :]) <= patch ] if eligible: return max(eligible, key=lambda entry: entry[0])[1] if anchor in RELEASE_CODENAMES: return RELEASE_CODENAMES[anchor] return None
[docs] def codename_for_current() -> str | None: """Return the codename of the running vibe-sketch, or ``None``. Imported lazily so this module stays importable with nothing but the standard library: ``docs/conf.py`` loads it by path, outside the package. """ from vibesketch import __version__ return codename_for_version(__version__)
[docs] def version_label(version: str | None = None) -> str: """Format the version string the user-facing surfaces print. ``"0.1.2 — Dötz’s Flask"`` when a codename is registered, and the bare version when one is not, so an unnamed line never prints a dangling dash. """ if version is None: from vibesketch import __version__ version = __version__ name = codename_for_version(version) return f"{version} — {name}" if name else version