Skip to content

feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)#43

Closed
yinsong1986 wants to merge 1 commit into
strands-labs:mainfrom
yinsong1986:feat/isaac-extras-and-lazy-import
Closed

feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)#43
yinsong1986 wants to merge 1 commit into
strands-labs:mainfrom
yinsong1986:feat/isaac-extras-and-lazy-import

Conversation

@yinsong1986
Copy link
Copy Markdown
Contributor

Summary

PR-1 of 5 in the #31 split tracked by #42. Lands the dependency-free bottom of the 5-PR graph: the [isaac] optional-dependencies extra, the strands_robots.backends entry points (isaac + isaac_sim alias), and the PEP 562 lazy stub at strands_robots_sim/isaac/__init__.py.

This is the slice that demonstrates entry-point discovery without any simulation code. The lazy stub references strands_robots_sim.isaac.simulation:IsaacSimulation and strands_robots_sim.isaac.config:IsaacConfig, which land in PR-4 and PR-2 respectively. The entry points in pyproject.toml are intentionally "dangling" until PR-4 merges; PR-1's tests verify declaration (parsing pyproject.toml) and the lazy-import contract (import strands_robots_sim.isaac adds zero omni.* modules to sys.modules), not loadability of the resolved class.

Lands first in the dependency graph below; PR-2 + PR-3 unblock once this merges, then PR-4 needs both, and PR-5 is parallel-mergeable with PR-4.

PR-1 (this PR — entry-point + extras + lazy import)
  ├── PR-2 (config dataclass)
  └── PR-3 (procedural USD builders)
        └── PR-4 (IsaacSimulation backend)
              ├── PR-5 (docs, parallel)
              └── close #31

What changed

