From 88c20347ed7993ea5ac995a2dd93885e8c721b58 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 18:21:48 -0400 Subject: [PATCH 1/2] Defer Fiji runtime settings until first use and expose owned cache path --- src/polystore/imagej_distribution.py | 68 ++++++++++++++++++-------- tests/test_imagej_cache_environment.py | 27 ++++++++++ 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/polystore/imagej_distribution.py b/src/polystore/imagej_distribution.py index 398134f..472d9f1 100644 --- a/src/polystore/imagej_distribution.py +++ b/src/polystore/imagej_distribution.py @@ -10,6 +10,7 @@ import shutil import stat import tempfile +import threading import time import urllib.request import zipfile @@ -419,15 +420,25 @@ def label(self) -> str: return f"Fiji {self.imagej_version} archive {self.release_id}" + @property + def resolved_cache_root(self) -> Path: + """Resolve the bundle cache once when configuring a process environment.""" + return ( + Path(self.cache_root) + if self.cache_root is not None + else self.default_cache_root() + ) + + @staticmethod + def default_cache_root() -> Path: + """Return the platform-owned default for the immutable bundle cache.""" + return user_cache_path("polystore") / "imagej" + def materialize(self) -> ImageJRuntimeLaunch: """Download, verify, and atomically cache this host's Fiji bundle.""" asset = FijiBundleAsset.for_current_host() - cache_parent = ( - Path(self.cache_root) - if self.cache_root is not None - else user_cache_path("polystore") / "imagej" - ) + cache_parent = self.resolved_cache_root runtime_digest = hashlib.sha256( "".join((asset.sha256, *(overlay.sha256 for overlay in self.runtime_overlays))).encode( "ascii" @@ -542,21 +553,36 @@ def require_compatible_gateway(self, gateway: Any) -> None: ) -FIJI_IMAGEJ_DISTRIBUTION = FijiArchiveDistribution( - release_id="20260718-0417", - imagej_version="2.18.0", - archive_base_url=("https://downloads.imagej.net/fiji/archive/latest/20260718-0417"), - cache_root=FijiArchiveDistribution.cache_root_from_environment(), - download_policy=ImageJArchiveDownloadPolicy.from_environment(), - runtime_overlays=( - ImageJRuntimeOverlay( - label="PyImageJ 1.7.0 Fiji bridge overlay", - url=( - "https://github.com/OpenHCSDev/PolyStore/releases/download/" - "imagej-runtime-20260718-0417-pyimagej-1.7.0/" - "pyimagej-1.7.0-fiji-overlay.zip" +def _create_fiji_distribution() -> FijiArchiveDistribution: + """Decode process settings at first runtime access, not declaration import.""" + return FijiArchiveDistribution( + release_id="20260718-0417", + imagej_version="2.18.0", + archive_base_url=("https://downloads.imagej.net/fiji/archive/latest/20260718-0417"), + cache_root=FijiArchiveDistribution.cache_root_from_environment(), + download_policy=ImageJArchiveDownloadPolicy.from_environment(), + runtime_overlays=( + ImageJRuntimeOverlay( + label="PyImageJ 1.7.0 Fiji bridge overlay", + url=( + "https://github.com/OpenHCSDev/PolyStore/releases/download/" + "imagej-runtime-20260718-0417-pyimagej-1.7.0/" + "pyimagej-1.7.0-fiji-overlay.zip" + ), + sha256="210d1ebb2c1f1ede85d0e94e741a772e07cfb9233992ae4e85cd245b06890345", ), - sha256="210d1ebb2c1f1ede85d0e94e741a772e07cfb9233992ae4e85cd245b06890345", ), - ), -) + ) + + +_distribution_lock = threading.Lock() + + +def __getattr__(name: str) -> Any: + """Keep declaration imports safe for pre-runtime environment configuration.""" + if name != "FIJI_IMAGEJ_DISTRIBUTION": + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + with _distribution_lock: + if name not in globals(): + globals()[name] = _create_fiji_distribution() + return globals()[name] diff --git a/tests/test_imagej_cache_environment.py b/tests/test_imagej_cache_environment.py index 4d6b272..874027d 100644 --- a/tests/test_imagej_cache_environment.py +++ b/tests/test_imagej_cache_environment.py @@ -20,6 +20,33 @@ ) +def test_declaration_import_defers_process_configuration(tmp_path): + environment = dict(os.environ) + environment.pop(FijiArchiveDistribution.cache_root_environment_key, None) + environment.pop(ImageJArchiveDownloadPolicy.allow_download_environment_key, None) + environment["PYTHONPATH"] = str(Path(__file__).resolve().parents[1] / "src") + child = subprocess.run( + [sys.executable, "-c", ''' +import os +import sys +from pathlib import Path +import polystore.imagej_distribution as declarations +assert "FIJI_IMAGEJ_DISTRIBUTION" not in declarations.__dict__ +os.environ[declarations.FijiArchiveDistribution.cache_root_environment_key] = sys.argv[1] +os.environ[declarations.ImageJArchiveDownloadPolicy.allow_download_environment_key] = "false" +from polystore.imagej_distribution import FIJI_IMAGEJ_DISTRIBUTION +from polystore.imagej_runtime import FIJI_IMAGEJ_RUNTIME +assert FIJI_IMAGEJ_DISTRIBUTION.cache_root == Path(sys.argv[1]) +assert not FIJI_IMAGEJ_DISTRIBUTION.download_policy.allow_download +assert FIJI_IMAGEJ_RUNTIME.distribution is FIJI_IMAGEJ_DISTRIBUTION +assert declarations.FIJI_IMAGEJ_DISTRIBUTION is FIJI_IMAGEJ_DISTRIBUTION +''', str(tmp_path / "shared")], + env=environment, capture_output=True, text=True, timeout=10, + ) + assert child.returncode == 0, child.stdout + child.stderr + assert not (tmp_path / "shared").exists() + + @pytest.mark.parametrize("value, allowed", ((None, True), ("true", True), (" FALSE ", False))) def test_download_policy_decodes_permission_without_copying_retry_defaults(value, allowed): environment = {} if value is None else { From 1209068f5fc9e4f7025a24ada8a73ef30dd1ddd7 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 29 Sep 2026 18:25:18 -0400 Subject: [PATCH 2/2] Own pre-runtime cache pinning and apply no-download defaults to PolyStore tests --- src/polystore/imagej_distribution.py | 27 +++++++++++++++++++++++++- tests/conftest.py | 6 ++++++ tests/test_imagej_cache_environment.py | 15 ++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/polystore/imagej_distribution.py b/src/polystore/imagej_distribution.py index 472d9f1..e5b77f8 100644 --- a/src/polystore/imagej_distribution.py +++ b/src/polystore/imagej_distribution.py @@ -15,7 +15,7 @@ import urllib.request import zipfile from abc import ABC, abstractmethod -from collections.abc import Iterator, Mapping +from collections.abc import Iterator, Mapping, MutableMapping from contextlib import contextmanager from dataclasses import dataclass from enum import Enum @@ -434,6 +434,31 @@ def default_cache_root() -> Path: """Return the platform-owned default for the immutable bundle cache.""" return user_cache_path("polystore") / "imagej" + @classmethod + def configure_process_environment( + cls, + *, + default_download_policy: ImageJArchiveDownloadPolicy, + environment: MutableMapping[str, str] | None = None, + ) -> None: + """Pin bundle storage before disposable caches or runtime imports. + + Explicit selectors remain authoritative. This only configures the + environment inherited by children; it neither materializes a bundle + nor starts Java. Call before first access to the declared runtime. + """ + values = os.environ if environment is None else environment + root = cls.cache_root_from_environment(values) + if root is None: + root = cls.default_cache_root() + download_key = default_download_policy.allow_download_environment_key + permission = "true" if default_download_policy.allow_download else "false" + policy_values = dict(values) + policy_values.setdefault(download_key, permission) + ImageJArchiveDownloadPolicy.from_environment(policy_values) + values[cls.cache_root_environment_key] = str(root) + values.setdefault(download_key, permission) + def materialize(self) -> ImageJRuntimeLaunch: """Download, verify, and atomically cache this host's Fiji bundle.""" diff --git a/tests/conftest.py b/tests/conftest.py index 3660b3b..d1acfe2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,12 @@ import numpy as np from pathlib import Path +from polystore.imagej_distribution import FijiArchiveDistribution, ImageJArchiveDownloadPolicy + +FijiArchiveDistribution.configure_process_environment( + default_download_policy=ImageJArchiveDownloadPolicy(allow_download=False) +) + from polystore import FileManager from polystore.disk import DiskBackend from polystore.memory import MemoryBackend diff --git a/tests/test_imagej_cache_environment.py b/tests/test_imagej_cache_environment.py index 874027d..3b366d2 100644 --- a/tests/test_imagej_cache_environment.py +++ b/tests/test_imagej_cache_environment.py @@ -47,6 +47,21 @@ def test_declaration_import_defers_process_configuration(tmp_path): assert not (tmp_path / "shared").exists() +def test_invalid_process_permission_leaves_environment_unchanged(tmp_path): + values = { + FijiArchiveDistribution.cache_root_environment_key: str(tmp_path / "shared"), + ImageJArchiveDownloadPolicy.allow_download_environment_key: "yes", + } + before = dict(values) + with pytest.raises(ImageJDistributionUnavailableError, match="true or false"): + FijiArchiveDistribution.configure_process_environment( + environment=values, + default_download_policy=ImageJArchiveDownloadPolicy(allow_download=False), + ) + assert values == before + assert not (tmp_path / "shared").exists() + + @pytest.mark.parametrize("value, allowed", ((None, True), ("true", True), (" FALSE ", False))) def test_download_policy_decodes_permission_without_copying_retry_defaults(value, allowed): environment = {} if value is None else {