diff --git a/src/anthias_server/api/tests/test_info_endpoints.py b/src/anthias_server/api/tests/test_info_endpoints.py index fa6c0140..715466ae 100644 --- a/src/anthias_server/api/tests/test_info_endpoints.py +++ b/src/anthias_server/api/tests/test_info_endpoints.py @@ -2,7 +2,6 @@ Tests for Info API endpoints (v1 and v2). """ -from importlib.metadata import version as _pkg_version from typing import Any from unittest import mock @@ -11,10 +10,14 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient -# Pulled from pyproject.toml's [project].version via the installed -# package metadata so the test moves in lockstep with the release -# bumper without anyone having to update both places. -_ANTHIAS_RELEASE = _pkg_version('anthias') +from anthias_server.lib.diagnostics import get_anthias_release + +# Pulled from pyproject.toml's [project].version via the diagnostics +# helper so the test moves in lockstep with the release bumper, and +# also works in environments built with `uv sync --no-install-project` +# (production, host install) where importlib.metadata wouldn't find +# the package — the helper falls back to a tomllib read. +_ANTHIAS_RELEASE = get_anthias_release() @pytest.fixture diff --git a/src/anthias_server/lib/diagnostics.py b/src/anthias_server/lib/diagnostics.py index c5a43b72..9be2d9ac 100755 --- a/src/anthias_server/lib/diagnostics.py +++ b/src/anthias_server/lib/diagnostics.py @@ -92,17 +92,57 @@ _RELEASE_BRANCHES = frozenset({'master', 'main'}) def get_anthias_release() -> str: - """Read the project version from the installed package metadata - (sourced from pyproject.toml's [project].version, currently - CalVer ``YYYY.M.MICRO``). Falls back to the empty string when the - package isn't installed under this interpreter — e.g. running - ad-hoc scripts straight off the source tree without `uv sync`.""" + """Read the project version, sourced from pyproject.toml's + [project].version (currently CalVer ``YYYY.M.MICRO``). + + Resolution order: + 1. ``importlib.metadata.version('anthias')`` — works for + editable installs (``pip install -e .``) and any path where + the project ships as a wheel. + 2. Fallback: parse ``pyproject.toml`` directly with ``tomllib``. + Required because every production / test / host environment + runs ``uv sync --no-install-project`` (see + docker/uv-builder.j2, docker/Dockerfile.{server,test,viewer}, + bin/install.sh) — that flag installs the project's deps but + NOT the project itself, so importlib.metadata has no record + of an ``anthias`` distribution to read. + + Cached after first successful read so the System Info HTML render + (called per request) and the v2 info API don't re-open the file. + Returns the empty string only when both sources fail. + """ + cached: str | None = getattr(get_anthias_release, '_cached', None) + if cached is not None: + return cached + value: str try: from importlib.metadata import PackageNotFoundError, version - return version('anthias') - except PackageNotFoundError: - return '' + try: + value = version('anthias') + except PackageNotFoundError: + value = _read_version_from_pyproject() + except Exception: + value = '' + setattr(get_anthias_release, '_cached', value) + return value + + +def _read_version_from_pyproject() -> str: + """Last-ditch source for the project version when the package + isn't installed in the active venv. Walks up from this file to + find the repo-root pyproject.toml — `__file__` lives at + ``src/anthias_server/lib/diagnostics.py``, so parents[3] is the + checkout root in both editable installs and the + ``uv sync --no-install-project`` Docker layout.""" + try: + import tomllib + from pathlib import Path + + pyproject = Path(__file__).resolve().parents[3] / 'pyproject.toml' + with pyproject.open('rb') as f: + data = tomllib.load(f) + return str(data.get('project', {}).get('version', '')) except Exception: return ''