Skip to content

Commit fc89b1f

Browse files
committed
fix: address @awsarron review feedback
- assets/manager.py: remove 'import os as _os' alias — just import os at top - simulation/models.py: clarify _model vs _data vs _backend_state roles in docstring so backend implementers know which to use (each has a distinct purpose: core model handle, core state handle, catch-all dict) - utils.py: decouple get_base_dir() from STRANDS_ASSETS_DIR. Introduce STRANDS_BASE_DIR as the explicit override for the base directory so user_robots.json no longer lands in an unexpected parent of the assets path. STRANDS_ASSETS_DIR now *only* moves the assets subtree. - registry/user_registry.py: update module docstring to reflect the new STRANDS_BASE_DIR semantics - tests/test_user_registry.py: update fixture + integration tests to set STRANDS_BASE_DIR and STRANDS_ASSETS_DIR independently; add tests asserting that STRANDS_ASSETS_DIR does *not* move the registry / base dir
1 parent f1d2dc5 commit fc89b1f

5 files changed

Lines changed: 90 additions & 37 deletions

File tree

strands_robots/assets/manager.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import logging
11+
import os
1112
from pathlib import Path
1213

1314
from strands_robots.registry import (
@@ -73,14 +74,12 @@ def _has_meshes(directory: Path) -> bool:
7374
if cached is not None:
7475
return cached
7576

76-
import os as _os
77-
7877
def _walk(path: str) -> bool:
7978
try:
80-
with _os.scandir(path) as it:
79+
with os.scandir(path) as it:
8180
for entry in it:
8281
if entry.is_file(follow_symlinks=False):
83-
ext = _os.path.splitext(entry.name)[1].lower()
82+
ext = os.path.splitext(entry.name)[1].lower()
8483
if ext in _MESH_EXTS:
8584
return True
8685
elif entry.is_dir(follow_symlinks=False) and _walk(entry.path):

strands_robots/registry/user_registry.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
alongside the asset cache.
66
77
File location (in priority order):
8-
1. ``$STRANDS_ASSETS_DIR/user_robots.json``
8+
1. ``$STRANDS_BASE_DIR/user_robots.json``
99
2. ``~/.strands_robots/user_robots.json``
1010
11+
Note:
12+
``STRANDS_ASSETS_DIR`` only controls where *assets* live, not the
13+
user registry. Use ``STRANDS_BASE_DIR`` to relocate user metadata.
14+
1115
At load time the user overlay is merged *on top of* the package
1216
``robots.json`` — user entries win on name collision, so you can also
1317
override built-in robots locally.

strands_robots/simulation/models.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,22 @@ class TrajectoryStep:
100100
class SimWorld:
101101
"""Complete simulation world state.
102102
103-
Backend-independent state with engine-specific internals isolated in
104-
``_model``, ``_data``, and ``_backend_state`` — all typed as ``Any``
105-
or ``dict`` so that each backend can store its own native handles
106-
without leaking implementation details into this base module.
103+
Backend-independent state with engine-specific internals kept in three
104+
escape hatches, each with a distinct role so backend implementers know
105+
which to use:
106+
107+
* ``_model``: the physics engine's **core model handle** — the single
108+
compiled/loaded representation of the scene (e.g. ``mujoco.MjModel``,
109+
Isaac's ``Scene``, PyBullet's body registry). Every backend has one.
110+
* ``_data``: the physics engine's **core simulation state handle** —
111+
the mutable per-step state companion to ``_model``
112+
(e.g. ``mujoco.MjData``, Isaac's ``World``). Every backend has one.
113+
* ``_backend_state``: a **catch-all dict** for everything else the
114+
backend needs to persist — generated XML, temp dirs, recording
115+
buffers, caches, etc. Prefer this over adding new fields here.
116+
117+
All three are typed ``Any``/``dict`` so nothing leaks engine-specific
118+
types into this base module.
107119
"""
108120

109121
robots: dict[str, SimRobot] = field(default_factory=dict)
@@ -115,16 +127,15 @@ class SimWorld:
115127
status: SimStatus = SimStatus.IDLE
116128
sim_time: float = 0.0
117129
step_count: int = 0
118-
# Engine-specific internals (set after world is built by the backend).
119-
# Each backend stores its own native handles here.
130+
# Engine core handles — set after the backend builds the world.
131+
# Use these for the primary model/state objects only; put everything
132+
# else in ``_backend_state`` below.
120133
_model: Any = None # Engine-specific model handle (e.g. MjModel, Scene)
121134
_data: Any = None # Engine-specific data handle (e.g. MjData, World)
122-
# Backend-specific state bag — backends store format-specific data here
123-
# instead of polluting this base class with implementation details.
124-
# Recording state (``_recording``, ``_trajectory``, ``_dataset_recorder``)
125-
# and engine-specific handles (e.g. MuJoCo ``xml``, ``robot_base_xml``,
126-
# ``tmpdir``) all go in this dict rather than being declared as separate
127-
# fields on the base class.
135+
# Catch-all for backend-specific state that isn't the core model/data.
136+
# Examples: generated XML strings, temp dirs, recording buffers
137+
# (``_recording``, ``_trajectory``, ``_dataset_recorder``), caches, etc.
138+
# Prefer this over adding new fields to ``SimWorld``.
128139
_backend_state: dict[str, Any] = field(default_factory=dict)
129140
# Physics state checkpoints (used by save_state/restore_state in PR #85).
130141
# Kept as a top-level field — requested by @yinsong1986 during review to

strands_robots/utils.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,25 @@ def require_optional(
6464
def get_base_dir() -> Path:
6565
"""Get the base directory for strands-robots user data.
6666
67-
If ``STRANDS_ASSETS_DIR`` is set, returns its parent
68-
(the assets dir is a subdirectory of the base).
69-
Otherwise returns ``~/.strands_robots/``.
67+
Resolution (in priority order):
68+
69+
1. ``STRANDS_BASE_DIR`` env var — explicit override. Use this when
70+
you want to relocate *all* strands-robots user data (assets,
71+
user registry, caches) to a non-default location.
72+
2. ``~/.strands_robots/`` — default.
73+
74+
Note:
75+
``STRANDS_ASSETS_DIR`` **only** controls the assets subdirectory
76+
(see :func:`get_assets_dir`). It does *not* move the base dir,
77+
so user-level metadata like ``user_robots.json`` always lands in
78+
a predictable location rather than wherever the assets happen
79+
to be pointed.
7080
7181
Returns:
7282
Path to the base directory (created if needed).
7383
"""
74-
custom = os.getenv("STRANDS_ASSETS_DIR")
75-
if custom:
76-
d = Path(custom).parent
77-
else:
78-
d = DEFAULT_BASE_DIR
84+
custom = os.getenv("STRANDS_BASE_DIR")
85+
d = Path(custom) if custom else DEFAULT_BASE_DIR
7986
d.mkdir(parents=True, exist_ok=True)
8087
return d
8188

tests/test_user_registry.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@
3535

3636
@pytest.fixture(autouse=True)
3737
def _isolate_registry(tmp_path, monkeypatch):
38-
"""Point STRANDS_ASSETS_DIR to a temp dir and clear caches for every test."""
38+
"""Point STRANDS_BASE_DIR + STRANDS_ASSETS_DIR to temp dirs for every test.
39+
40+
``STRANDS_BASE_DIR`` controls where ``user_robots.json`` lives.
41+
``STRANDS_ASSETS_DIR`` controls where robot asset directories live.
42+
The two are independent — the base dir is not derived from the assets dir.
43+
"""
3944
assets_dir = tmp_path / "assets"
4045
assets_dir.mkdir()
46+
monkeypatch.setenv("STRANDS_BASE_DIR", str(tmp_path))
4147
monkeypatch.setenv("STRANDS_ASSETS_DIR", str(assets_dir))
4248
_invalidate_cache()
4349
yield
@@ -288,20 +294,34 @@ def test_import_error_returns_data_unchanged(self):
288294

289295

290296
# ===========================================================================
291-
# STRANDS_ASSETS_DIR integration
297+
# STRANDS_BASE_DIR integration
292298
# ===========================================================================
293299

294300

295-
class TestStrandsAssetsDirIntegration:
296-
"""Registry file location respects STRANDS_ASSETS_DIR env var."""
301+
class TestStrandsBaseDirIntegration:
302+
"""Registry file location respects STRANDS_BASE_DIR env var.
303+
304+
STRANDS_ASSETS_DIR intentionally does NOT move the registry — it only
305+
controls where asset directories live. See utils.get_base_dir() docstring.
306+
"""
297307

298-
def test_registry_file_in_parent_of_assets_dir(self, tmp_path):
299-
custom = tmp_path / "custom_assets"
308+
def test_registry_file_lives_in_base_dir(self, tmp_path):
309+
custom = tmp_path / "custom_base"
300310
custom.mkdir()
301-
with mock.patch.dict(os.environ, {"STRANDS_ASSETS_DIR": str(custom)}):
302-
assert _get_user_registry_path().parent == custom.parent
311+
with mock.patch.dict(os.environ, {"STRANDS_BASE_DIR": str(custom)}, clear=False):
312+
assert _get_user_registry_path().parent == custom
313+
314+
def test_assets_dir_does_not_move_registry(self, tmp_path, monkeypatch):
315+
"""Setting only STRANDS_ASSETS_DIR must not change the registry location."""
316+
monkeypatch.delenv("STRANDS_BASE_DIR", raising=False)
317+
custom_assets = tmp_path / "custom_assets"
318+
custom_assets.mkdir()
319+
monkeypatch.setenv("STRANDS_ASSETS_DIR", str(custom_assets))
320+
# Registry should land under the default base, not the assets dir.
321+
assert ".strands_robots" in str(_get_user_registry_path())
303322

304323
def test_defaults_to_dot_strands_robots(self, monkeypatch):
324+
monkeypatch.delenv("STRANDS_BASE_DIR", raising=False)
305325
monkeypatch.delenv("STRANDS_ASSETS_DIR", raising=False)
306326
assert ".strands_robots" in str(_get_user_registry_path())
307327

@@ -328,17 +348,29 @@ def test_custom(self, tmp_path, monkeypatch):
328348

329349

330350
class TestGetBaseDir:
331-
"""get_base_dir() returns parent of STRANDS_ASSETS_DIR or ~/.strands_robots/."""
351+
"""get_base_dir() returns STRANDS_BASE_DIR or ~/.strands_robots/.
352+
353+
It is independent of STRANDS_ASSETS_DIR by design — the base dir holds
354+
user metadata (user_robots.json) and should not move just because the
355+
user repoints the asset cache.
356+
"""
332357

333358
def test_default(self, monkeypatch):
359+
monkeypatch.delenv("STRANDS_BASE_DIR", raising=False)
334360
monkeypatch.delenv("STRANDS_ASSETS_DIR", raising=False)
335361
assert str(get_base_dir()).endswith(".strands_robots")
336362

337363
def test_custom(self, tmp_path, monkeypatch):
338-
custom = tmp_path / "custom_assets"
364+
custom = tmp_path / "custom_base"
339365
custom.mkdir()
340-
monkeypatch.setenv("STRANDS_ASSETS_DIR", str(custom))
341-
assert get_base_dir() == tmp_path
366+
monkeypatch.setenv("STRANDS_BASE_DIR", str(custom))
367+
assert get_base_dir() == custom
368+
369+
def test_assets_dir_does_not_move_base(self, tmp_path, monkeypatch):
370+
"""STRANDS_ASSETS_DIR must not affect get_base_dir()."""
371+
monkeypatch.delenv("STRANDS_BASE_DIR", raising=False)
372+
monkeypatch.setenv("STRANDS_ASSETS_DIR", str(tmp_path / "assets"))
373+
assert str(get_base_dir()).endswith(".strands_robots")
342374

343375

344376
class TestResolveAssetPath:

0 commit comments

Comments
 (0)