From 72cbce67f8d26f148f61c838bc7489b1350cc758 Mon Sep 17 00:00:00 2001 From: Tobias Juelg Date: Fri, 16 Oct 2026 17:41:05 -0700 Subject: [PATCH 1/2] fix(assets): stop re-downloading assets on every import rcs.get_prefix decides whether assets are present by checking for assets/scenes/empty_world/scene.xml, an RCS file that is not part of the duobench archive. For non-editable installs the check therefore always failed and every `import duobench` re-downloaded and rewrote the assets, so parallel workers could read half-written files. Write assets/.duobench_version after a complete download and skip rcs.get_prefix when it matches the installed version. This also re-fetches the assets after a duobench upgrade and after an interrupted download. Works with the released rcs-core get_prefix API. --- src/duobench/__init__.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/duobench/__init__.py b/src/duobench/__init__.py index 3da5dbc..9425a91 100644 --- a/src/duobench/__init__.py +++ b/src/duobench/__init__.py @@ -5,17 +5,41 @@ import numpy as np import rcs -from rcs import get_prefix +from rcs import download_assets, get_prefix from rcs._core import common DUOBENCH_GITHUB_ASSET_ARCHIVE_URL = "https://github.com/RobotControlStack/duobench/archive/refs/tags/{tag}.zip" +# written after a complete download, marks which duobench version the assets belong to +ASSETS_VERSION_FILE = Path("assets/.duobench_version") -DUOBENCH_PREFIX = get_prefix( + +def _download_assets(version: str, prefix: Path, github_asset_archive_url: str, assets_dirname: str) -> None: + download_assets(version, prefix, github_asset_archive_url, assets_dirname) + (prefix / ASSETS_VERSION_FILE).write_text(version) + + +def _get_duobench_prefix(env_prefix: str | None, editable_prefix: Path, default_prefix: Path) -> str: + # rcs.get_prefix checks for an RCS asset that is not part of the duobench archive, so it would + # re-download the duobench assets on every import; skip it if the assets of this version are there + if env_prefix or not (editable_prefix.resolve() / "assets").is_dir(): + prefix = Path(env_prefix).expanduser().resolve() if env_prefix else default_prefix.resolve() + version_file = prefix / ASSETS_VERSION_FILE + if version_file.is_file() and version_file.read_text() == __version__: + return str(prefix) + return get_prefix( + env_prefix, + editable_prefix, + default_prefix, + __version__, + DUOBENCH_GITHUB_ASSET_ARCHIVE_URL, + download_fn=_download_assets, + ) + + +DUOBENCH_PREFIX = _get_duobench_prefix( os.environ.get("DUOBENCH_PREFIX"), Path(__file__).resolve().parents[2], Path.home() / ".duobench", - __version__, - DUOBENCH_GITHUB_ASSET_ARCHIVE_URL, ) CAMERA_PATHS: dict[str, str] = {} From 4a96558c09c80505397e6f36b3342eb7503394c0 Mon Sep 17 00:00:00 2001 From: Tobias Juelg Date: Fri, 16 Oct 2026 17:41:05 -0700 Subject: [PATCH 2/2] test(assets): cover asset download caching and version marker --- src/tests/test_asset_prefix.py | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/tests/test_asset_prefix.py diff --git a/src/tests/test_asset_prefix.py b/src/tests/test_asset_prefix.py new file mode 100644 index 0000000..7cf4fe7 --- /dev/null +++ b/src/tests/test_asset_prefix.py @@ -0,0 +1,48 @@ +from pathlib import Path + +import pytest + +import duobench + + +@pytest.fixture() +def downloads(monkeypatch: pytest.MonkeyPatch) -> list[Path]: + calls: list[Path] = [] + + def fake_download(_version: str, prefix: Path, _url: str, assets_dirname: str) -> None: + calls.append(prefix) + (prefix / assets_dirname).mkdir(parents=True, exist_ok=True) + + monkeypatch.setattr(duobench, "download_assets", fake_download) + return calls + + +def test_assets_downloaded_once(tmp_path: Path, downloads: list[Path]): + prefix = tmp_path / "prefix" + for _ in range(3): + assert duobench._get_duobench_prefix(str(prefix), tmp_path / "pkg", tmp_path / "default") == str(prefix) + assert downloads == [prefix] + assert (prefix / duobench.ASSETS_VERSION_FILE).read_text() == duobench.__version__ + + +def test_default_prefix_used_without_env(tmp_path: Path, downloads: list[Path]): + default = tmp_path / "default" + for _ in range(2): + assert duobench._get_duobench_prefix(None, tmp_path / "pkg", default) == str(default) + assert downloads == [default] + + +def test_assets_redownloaded_on_version_change(tmp_path: Path, downloads: list[Path]): + prefix = tmp_path / "prefix" + (prefix / "assets").mkdir(parents=True) + (prefix / duobench.ASSETS_VERSION_FILE).write_text("0.0.0") + duobench._get_duobench_prefix(str(prefix), tmp_path / "pkg", tmp_path / "default") + assert downloads == [prefix] + assert (prefix / duobench.ASSETS_VERSION_FILE).read_text() == duobench.__version__ + + +def test_editable_install_skips_download(tmp_path: Path, downloads: list[Path]): + editable = tmp_path / "pkg" + (editable / "assets").mkdir(parents=True) + assert duobench._get_duobench_prefix(None, editable, tmp_path / "default") == str(editable) + assert downloads == []