File Change
pyproject.toml + [isaac] extras (usd-core>=24.5,<26.0, warp-lang>=1.13.0, pytest>=7.0); + [project.entry-points."strands_robots.backends"] for isaac and isaac_sim alias; + [tool.pytest.ini_options] markers (gpu marker reserved for PR-4's test_gpu_integ.py); + all extra rolling up [isaac]; hatch test script switched from import-smoke to pytest strands_robots_sim/isaac/tests/ -v.
strands_robots_sim/isaac/__init__.py PEP 562 __getattr__ lazy resolution for IsaacSimulation + IsaacConfig. __all__ advertises the planned public surface.
strands_robots_sim/isaac/tests/__init__.py Empty package marker.
strands_robots_sim/isaac/tests/test_entrypoint.py 10 tests scoped to what PR-1 actually delivers (see below).

Diff size: +233 / −7, 4 files. Within the issue's "~250 LOC" budget for PR-1.

Test surface (10 tests)

Two test classes, scoped to what PR-1 can prove standalone:

TestEntryPointDeclaration (6 tests)

  • test_pyproject_exists — locate pyproject.toml
  • test_isaac_entry_point_declared_in_pyprojectisaac = "strands_robots_sim.isaac.simulation:IsaacSimulation" present
  • test_isaac_sim_alias_entry_point_declared_in_pyprojectisaac_sim alias present
  • test_isaac_extra_declared_in_pyproject[isaac] extras block exists
  • test_isaac_extra_includes_pytest[isaac] ships pytest so pip install '.[isaac]' covers test deps
  • test_entry_points_visible_via_importlib_metadata_when_installed — runtime entry-point discovery (skips gracefully if package isn't pip-installed in the env)

TestLazyImportSurface (4 tests)

  • test_import_isaac_does_not_load_omniimport strands_robots_sim.isaac adds zero omni.* modules to sys.modules
  • test_isaac_subpackage_exposes_lazy_attrs_in___all____all__ advertises both classes
  • test_unknown_attr_raises_attributeerror — unknown attribute access raises AttributeError, not ImportError (PEP 562 hook hygiene)
  • test_dunder_getattr_is_present — module-level __getattr__ exists and is callable

Tests covering SimEngine subclassing, abstract-method completeness, is_available() return shape, and no-GPU constructor instantiation are intentionally deferred to PR-4 because they require importing the simulation module that hasn't landed yet. Including them here would re-create the cross-PR dependency the split is trying to avoid.

Acceptance criteria from #42 (PR-1 row)

  • pyproject.toml: [isaac] extras
  • pyproject.toml: entry-points (isaac, isaac_sim alias)
  • isaac/__init__.py: PEP 562 lazy stub
  • isaac/tests/__init__.py
  • isaac/tests/test_entrypoint.py
  • Demonstrates entry-point discovery without any simulation code
  • Lands first in dependency-graph order
  • ~250 LOC (actual: 233)
  • CI green standalone (parallel-mergeable group)

Local validation

$ hatch run lint
black --check strands_robots_sim examples         # clean
isort --check-only strands_robots_sim examples    # clean
flake8 strands_robots_sim examples                # clean

$ hatch run test
9 passed, 1 skipped in 0.04s
# (the skip is `test_entry_points_visible_via_importlib_metadata_when_installed`;
#  hatch's hash-locked virtualenv doesn't see the source-tree editable install,
#  so importlib.metadata returns no entry points there. The test surface is correct
#  -- skip vs pass is purely env-dependent.)

$ pip install -e . && python -m pytest strands_robots_sim/isaac/tests/ -v
10 passed in 0.14s
# (under editable install, the importlib.metadata skip becomes a pass --
#  both `isaac` and `isaac_sim` resolve to
#  `strands_robots_sim.isaac.simulation:IsaacSimulation`.)

$ python -c "import sys; import strands_robots_sim.isaac; print([m for m in sys.modules if m.startswith('omni')])"
[]
# zero `omni.*` modules loaded on import — PEP 562 contract holds.

Out-of-scope follow-ups (deferred to other PRs in the split)

  • PR-2IsaacConfig dataclass + R1's "reject unknown kwargs" pin
  • PR-3 — procedural USD builders (SO-100 / Panda / G1) + R2's G1-DOF=21 truth-source pin
  • PR-4IsaacSimulation backend with R1's omni.isaac.kit-specific is_available() probe + dropped unsafe __del__ + narrow except clauses + R4's get_observation diagnostic logs
  • PR-5docs/backends/isaac.md reference with R2's Phase-1-status disclosure banner

The 9 review threads on #31 stay open until the corresponding child PR closes them. R1/R2/R3/R4 commit chronicle on #31 remains the forensic source of truth; child PRs preserve those commits as cherry-picks where the review-thread anchors apply (mirrors strands-labs/robots#195 → #220-#228 / robots#219 precedent).

Project board

This PR's issue #42 is the umbrella tracker; please move to "In review" on the Strands Labs - Robots board.

Refs: #31, #42

strands-labs#31 split)

PR-1 of the 5-PR split of strands-labs#31 (tracker: strands-labs#42). Lands the dependency-free
bottom of the graph: the [isaac] optional-dependencies extra, the
`strands_robots.backends` entry points (`isaac` + `isaac_sim` alias),
and the PEP 562 lazy stub at `strands_robots_sim/isaac/__init__.py`.

This is the slice that demonstrates entry-point discovery without any
simulation code -- the lazy stub references
`strands_robots_sim.isaac.simulation:IsaacSimulation` and
`strands_robots_sim.isaac.config:IsaacConfig` which land in PR-4 and
PR-2 respectively. The entry point in pyproject.toml is intentionally
"dangling" until PR-4 merges; PR-1's tests verify *declaration* (parsing
pyproject.toml) and the lazy-import contract (`import
strands_robots_sim.isaac` adds zero `omni.*` modules to `sys.modules`),
not loadability of the resolved class.

Changes:
- pyproject.toml: `[isaac]` extra (usd-core>=24.5,<26.0, warp-lang>=1.13.0,
  pytest>=7.0), `[project.entry-points."strands_robots.backends"]` for
  `isaac` + `isaac_sim` alias, `[tool.pytest.ini_options]` markers (`gpu`
  for the test_gpu_integ.py marker that lands in PR-4), `[isaac]` extra
  rolled into `all`, hatch test script switched from import-smoke to
  `pytest strands_robots_sim/isaac/tests/ -v`.
- strands_robots_sim/isaac/__init__.py: PEP 562 `__getattr__` lazy
  resolution for `IsaacSimulation` + `IsaacConfig`. `__all__` advertises
  the planned public surface.
- strands_robots_sim/isaac/tests/__init__.py: empty package marker.
- strands_robots_sim/isaac/tests/test_entrypoint.py: 10 tests scoped to
  what PR-1 actually delivers -- entry-point declaration in pyproject.toml,
  `[isaac]` extras presence + pytest inclusion, importlib.metadata
  discovery (skipped if package not pip-installed), no-omni-on-import
  contract, `__all__` advertisement, AttributeError-not-ImportError on
  unknown attrs, and presence of the PEP 562 hook itself.

Tests covering `SimEngine` subclassing, abstract-method completeness,
`is_available()` return shape, and no-GPU constructor instantiation are
intentionally deferred to PR-4 because they require importing the
`simulation` module that hasn't landed yet -- including them here would
re-create the cross-PR dependency the split is trying to avoid.

Local validation:
- `hatch run lint` -> clean (black/isort/flake8 on all touched files)
- `hatch run test` -> 9 passed, 1 skipped (importlib.metadata skip is
  expected; hatch's hash-locked virtualenv doesn't see the source-tree
  editable install. Re-running under `pip install -e .` -> 10 passed.)
- `pip install -e .` + `python -c" "import strands_robots_sim.isaac" -> OK
- entry-point lookup in editable env -> both `isaac` and `isaac_sim`
  resolve to `strands_robots_sim.isaac.simulation:IsaacSimulation`.

Refs: strands-labs#31, strands-labs#42 (umbrella tracker for the 5-PR split)
Copy link
Copy Markdown
Member

@cagataycali cagataycali left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local review on Thor — duplicate of #44 but actually has the better artifact

Reviewed PR43 vs PR44 side-by-side. They have the same lazy __init__.py contract but differ on test surface and hatch script:

Aspect PR43 (this) PR44
Branch feat/isaac-extras-and-lazy-import pr1/isaac-foundation
Tests 10 tests in test_entrypoint.py, all pass 0 tests
Hatch test script pytest strands_robots_sim/isaac/tests/ -v placeholder import-smoke
pyproject [isaac] extra includes pytest>=7.0 doesn't
Lazy contract verified ✅ (zero omni modules loaded)
Stack lineage orphan (PR45/46/47/48 don't branch from this) parent of PR45/46/47/48

Verdict

PR43 is the better artifact, but PR44 is the structurally correct one because PR45-48 all branched from PR44's commit 3a0a854, not from PR43's e717e71.

Recommendation

  1. Close this PR (#43) as superseded by #44.
  2. Port the 10 tests + hatch test script + pytest>=7.0 extra from this PR into #44 as a new commit on pr1/isaac-foundation. The 10 tests are good (they already pass cleanly on Thor) and PR44's lack-of-tests is the main gap I'd block #44 on otherwise.
  3. The 10 tests carved cleanly so they only verify what PR-1 actually delivers (entry-point declaration + lazy-import contract) — no cross-PR coupling. Safe to add.

If you'd rather close PR44 and keep this one, the cost is rebasing PR45/46/47/48 onto PR43's branch — also fine, just 4 rebases.

@yinsong1986
Copy link
Copy Markdown
Contributor Author

Closing as duplicate of #44 — both cover the same "1/5 of #31 split" slice, but #44 is the canonical chain head (followed by #45 / #46 / #47 / #48 per #42).

Substantive shape difference:

#43 #44
tests/test_entrypoint.py included deferred to #47 alongside simulation.py
pyproject.toml test command flipped to pytest … left as import-smoke placeholder
Standalone CI red (3/4 entry-point tests need simulation.py) green

The deferral of test_entrypoint.py to #47 keeps the foundation slice CI-green standalone — test_entrypoint.py imports from strands_robots_sim.isaac.simulation import IsaacSimulation, SimEngine at the top of every test, so it can't pass without PR-4 in place.

No work is lost: #44 keeps the same pyproject.toml extras + entry-points + pytest dep + gpu marker that #43 had; the test_entrypoint.py content lands in #47 where its imports are satisfied.

cagataycali pushed a commit that referenced this pull request May 26, 2026
#31 split) (#44)

* feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)

Part 1 / 5 of the split of #31 — tracked by #42.

Adds the package skeleton for the Isaac Sim backend with zero `omni`
overhead on `import strands_robots_sim`:

- `pyproject.toml`:
  - new `[isaac]` extra (lightweight Isaac-companion deps that ARE
    pip-installable: `usd-core>=24.5,<26.0`, `warp-lang>=1.13.0`).
    Comment explains that Isaac Sim itself is NOT installable from
    PyPI — must come via Omniverse Launcher / Isaac Lab / nvcr.io.
  - new `[all]` aggregate extra (currently delegates to `[isaac]`;
    will gain `[newton]` etc. as later backends land).
  - new `[project.entry-points."strands_robots.backends"]` declaring
    both `isaac` and `isaac_sim` aliases pointing at
    `strands_robots_sim.isaac.simulation:IsaacSimulation` (the class
    itself lands in PR-4 / `feat(isaac): IsaacSimulation backend`).
  - new `pytest>=7.0` in `dev` extras and `[tool.hatch.envs.default]`
    deps so future PRs in this split chain can run pytest under
    `hatch run test`.
  - new `[tool.pytest.ini_options]` with a `gpu` marker so PR-4's
    GPU-only integ tests can gate on `STRANDS_GPU_TEST=1`.

- `strands_robots_sim/isaac/__init__.py`:
  PEP 562 lazy stub — declares `__all__ = ["IsaacSimulation",
  "IsaacConfig"]` and routes attribute access through `__getattr__`
  to deferred imports of `.simulation` and `.config`. The bare
  `import strands_robots_sim.isaac` does NOT import any `omni`
  modules; that overhead only fires when a caller actually accesses
  `IsaacSimulation` or `IsaacConfig`.

- `strands_robots_sim/isaac/tests/__init__.py`: empty package marker
  so future test files in this directory are collectable. Test files
  themselves land in subsequent slices alongside the code they cover.

CI signal: lint clean (black / isort / flake8); the existing
`hatch run test` import-smoke passes (it doesn't touch `isaac`, so
no missing-submodule errors). Pytest doesn't run yet from `hatch
run test` — the test command stays as the import-smoke placeholder
in this slice and flips to `pytest strands_robots_sim/isaac/tests/`
in PR-3 once the first real test files land.

Why this lands first: PR-2 / PR-3 / PR-4 all import from
`strands_robots_sim.isaac`, so the package stub + extras + entry
points have to exist before any of those files can be code-reviewed
against current `main`.

Original work by @cagataycali in #31 (`413ff15..befb1e3`); this
slice cherry-picks just the foundation files. Review-thread anchors
on the original commits stay intact for whichever child PR consumes
the relevant code.

* review(isaac): port 10 entry-point + lazy-import tests from #43 (PR #44 R1)

Closes @cagataycali's R1 review on #44 -- the lazy-import contract
ships with concrete CI signal now, not just a manual probe in the PR
description.

Three coordinated changes per the review:

1. **Port `tests/test_entrypoint.py` (140 LOC, 10 tests)** from the
   closed orphan PR #43 (`feat/isaac-extras-and-lazy-import`). These
   tests verify *only* PR-1's surface and have **zero dependency on
   `simulation.py`** (which lands in PR-4 of the #31 split):

   - `TestEntryPointDeclaration` (6 tests):
     - `test_pyproject_exists`
     - `test_isaac_entry_point_declared_in_pyproject`
     - `test_isaac_sim_alias_entry_point_declared_in_pyproject`
     - `test_isaac_extra_declared_in_pyproject`
     - `test_isaac_extra_includes_pytest`
     - `test_entry_points_visible_via_importlib_metadata_when_installed`
       (skips with actionable hint when not pip-installed)

   - `TestLazyImportSurface` (4 tests):
     - `test_import_isaac_does_not_load_omni` -- pins the `omni`-not-
       loaded contract via `sys.modules` set diff before/after
       import.
     - `test_isaac_subpackage_exposes_lazy_attrs_in___all__`
     - `test_unknown_attr_raises_attributeerror` -- pins the PEP 562
       `__getattr__` raises `AttributeError` (not `ImportError`) on
       unknown attrs.
     - `test_dunder_getattr_is_present`

2. **Add `pytest>=7.0` to the `[isaac]` extra** in `pyproject.toml`.
   Means `pip install '.[isaac]'` covers the test deps without a
   separate dev extra on CI hosts. Also pinned by
   `test_isaac_extra_includes_pytest`.

3. **Flip `hatch run test`** from the import-smoke placeholder
   (`python -c "import strands_robots_sim; ..."`) to
   `pytest strands_robots_sim/isaac/tests/ -v`. The 10 tests collect
   and pass standalone; PR-4's simulation-module tests join the
   collection automatically when that PR rebases.

Verification:

- `pytest strands_robots_sim/isaac/tests/ -v` -> **10 passed in 0.14s**
- `black --check` / `isort --check-only` / `flake8` (max-line=120) on
  `strands_robots_sim/` + `examples/` -> clean
- AST parse on touched files -> ok
- The 10 tests have no import path through `simulation.py`; they
  validate only PR-1's actual surface (pyproject parsing, entry-
  point declaration, PEP 562 `__getattr__` contract).

The original `test_entrypoint.py` on `cagataycali/feat/isaac-sim`
imports from `strands_robots_sim.isaac.simulation`, which is why
PR-1 of the split couldn't ship that file unmodified. The version
ported here was rewritten on the #43 branch specifically for the
PR-1 surface; cleanly self-contained against this slice.
cagataycali pushed a commit that referenced this pull request May 26, 2026
…#45)

* feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)

Part 1 / 5 of the split of #31 — tracked by #42.

Adds the package skeleton for the Isaac Sim backend with zero `omni`
overhead on `import strands_robots_sim`:

- `pyproject.toml`:
  - new `[isaac]` extra (lightweight Isaac-companion deps that ARE
    pip-installable: `usd-core>=24.5,<26.0`, `warp-lang>=1.13.0`).
    Comment explains that Isaac Sim itself is NOT installable from
    PyPI — must come via Omniverse Launcher / Isaac Lab / nvcr.io.
  - new `[all]` aggregate extra (currently delegates to `[isaac]`;
    will gain `[newton]` etc. as later backends land).
  - new `[project.entry-points."strands_robots.backends"]` declaring
    both `isaac` and `isaac_sim` aliases pointing at
    `strands_robots_sim.isaac.simulation:IsaacSimulation` (the class
    itself lands in PR-4 / `feat(isaac): IsaacSimulation backend`).
  - new `pytest>=7.0` in `dev` extras and `[tool.hatch.envs.default]`
    deps so future PRs in this split chain can run pytest under
    `hatch run test`.
  - new `[tool.pytest.ini_options]` with a `gpu` marker so PR-4's
    GPU-only integ tests can gate on `STRANDS_GPU_TEST=1`.

- `strands_robots_sim/isaac/__init__.py`:
  PEP 562 lazy stub — declares `__all__ = ["IsaacSimulation",
  "IsaacConfig"]` and routes attribute access through `__getattr__`
  to deferred imports of `.simulation` and `.config`. The bare
  `import strands_robots_sim.isaac` does NOT import any `omni`
  modules; that overhead only fires when a caller actually accesses
  `IsaacSimulation` or `IsaacConfig`.

- `strands_robots_sim/isaac/tests/__init__.py`: empty package marker
  so future test files in this directory are collectable. Test files
  themselves land in subsequent slices alongside the code they cover.

CI signal: lint clean (black / isort / flake8); the existing
`hatch run test` import-smoke passes (it doesn't touch `isaac`, so
no missing-submodule errors). Pytest doesn't run yet from `hatch
run test` — the test command stays as the import-smoke placeholder
in this slice and flips to `pytest strands_robots_sim/isaac/tests/`
in PR-3 once the first real test files land.

Why this lands first: PR-2 / PR-3 / PR-4 all import from
`strands_robots_sim.isaac`, so the package stub + extras + entry
points have to exist before any of those files can be code-reviewed
against current `main`.

Original work by @cagataycali in #31 (`413ff15..befb1e3`); this
slice cherry-picks just the foundation files. Review-thread anchors
on the original commits stay intact for whichever child PR consumes
the relevant code.

* review(isaac): port 10 entry-point + lazy-import tests from #43 (PR #44 R1)

Closes @cagataycali's R1 review on #44 -- the lazy-import contract
ships with concrete CI signal now, not just a manual probe in the PR
description.

Three coordinated changes per the review:

1. **Port `tests/test_entrypoint.py` (140 LOC, 10 tests)** from the
   closed orphan PR #43 (`feat/isaac-extras-and-lazy-import`). These
   tests verify *only* PR-1's surface and have **zero dependency on
   `simulation.py`** (which lands in PR-4 of the #31 split):

   - `TestEntryPointDeclaration` (6 tests):
     - `test_pyproject_exists`
     - `test_isaac_entry_point_declared_in_pyproject`
     - `test_isaac_sim_alias_entry_point_declared_in_pyproject`
     - `test_isaac_extra_declared_in_pyproject`
     - `test_isaac_extra_includes_pytest`
     - `test_entry_points_visible_via_importlib_metadata_when_installed`
       (skips with actionable hint when not pip-installed)

   - `TestLazyImportSurface` (4 tests):
     - `test_import_isaac_does_not_load_omni` -- pins the `omni`-not-
       loaded contract via `sys.modules` set diff before/after
       import.
     - `test_isaac_subpackage_exposes_lazy_attrs_in___all__`
     - `test_unknown_attr_raises_attributeerror` -- pins the PEP 562
       `__getattr__` raises `AttributeError` (not `ImportError`) on
       unknown attrs.
     - `test_dunder_getattr_is_present`

2. **Add `pytest>=7.0` to the `[isaac]` extra** in `pyproject.toml`.
   Means `pip install '.[isaac]'` covers the test deps without a
   separate dev extra on CI hosts. Also pinned by
   `test_isaac_extra_includes_pytest`.

3. **Flip `hatch run test`** from the import-smoke placeholder
   (`python -c "import strands_robots_sim; ..."`) to
   `pytest strands_robots_sim/isaac/tests/ -v`. The 10 tests collect
   and pass standalone; PR-4's simulation-module tests join the
   collection automatically when that PR rebases.

Verification:

- `pytest strands_robots_sim/isaac/tests/ -v` -> **10 passed in 0.14s**
- `black --check` / `isort --check-only` / `flake8` (max-line=120) on
  `strands_robots_sim/` + `examples/` -> clean
- AST parse on touched files -> ok
- The 10 tests have no import path through `simulation.py`; they
  validate only PR-1's actual surface (pyproject parsing, entry-
  point declaration, PEP 562 `__getattr__` contract).

The original `test_entrypoint.py` on `cagataycali/feat/isaac-sim`
imports from `strands_robots_sim.isaac.simulation`, which is why
PR-1 of the split couldn't ship that file unmodified. The version
ported here was rewritten on the #43 branch specifically for the
PR-1 surface; cleanly self-contained against this slice.

* feat(isaac): IsaacConfig dataclass with validation (2/5 of #31 split)

Part 2 / 5 of the split of #31 — tracked by #42. Branched off PR-1
(`pr1/isaac-foundation` -> #44); rebases cleanly to `main` once PR-1
merges.

Adds the configuration dataclass that `IsaacSimulation` (PR-4) consumes:

- `strands_robots_sim/isaac/config.py` (116 LOC):
  `IsaacConfig` dataclass with:
  - `num_envs`, `device` (default `"cuda:0"`), `headless` (default
    `True` -- safe for cloud/CI runners), `render_mode`, `physics_dt`,
    `gravity`, `ground_plane`, `camera_width`, `camera_height`,
    `rtx_pathtracing`, `nucleus_url`.
  - environment-variable overrides via `STRANDS_ISAAC_*` (`HEADLESS`,
    `RTX_PATHTRACING`, `NUCLEUS_URL`).
  - dataclass-level validation in `__post_init__` so e.g.
    `IsaacConfig(num_envs=0)` raises `ValueError` at construction.
  - `from_kwargs` classmethod that PR-4's `IsaacSimulation.__init__`
    uses to validate caller-supplied kwargs and surface unknown keys
    as `TypeError` (the R1 "reject unknown kwargs" pin).

No `omni` / Isaac Sim imports. Pure-Python dataclass; can be imported
on any host without a GPU. Tests for this module ship in PR-4
alongside `test_unit.py::TestIsaacConfig` (cleaner than carving a
test slice for ~120 LOC of dataclass).

CI signal: lint clean (black / isort / flake8); the lazy
`__init__.py` from PR-1 now resolves `IsaacConfig` to a real class
when accessed -- the `from strands_robots_sim.isaac import
IsaacConfig` import path becomes live in this slice.

Original work by @cagataycali in #31 (`413ff15`); this slice
cherry-picks just `config.py`. R1's `from_kwargs` validation pin
(commit `32ef307`) is already baked into this file.

* fix(isaac): address review comment on PR #45

cagataycali's comment at strands_robots_sim/isaac/config.py:116 flagged
that from_kwargs was promised by the PR description but missing from
the slice. Added the classmethod so PR-4 (#47) can DRY its
unknown-kwarg validation against IsaacConfig.from_kwargs rather than
inlining dataclasses.fields() reflection at simulation.py:174-180.
yinsong1986 added a commit that referenced this pull request May 27, 2026
…split) (#46)

* feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)

Part 1 / 5 of the split of #31 — tracked by #42.

Adds the package skeleton for the Isaac Sim backend with zero `omni`
overhead on `import strands_robots_sim`:

- `pyproject.toml`:
  - new `[isaac]` extra (lightweight Isaac-companion deps that ARE
    pip-installable: `usd-core>=24.5,<26.0`, `warp-lang>=1.13.0`).
    Comment explains that Isaac Sim itself is NOT installable from
    PyPI — must come via Omniverse Launcher / Isaac Lab / nvcr.io.
  - new `[all]` aggregate extra (currently delegates to `[isaac]`;
    will gain `[newton]` etc. as later backends land).
  - new `[project.entry-points."strands_robots.backends"]` declaring
    both `isaac` and `isaac_sim` aliases pointing at
    `strands_robots_sim.isaac.simulation:IsaacSimulation` (the class
    itself lands in PR-4 / `feat(isaac): IsaacSimulation backend`).
  - new `pytest>=7.0` in `dev` extras and `[tool.hatch.envs.default]`
    deps so future PRs in this split chain can run pytest under
    `hatch run test`.
  - new `[tool.pytest.ini_options]` with a `gpu` marker so PR-4's
    GPU-only integ tests can gate on `STRANDS_GPU_TEST=1`.

- `strands_robots_sim/isaac/__init__.py`:
  PEP 562 lazy stub — declares `__all__ = ["IsaacSimulation",
  "IsaacConfig"]` and routes attribute access through `__getattr__`
  to deferred imports of `.simulation` and `.config`. The bare
  `import strands_robots_sim.isaac` does NOT import any `omni`
  modules; that overhead only fires when a caller actually accesses
  `IsaacSimulation` or `IsaacConfig`.

- `strands_robots_sim/isaac/tests/__init__.py`: empty package marker
  so future test files in this directory are collectable. Test files
  themselves land in subsequent slices alongside the code they cover.

CI signal: lint clean (black / isort / flake8); the existing
`hatch run test` import-smoke passes (it doesn't touch `isaac`, so
no missing-submodule errors). Pytest doesn't run yet from `hatch
run test` — the test command stays as the import-smoke placeholder
in this slice and flips to `pytest strands_robots_sim/isaac/tests/`
in PR-3 once the first real test files land.

Why this lands first: PR-2 / PR-3 / PR-4 all import from
`strands_robots_sim.isaac`, so the package stub + extras + entry
points have to exist before any of those files can be code-reviewed
against current `main`.

Original work by @cagataycali in #31 (`413ff15..befb1e3`); this
slice cherry-picks just the foundation files. Review-thread anchors
on the original commits stay intact for whichever child PR consumes
the relevant code.

* review(isaac): port 10 entry-point + lazy-import tests from #43 (PR #44 R1)

Closes @cagataycali's R1 review on #44 -- the lazy-import contract
ships with concrete CI signal now, not just a manual probe in the PR
description.

Three coordinated changes per the review:

1. **Port `tests/test_entrypoint.py` (140 LOC, 10 tests)** from the
   closed orphan PR #43 (`feat/isaac-extras-and-lazy-import`). These
   tests verify *only* PR-1's surface and have **zero dependency on
   `simulation.py`** (which lands in PR-4 of the #31 split):

   - `TestEntryPointDeclaration` (6 tests):
     - `test_pyproject_exists`
     - `test_isaac_entry_point_declared_in_pyproject`
     - `test_isaac_sim_alias_entry_point_declared_in_pyproject`
     - `test_isaac_extra_declared_in_pyproject`
     - `test_isaac_extra_includes_pytest`
     - `test_entry_points_visible_via_importlib_metadata_when_installed`
       (skips with actionable hint when not pip-installed)

   - `TestLazyImportSurface` (4 tests):
     - `test_import_isaac_does_not_load_omni` -- pins the `omni`-not-
       loaded contract via `sys.modules` set diff before/after
       import.
     - `test_isaac_subpackage_exposes_lazy_attrs_in___all__`
     - `test_unknown_attr_raises_attributeerror` -- pins the PEP 562
       `__getattr__` raises `AttributeError` (not `ImportError`) on
       unknown attrs.
     - `test_dunder_getattr_is_present`

2. **Add `pytest>=7.0` to the `[isaac]` extra** in `pyproject.toml`.
   Means `pip install '.[isaac]'` covers the test deps without a
   separate dev extra on CI hosts. Also pinned by
   `test_isaac_extra_includes_pytest`.

3. **Flip `hatch run test`** from the import-smoke placeholder
   (`python -c "import strands_robots_sim; ..."`) to
   `pytest strands_robots_sim/isaac/tests/ -v`. The 10 tests collect
   and pass standalone; PR-4's simulation-module tests join the
   collection automatically when that PR rebases.

Verification:

- `pytest strands_robots_sim/isaac/tests/ -v` -> **10 passed in 0.14s**
- `black --check` / `isort --check-only` / `flake8` (max-line=120) on
  `strands_robots_sim/` + `examples/` -> clean
- AST parse on touched files -> ok
- The 10 tests have no import path through `simulation.py`; they
  validate only PR-1's actual surface (pyproject parsing, entry-
  point declaration, PEP 562 `__getattr__` contract).

The original `test_entrypoint.py` on `cagataycali/feat/isaac-sim`
imports from `strands_robots_sim.isaac.simulation`, which is why
PR-1 of the split couldn't ship that file unmodified. The version
ported here was rewritten on the #43 branch specifically for the
PR-1 surface; cleanly self-contained against this slice.

* feat(isaac): procedural USD builders (SO-100, Panda, G1) (3/5 of #31 split)

Part 3 / 5 of the split of #31 — tracked by #42. Branched off PR-1
(`pr1/isaac-foundation` -> #44); rebases cleanly onto `main` once
PR-1 merges.

Adds the procedural USD robot builders that `IsaacSimulation`
(PR-4) consumes via `add_robot(data_config=<name>, procedural=True)`
without requiring binary asset downloads from Nucleus:

- `strands_robots_sim/isaac/procedural.py` (258 LOC):
  - `ProceduralRobot` named-tuple shape: `(prim_root, num_joints,
    joint_names, body_names, parent_body_indices)`.
  - `get_procedural_robot(name: str) -> ProceduralRobot` registry.
  - Three builders behind the registry:
    - `_build_so100`: 6-DOF SO-100 arm (HuggingFace's open-source
      teaching arm).
    - `_build_franka_panda`: 9-DOF Panda + 2-DOF gripper.
    - `_build_unitree_g1`: 21-DOF G1 humanoid (1 torso + 6 left leg
      + 6 right leg + 4 left arm + 4 right arm).
  - All builders are pure-Python; no `omni` / Isaac Sim imports.

- `strands_robots_sim/isaac/tests/test_procedural_g1_dof.py` (~70
  LOC, 3 tests):
  Carved out of cagataycali's original
  `test_phase1_doc_honesty.py` -- the `TestG1DOFCount` class only.
  The companion `TestIsaacDocsPhase1Banner` class (which reads
  `docs/backends/isaac.md`) lands in PR-5 alongside the docs file
  itself. Pin tests:
  - `test_g1_actual_joint_count_is_21`: truth-source pin against
    `procedural.py`'s actual joint set.
  - `test_g1_module_docstring_advertises_21_not_29`: pin against
    R2's stale 29-DOF claim in the module docstring (commit
    `85e180f`).
  - `test_g1_builder_docstring_advertises_21_not_29`: same pin on
    the `_build_unitree_g1` function docstring.

CI signal: lint clean (black / isort / flake8); the 3 G1-DOF pin
tests pass standalone (`pytest strands_robots_sim/isaac/tests/
test_procedural_g1_dof.py -v` -> 3 passed).

Note: this slice does NOT pin the kinematic-tree topology defect
on the G1 joint graph (duplicate `(parent_body, child_body)`
edges -- e.g. `l_hip_roll` and `l_hip_pitch` both map bodies 3 ->
4). That defect is documented in `procedural.py` line 165 as
deferred to Phase 2; it requires intermediate massless link
bodies that this Phase 1 stub does not instantiate. The dormant-
on-Phase-1 framing is the same one R2 captured.

Why this lands in parallel with PR-2 (config): both are pure
modules with no cross-dependencies; either can land first. PR-4
(simulation) needs both.

Original work by @cagataycali in #31 (`413ff15..85e180f`); this
slice cherry-picks `procedural.py` plus the carved-out G1 test
file. R2's DOF-count fix (commit `85e180f`) is already baked into
this file's docstrings.

* fix(isaac): add Phase-1 fail-fast guard for duplicate kinematic edges

cagataycali's comment at strands_robots_sim/isaac/procedural.py:213
flagged that the documented duplicate-edge defect on G1 is silently
returned by _build_unitree_g1 and would only surface as a cryptic
USD/MuJoCo articulation error two layers down on Phase-2 wireup.

Adds _validate_kinematic_tree(), wired into the end of all three
procedural builders, opt-in via STRANDS_ISAAC_VALIDATE_KINEMATICS=1
(Phase-2 dev path). Default is no-op so Phase-1 callers (which never
instantiate the articulation) are unaffected; with the env var set,
G1 raises ValueError naming the duplicate edges + offending joints,
while SO-100 and Panda still build cleanly.

Pinned by tests/test_procedural_kinematic_guard.py: default-off
behaviour, accepted env values (1/true/yes, case-insensitive),
G1 raises with diagnostic context, SO-100 and Panda pass guard
when enabled.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants