From 739aca4777271b87d4f9e8c4bbeb5205f9267562 Mon Sep 17 00:00:00 2001 From: Jay Chooi Date: Tue, 14 Jul 2026 09:04:50 -0700 Subject: [PATCH] fix: support lerobot >= 0.6.0 in the hw_to_dataset_features import seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lerobot 0.6.0 moved hw_to_dataset_features from lerobot.datasets.feature_utils to lerobot.utils.feature_utils, breaking policy loading against 0.6.x. Extend the version seam to try the new location first, falling back to the 0.5.1 and 0.5.0 layouts. Verified live on hardware against lerobot 0.6.1 (SO-101 follower + smolvla_base end-to-end). Note 0.6.x also gates previously-bundled deps behind extras at import/load time (grpcio via lerobot[async], datasets via lerobot[dataset]) — install those alongside. Test fakes now model all three layouts via feature_layout ("v060" default / "v051" / "v050"), with explicit fallback tests for both older layouts. Co-Authored-By: Claude Fable 5 --- src/inspect_robots_so101/policy.py | 12 ++++++--- tests/test_policy.py | 43 ++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/inspect_robots_so101/policy.py b/src/inspect_robots_so101/policy.py index 1b423d7..21632b3 100644 --- a/src/inspect_robots_so101/policy.py +++ b/src/inspect_robots_so101/policy.py @@ -58,13 +58,17 @@ def _import_hw_to_dataset_features() -> Callable[..., dict[str, Any]]: """Import lerobot's ``hw_to_dataset_features`` across supported versions. lerobot moved it from ``lerobot.datasets.utils`` (v0.5.0) to - ``lerobot.datasets.feature_utils`` (v0.5.1); the ``lerobot-seam`` CI job + ``lerobot.datasets.feature_utils`` (v0.5.1) to + ``lerobot.utils.feature_utils`` (v0.6.0); the ``lerobot-seam`` CI job guards this against further drift. """ try: - from lerobot.datasets.feature_utils import hw_to_dataset_features - except ImportError: # lerobot == 0.5.0 - from lerobot.datasets.utils import hw_to_dataset_features + from lerobot.utils.feature_utils import hw_to_dataset_features + except ImportError: + try: # lerobot 0.5.1 - 0.5.x + from lerobot.datasets.feature_utils import hw_to_dataset_features + except ImportError: # lerobot == 0.5.0 + from lerobot.datasets.utils import hw_to_dataset_features return hw_to_dataset_features # type: ignore[no-any-return] diff --git a/tests/test_policy.py b/tests/test_policy.py index ae05083..8fa37de 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -176,9 +176,9 @@ def _install_lerobot_fakes( *, chunk: np.ndarray, image_features: dict[str, Any] | None = None, - feature_utils: bool = True, + feature_layout: str = "v060", ) -> dict[str, Any]: - """Install fake torch/lerobot modules; return a dict recording every call.""" + """Install fakes for the v0.6.0, v0.5.1, or v0.5.0 feature layout.""" calls: dict[str, Any] = {} if image_features is None: image_features = {"observation.images.front": SimpleNamespace(shape=(3, 4, 4))} @@ -267,19 +267,37 @@ def _stack(tensors: list[FakeTensor], dim: int = 0) -> FakeTensor: "lerobot.async_inference.helpers": helpers, "lerobot.datasets": datasets, } - if feature_utils: + if feature_layout == "v060": + # lerobot >= 0.6.0: hw_to_dataset_features lives in lerobot.utils.feature_utils. + monkeypatch.delitem(sys.modules, "lerobot.datasets.feature_utils", raising=False) + monkeypatch.delitem(sys.modules, "lerobot.datasets.utils", raising=False) + utils = ModuleType("lerobot.utils") + feature_utils_mod = ModuleType("lerobot.utils.feature_utils") + feature_utils_mod.hw_to_dataset_features = hw_to_dataset_features # type: ignore[attr-defined] + utils.feature_utils = feature_utils_mod # type: ignore[attr-defined] + lerobot.utils = utils # type: ignore[attr-defined] + modules["lerobot.utils"] = utils + modules["lerobot.utils.feature_utils"] = feature_utils_mod + elif feature_layout == "v051": + # lerobot 0.5.1 - 0.5.x: the helper moved to datasets.feature_utils. + monkeypatch.delitem(sys.modules, "lerobot.utils.feature_utils", raising=False) + monkeypatch.delitem(sys.modules, "lerobot.utils", raising=False) feature_utils_mod = ModuleType("lerobot.datasets.feature_utils") feature_utils_mod.hw_to_dataset_features = hw_to_dataset_features # type: ignore[attr-defined] datasets.feature_utils = feature_utils_mod # type: ignore[attr-defined] modules["lerobot.datasets.feature_utils"] = feature_utils_mod - else: + elif feature_layout == "v050": # lerobot == 0.5.0 layout: hw_to_dataset_features in lerobot.datasets.utils # and NO feature_utils module (the import must raise ImportError). + monkeypatch.delitem(sys.modules, "lerobot.utils.feature_utils", raising=False) + monkeypatch.delitem(sys.modules, "lerobot.utils", raising=False) monkeypatch.delitem(sys.modules, "lerobot.datasets.feature_utils", raising=False) utils_mod = ModuleType("lerobot.datasets.utils") utils_mod.hw_to_dataset_features = hw_to_dataset_features # type: ignore[attr-defined] datasets.utils = utils_mod # type: ignore[attr-defined] modules["lerobot.datasets.utils"] = utils_mod + else: + raise ValueError(f"unknown feature layout: {feature_layout}") for name, mod in modules.items(): monkeypatch.setitem(sys.modules, name, mod) @@ -370,7 +388,22 @@ def test_default_predict_rejects_camera_feature_mismatch( def test_default_predict_hw_features_fallback_import(monkeypatch: pytest.MonkeyPatch) -> None: # lerobot 0.5.0 layout: no lerobot.datasets.feature_utils -> fall back to # lerobot.datasets.utils. - calls = _install_lerobot_fakes(monkeypatch, chunk=np.ones((1, 2, 6)), feature_utils=False) + calls = _install_lerobot_fakes(monkeypatch, chunk=np.ones((1, 2, 6)), feature_layout="v050") + pol = LeRobotPolicy(_seam_cfg()) + pol.reset(Scene(id="s", instruction="x")) + out = pol.act(_obs()) + assert len(out) == 2 + assert calls["hw_prefix"] == "observation" + + +def test_default_predict_hw_features_v051_fallback_import( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls = _install_lerobot_fakes( + monkeypatch, + chunk=np.ones((1, 2, 6)), + feature_layout="v051", + ) pol = LeRobotPolicy(_seam_cfg()) pol.reset(Scene(id="s", instruction="x")) out = pol.act(_obs())