From 0d42bdcc17cc58acca8f0fa9405ecc913520982b Mon Sep 17 00:00:00 2001 From: Peter Grotz <199680633+peter-grotz@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:07:58 -0700 Subject: [PATCH] fix(stage): fall back to the installed library version for Code.version CODE_VERSION was introduced as a contract for the pinned capsule commit, on the assumption that main.nf could export it. It cannot: Code Ocean generates main.nf from the pipeline UI -- it carries a hash:sha256 marker -- and exports only CO_CAPSULE_ID, CO_CPUS and CO_MEMORY. The pinned commit is used in a git checkout and then the repo is deleted, so the running code never sees it. Code.version now resolves CODE_VERSION, then the installed version of exaspim-swc-processing, then None. For capsules that are thin glue over the library, the library version is what actually determines behaviour, and AIND accepts a version in place of a commit hash. --- src/exaspim_swc_processing/stage.py | 34 +++++++++++++++++++++++++++-- tests/test_stage.py | 23 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/exaspim_swc_processing/stage.py b/src/exaspim_swc_processing/stage.py index 78e1903..f770b27 100644 --- a/src/exaspim_swc_processing/stage.py +++ b/src/exaspim_swc_processing/stage.py @@ -19,6 +19,7 @@ import sys from collections.abc import Mapping, Sequence from datetime import datetime, timezone +from importlib import metadata from pathlib import Path from aind_data_schema.components.identifiers import Code @@ -54,11 +55,37 @@ def _resources() -> ResourceUsage: ) +def installed_version(distribution: str) -> str | None: + """Return the installed version of a distribution, if it is installed. + + Used as the fallback for ``Code.version``. Code Ocean generates ``main.nf`` and + exports only ``CO_CAPSULE_ID``, ``CO_CPUS`` and ``CO_MEMORY``, so the pinned capsule + commit is not available to the running code. The version of the library the capsule + installed is available, is what actually determines behaviour for these thin + capsules, and is a value AIND accepts in place of a commit hash. + + Parameters + ---------- + distribution : str + Distribution name, e.g. ``"exaspim-swc-processing"``. + + Returns + ------- + str | None + The installed version, or ``None`` if the distribution is not installed. + """ + try: + return metadata.version(distribution) + except metadata.PackageNotFoundError: + return None + + def resolve_code( name: str, url: str | None = None, version: str | None = None, run_script: str = "code/run", + distribution: str = "exaspim-swc-processing", ) -> Code: """Describe the code a stage ran, from the environment. @@ -70,9 +97,12 @@ def resolve_code( Repository URL. AIND prefers a GitHub URL; when omitted the Code Ocean capsule URL is built from ``CO_CAPSULE_ID``. version : str | None, optional - Version or commit of the code that ran. Read from ``CODE_VERSION`` when omitted. + Version of the code that ran. When omitted, ``CODE_VERSION`` is read, then the + installed version of ``distribution``. See :func:`installed_version`. run_script : str, optional Entry point, relative to the repository root, by default ``"code/run"``. + distribution : str, optional + Installed package to fall back to for the version, by default this library. Returns ------- @@ -85,7 +115,7 @@ def resolve_code( return Code( url=resolved_url, name=name, - version=version or os.environ.get("CODE_VERSION") or None, + version=version or os.environ.get("CODE_VERSION") or installed_version(distribution), run_script=Path(run_script), language="Python", language_version=( diff --git a/tests/test_stage.py b/tests/test_stage.py index ac05007..5319fac 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -10,6 +10,7 @@ from exaspim_swc_processing.stage import ( DATA_PROCESS_FILENAME, build_stage_process, + installed_version, resolve_code, write_stage_process, ) @@ -41,13 +42,31 @@ def test_a_github_url_is_preferred_when_given(monkeypatch: pytest.MonkeyPatch) - assert code.url == "https://github.com/peter-grotz/exaspim-swc-processing" -def test_version_is_none_rather_than_empty_when_unknown( +def test_version_falls_back_to_the_installed_library( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Code Ocean generates main.nf and does not export the pinned capsule commit. + + The installed library version is what actually determines a thin capsule's + behaviour, and AIND accepts a version in place of a commit hash. + """ + monkeypatch.setenv("CO_CAPSULE_ID", CAPSULE_ID) + monkeypatch.delenv("CODE_VERSION", raising=False) + assert resolve_code("x").version == installed_version("exaspim-swc-processing") + + +def test_version_is_none_when_nothing_identifies_the_code( monkeypatch: pytest.MonkeyPatch, ) -> None: """An absent version is null, not an empty string that looks recorded.""" monkeypatch.setenv("CO_CAPSULE_ID", CAPSULE_ID) monkeypatch.delenv("CODE_VERSION", raising=False) - assert resolve_code("x").version is None + assert resolve_code("x", distribution="not-installed-anywhere").version is None + + +def test_installed_version_returns_none_for_an_absent_distribution() -> None: + """A capsule that did not install the library still produces a valid record.""" + assert installed_version("definitely-not-installed") is None def test_url_is_empty_outside_code_ocean(monkeypatch: pytest.MonkeyPatch) -> None: