Skip to content

Commit 0fbd20d

Browse files
committed
Wire styxcache into setup_runner for subprocess rbc invocations
tests/integration/test_all.py spawns `rbc all` via subprocess.run; that fresh process hits setup_runner() rather than the pytest autouse fixture, so it was running the full pipeline uncached. Cache diagnose confirmed 0 new entries during warm runs and revealed `setup` of those two fixtures took ~81 min combined. setup_runner now wraps the configured runner with styxcache's CachingRunner when RBC_STYXCACHE_DIR is set, matching the behavior we already had in tests/conftest.py. The wrapping logic (attribute proxy + policy) now lives in rbc.core.niwrap.maybe_wrap_with_cache; tests/conftest.py imports and reuses it. styxcache moves from dev to runtime dependency since rbc.core now imports it unconditionally. Drop the try/except fallback and the deptry DEP004 ignore.
1 parent 5e35d78 commit 0fbd20d

5 files changed

Lines changed: 61 additions & 60 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies = [
1313
"numpy>=2.4.2",
1414
"polars>=1.38.1",
1515
"scipy>=1.17.0",
16+
"styxcache>=0.2.0,<0.3",
1617
"styxpodman>=0.1.1",
1718
"tqdm>=4.67.3"
1819
]
@@ -28,8 +29,7 @@ dev = [
2829
"pytest-cov>=7.0.0",
2930
"ruff>=0.8.1",
3031
"deptry>=0.23.0",
31-
"pytest-xdist[psutil]>=3.8.0",
32-
"styxcache>=0.2.0,<0.3"
32+
"pytest-xdist[psutil]>=3.8.0"
3333
]
3434
docs = ["pdoc>=15.0.0"]
3535

@@ -44,11 +44,6 @@ markers = [
4444
"full_pipeline: End-to-end workflow tests"
4545
]
4646

47-
[tool.deptry.per_rule_ignores]
48-
# styxcache is optional — imported via try/except so the dependency is only
49-
# needed on CI where the cache is wired up. src/rbc runs fine without it.
50-
DEP004 = ["styxcache"]
51-
5247
[tool.coverage.report]
5348
omit = ["src/rbc_resources/"]
5449

src/rbc/core/common.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,20 @@
77

88
from __future__ import annotations
99

10-
import contextlib
1110
from typing import TYPE_CHECKING
1211

1312
import nibabel as nib
13+
import styxcache
1414
from niwrap import afni
1515

1616
if TYPE_CHECKING:
17-
from collections.abc import Iterator, Sequence
17+
from collections.abc import Sequence
1818
from pathlib import Path
1919

2020
from rbc.core.fileops import file_tmp_copy
2121
from rbc.core.nifti import strip_afni_volatile_metadata
2222
from rbc.core.niwrap import generate_exec_folder
2323

24-
try:
25-
from styxcache import bypass as _styxcache_bypass
26-
except ImportError: # styxcache is optional — only used on CI
27-
28-
@contextlib.contextmanager
29-
def _styxcache_bypass() -> Iterator[None]:
30-
yield
31-
32-
3324
__all__ = ["deoblique_and_reorient", "merge_3d_to_4d", "split_4d"]
3425

3526

@@ -52,11 +43,11 @@ def deoblique_and_reorient(
5243
AFNI 3dresample outputs (use ``.out_file`` for the reoriented image).
5344
"""
5445
with file_tmp_copy(in_file) as tmp_file:
55-
# 3drefit mutates in place, and styxcache 0.2.0 does not replay
46+
# 3drefit mutates in place, and styxcache does not replay
5647
# mutable-input mutations on cache hits. Bypass it so it always runs,
5748
# then strip AFNI's non-deterministic extension (timestamps + random
5849
# UUID) so the downstream cached 3dresample call keys on stable bytes.
59-
with _styxcache_bypass():
50+
with styxcache.bypass():
6051
afni.v_3drefit(in_file=tmp_file, deoblique=True)
6152
strip_afni_volatile_metadata(tmp_file)
6253
return afni.v_3dresample(

src/rbc/core/niwrap.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from typing import Literal, NamedTuple
1515

1616
import niwrap
17+
from styxcache import CachePolicy, CachingRunner
18+
from styxcache.backends import docker_digest_resolver, podman_digest_resolver
1719
from styxpodman import PodmanRunner
1820

1921
_LOG_LEVELS = [logging.WARNING, logging.INFO, logging.DEBUG]
@@ -27,6 +29,50 @@
2729
]
2830

2931

32+
# styxcache's CachingRunner doesn't proxy base-runner attributes, but rbc
33+
# touches data_dir / uid / execution_counter on the global runner. This shim
34+
# forwards anything it doesn't own to self.base.
35+
class _CacheProxyingRunner(CachingRunner):
36+
_OWN_ATTRS = frozenset({"base", "store", "policy"})
37+
38+
def __getattr__(self, name: str) -> object:
39+
return getattr(self.__dict__["base"], name)
40+
41+
def __setattr__(self, name: str, value: object) -> None:
42+
if "base" not in self.__dict__ or name in self._OWN_ATTRS:
43+
super().__setattr__(name, value)
44+
else:
45+
setattr(self.__dict__["base"], name, value)
46+
47+
48+
def maybe_wrap_with_cache(
49+
runner: niwrap.Runner, runner_type: RunnerType
50+
) -> niwrap.Runner:
51+
"""Wrap *runner* with a styxcache CachingRunner if RBC_STYXCACHE_DIR is set.
52+
53+
Registers the wrapped runner as the niwrap global and returns it. When the
54+
env var is unset, or the runner type isn't a container runner with a
55+
digest resolver, the runner is returned unchanged.
56+
"""
57+
cache_dir = os.environ.get("RBC_STYXCACHE_DIR")
58+
if not cache_dir or runner_type not in {"docker", "podman"}:
59+
return runner
60+
resolver = (
61+
docker_digest_resolver if runner_type == "docker" else podman_digest_resolver
62+
)
63+
wrapped = _CacheProxyingRunner(
64+
base=runner,
65+
cache_dir=cache_dir,
66+
policy=CachePolicy(
67+
image_digest=resolver,
68+
# Bump to invalidate when styxcache storage semantics change.
69+
extra={"cache_generation": "2026-1"},
70+
),
71+
)
72+
niwrap.set_global_runner(wrapped)
73+
return wrapped
74+
75+
3076
class StyxContext(NamedTuple):
3177
"""Styx execution context with logger and runner."""
3278

@@ -120,6 +166,11 @@ def setup_runner(
120166
log_level = min(verbose, len(_LOG_LEVELS) - 1)
121167
styx_logger.setLevel(_LOG_LEVELS[log_level])
122168

169+
# Opt-in persistent caching: subprocess invocations of `rbc` in CI (e.g.
170+
# tests/integration/test_all.py's `rbc all` spawn) pick this up through
171+
# the RBC_STYXCACHE_DIR env var inherited from the parent pytest process.
172+
styx_runner = maybe_wrap_with_cache(styx_runner, runner_type)
173+
123174
rbc_logger = logging.getLogger("rbc")
124175
rbc_logger.setLevel(_LOG_LEVELS[log_level])
125176
if not rbc_logger.handlers:

tests/conftest.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,12 @@
1212

1313
import niwrap
1414
import pytest
15-
from styxcache import CachePolicy, CachingRunner
16-
from styxcache.backends import docker_digest_resolver, podman_digest_resolver
1715
from styxpodman import PodmanRunner
1816

19-
from rbc.core.niwrap import resolve_runner
17+
from rbc.core.niwrap import maybe_wrap_with_cache, resolve_runner
2018
from rbc.orchestration import _DEFAULT_ENV_VARS
2119

2220

23-
class _AttrProxyCachingRunner(CachingRunner):
24-
# CachingRunner doesn't proxy base-runner attrs, but rbc.core.niwrap
25-
# reads/mutates data_dir, uid, execution_counter on the global runner.
26-
_OWN_ATTRS = frozenset({"base", "store", "policy"})
27-
28-
def __getattr__(self, name: str) -> object:
29-
return getattr(self.__dict__["base"], name)
30-
31-
def __setattr__(self, name: str, value: object) -> None:
32-
if "base" not in self.__dict__ or name in self._OWN_ATTRS:
33-
super().__setattr__(name, value)
34-
else:
35-
setattr(self.__dict__["base"], name, value)
36-
37-
3821
class TestSubjectData(NamedTuple):
3922
"""Test subject file paths."""
4023

@@ -100,26 +83,7 @@ def niwrap_runner(
10083
logger = logging.getLogger(runner.logger_name)
10184
logger.setLevel(logging.DEBUG)
10285

103-
cache_dir = os.environ.get("RBC_STYXCACHE_DIR")
104-
if cache_dir and runner_type in {"docker", "podman"}:
105-
resolver = (
106-
docker_digest_resolver
107-
if runner_type == "docker"
108-
else podman_digest_resolver
109-
)
110-
wrapped = _AttrProxyCachingRunner(
111-
base=runner,
112-
cache_dir=cache_dir,
113-
policy=CachePolicy(
114-
image_digest=resolver,
115-
# Bump to invalidate when styxcache storage semantics change
116-
# (e.g. 0.1.x entries lacked persisted stdout).
117-
extra={"cache_generation": "2026-1"},
118-
),
119-
)
120-
niwrap.set_global_runner(wrapped)
121-
return wrapped
122-
return runner
86+
return maybe_wrap_with_cache(runner, runner_type)
12387

12488

12589
@pytest.fixture(scope="session")

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)