Skip to content

Commit 4a4679e

Browse files
committed
fix(spikeinterface_types): address PR review comments (ITL-459)
- Move pytest.importorskip() into each SI-dependent test so that test_spikeinterface_not_installed_raises_import_error runs without SI - Fix finally block cleanup to handle ImportError when SI is genuinely absent - Update spec: LazyModule -> try/except ImportError, v0.1.json -> register_spikeinterface_types(), load_extractor -> spikeinterface.core.load throughout
1 parent efa13c0 commit 4a4679e

2 files changed

Lines changed: 37 additions & 18 deletions

File tree

superpowers/specs/2026-07-01-spikeinterface-baserecording-design.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ it. orcapod stores that JSON string and delegates all disk I/O to SI.
2727
return values with `BaseRecording` without errors.
2828
- `python_to_storage()` serializes via `recording.to_dict(recursive=True)` → JSON string.
2929
Raises a clear `ValueError` for in-memory recordings (`check_serializability("json") == False`).
30-
- `storage_to_python()` reconstructs via `spikeinterface.core.load_extractor(dict)`.
30+
- `storage_to_python()` reconstructs via `spikeinterface.core.load(dict)`.
3131
- A `SIRecordingHandler` registered in the semantic hasher that hashes the JSON string
3232
(phase 1 — source-path content hashing is deferred to a follow-up issue).
3333
- `spikeinterface` available as `pip install orcapod[spikeinterface]`; imports are lazy so
@@ -43,8 +43,11 @@ it. orcapod stores that JSON string and delegates all disk I/O to SI.
4343

4444
New file: `src/orcapod/extension_types/spikeinterface_types.py`
4545

46-
SI imports use `LazyModule` (same pattern as polars/pyarrow in the codebase) so that
47-
`import orcapod` does not fail when SpikeInterface is not installed.
46+
`spikeinterface_types.py` imports `BaseRecording` at module level using a `try/except ImportError`
47+
block that re-raises with a clear pip-install message if SI is absent. The
48+
`extension_types/__init__.py` wraps the import in its own `try/except ImportError` so that
49+
`import orcapod` never fails when SI is not installed — the SI types are silently omitted from
50+
`__all__` in that case.
4851

4952
### Optional dependency
5053

@@ -57,9 +60,11 @@ spikeinterface = ["spikeinterface>=0.101"]
5760

5861
### Registration
5962

60-
`LogicalSIRecording` is registered in `contexts/data/v0.1.json` alongside
61-
`LogicalFile`, `LogicalDirectory`, and `LogicalNumpyArray`. The `SIRecordingHandler`
62-
is registered in the `python_type_handler_registry` in the same context config.
63+
`LogicalSIRecording` and `SIRecordingHandler` are **not** registered in
64+
`contexts/data/v0.1.json` — doing so would break startup for users who have not installed the
65+
`spikeinterface` extras group. Instead, users opt in explicitly by calling
66+
`register_spikeinterface_types()` once at application startup. This function accepts an optional
67+
`DataContext`; when called with no argument it registers into the default context.
6368

6469
---
6570

@@ -97,7 +102,7 @@ for `np.ndarray`.
97102
**`storage_to_python(storage_value, converter)`:**
98103

99104
1. Parses `json.loads(storage_value)` to recover the SI dict.
100-
2. Calls `spikeinterface.core.load_extractor(si_dict)` and returns the result.
105+
2. Calls `spikeinterface.core.load(si_dict)` and returns the result.
101106
3. If the backing zarr/folder has been moved or deleted, SI raises naturally — no
102107
additional wrapping needed (same behaviour as `LogicalDirectory` with a deleted path).
103108

@@ -132,7 +137,7 @@ hashing infrastructure to land first (ITL-467).
132137
|-----------|-----------|
133138
| `NumpyRecording` or any recording with `check_serializability("json") == False` | `ValueError` clarifying that lazy file-backed recordings are fine but in-memory ones are not, with save instructions |
134139
| `spikeinterface` not installed | `ImportError` on first use of `LogicalSIRecording`, message: `"Install spikeinterface: pip install orcapod[spikeinterface]"` |
135-
| Backing zarr/folder deleted after storage | SI raises `FileNotFoundError` or similar on `load_extractor()` — propagates as-is |
140+
| Backing zarr/folder deleted after storage | SI raises `FileNotFoundError` or similar on `load()` — propagates as-is |
136141
| Corrupt JSON in storage | `json.JSONDecodeError` raised with the raw value in the message |
137142

138143
---
@@ -171,7 +176,7 @@ so they are skipped automatically when SI is not installed.
171176

172177
- `src/orcapod/extension_types/directory_type.py` — pattern for JSON-storage LogicalType
173178
- `src/orcapod/extension_types/numpy_type.py` — pattern for direct python_type binding + hash handler
174-
- `src/orcapod/hashing/semantic_hashing/builtin_handlers.py`where `SIRecordingHandler` registers
175-
- `src/orcapod/contexts/data/v0.1.json` — context config where LogicalType + handler are wired in
179+
- `src/orcapod/extension_types/spikeinterface_types.py`implementation (LogicalType, handler, registration)
180+
- `src/orcapod/contexts/data/v0.1.json` — context config changelog entry (LogicalType/handler NOT wired in statically)
176181
- SpikeInterface `BaseExtractor.to_dict()` / `check_serializability()`: `spikeinterface/core/base.py`
177182
- ITL-467 — database-level artifact storage (phase 2 dependency)

tests/test_extension_types/test_spikeinterface_types.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99

1010

1111
def test_spikeinterface_not_installed_raises_import_error(monkeypatch):
12-
"""Importing spikeinterface_types when SI is absent raises ImportError."""
12+
"""Importing spikeinterface_types when SI is absent raises ImportError.
13+
14+
This test runs regardless of whether spikeinterface is installed — it
15+
simulates absence by blocking the import via sys.modules.
16+
"""
1317
import sys
1418
import importlib
1519

@@ -36,22 +40,24 @@ def test_spikeinterface_not_installed_raises_import_error(monkeypatch):
3640
sys.modules[si_types_key] = saved_si_types
3741
else:
3842
sys.modules.pop(si_types_key, None)
39-
importlib.import_module(si_types_key)
40-
41-
42-
# All tests below require spikeinterface — skip if not installed
43-
si = pytest.importorskip("spikeinterface", reason="spikeinterface not installed")
44-
import spikeinterface.core as si_core # noqa: E402
43+
# Re-import only if SI is genuinely available; if not, there is
44+
# nothing to restore and the ImportError is expected.
45+
try:
46+
importlib.import_module(si_types_key)
47+
except ImportError:
48+
pass
4549

4650

47-
def _make_numpy_recording() -> si_core.NumpyRecording:
51+
def _make_numpy_recording():
4852
"""Create a small in-memory NumpyRecording for use as a test source."""
53+
import spikeinterface.core as si_core
4954
rng = np.random.default_rng(42)
5055
traces = rng.standard_normal((200, 4)).astype("float32")
5156
return si_core.NumpyRecording([traces], sampling_frequency=30_000)
5257

5358

5459
def test_logical_si_recording_importable():
60+
si_core = pytest.importorskip("spikeinterface.core", reason="spikeinterface not installed")
5561
from orcapod.extension_types.spikeinterface_types import LogicalSIRecording
5662
import pyarrow as pa
5763

@@ -63,6 +69,7 @@ def test_logical_si_recording_importable():
6369

6470
def test_in_memory_recording_raises():
6571
"""NumpyRecording (json=False) raises ValueError with clear instructions."""
72+
pytest.importorskip("spikeinterface", reason="spikeinterface not installed")
6673
from orcapod.extension_types.spikeinterface_types import LogicalSIRecording
6774

6875
rec = _make_numpy_recording()
@@ -78,6 +85,7 @@ def test_in_memory_recording_raises():
7885

7986
def test_folder_recording_round_trip(tmp_path):
8087
"""Binary-folder-backed recording round-trips through python_to_storage / storage_to_python."""
88+
pytest.importorskip("spikeinterface", reason="spikeinterface not installed")
8189
from orcapod.extension_types.spikeinterface_types import LogicalSIRecording
8290

8391
saved = _make_numpy_recording().save_to_folder(str(tmp_path / "rec"))
@@ -97,6 +105,7 @@ def test_folder_recording_round_trip(tmp_path):
97105

98106
def test_zarr_recording_round_trip(tmp_path):
99107
"""Zarr-backed recording round-trips through python_to_storage / storage_to_python."""
108+
pytest.importorskip("spikeinterface", reason="spikeinterface not installed")
100109
from orcapod.extension_types.spikeinterface_types import LogicalSIRecording
101110

102111
saved = _make_numpy_recording().save_to_zarr(str(tmp_path / "rec.zarr"))
@@ -120,6 +129,7 @@ def test_ephemeral_recording_round_trip(tmp_path):
120129
SpikeInterface re-applies the preprocessing chain lazily (using zscore
121130
which requires no optional dependencies and is fully deterministic).
122131
"""
132+
pytest.importorskip("spikeinterface", reason="spikeinterface not installed")
123133
import spikeinterface.preprocessing as spre
124134
from orcapod.extension_types.spikeinterface_types import LogicalSIRecording
125135

@@ -143,6 +153,7 @@ def test_ephemeral_recording_round_trip(tmp_path):
143153

144154
def test_si_recording_handler_hash_stability(tmp_path):
145155
"""Same recording produces identical ContentHash across two calls."""
156+
pytest.importorskip("spikeinterface", reason="spikeinterface not installed")
146157
from orcapod.extension_types.spikeinterface_types import SIRecordingHandler
147158
from orcapod.types import ContentHash
148159

@@ -158,6 +169,7 @@ def test_si_recording_handler_hash_stability(tmp_path):
158169

159170
def test_si_recording_handler_hash_changes_with_content(tmp_path):
160171
"""Different recordings produce different ContentHash values."""
172+
si_core = pytest.importorskip("spikeinterface.core", reason="spikeinterface not installed")
161173
from orcapod.extension_types.spikeinterface_types import SIRecordingHandler
162174

163175
rng = np.random.default_rng(0)
@@ -174,6 +186,7 @@ def test_si_recording_handler_hash_changes_with_content(tmp_path):
174186

175187
def test_si_recording_handler_in_memory_raises():
176188
"""SIRecordingHandler raises ValueError for in-memory recordings."""
189+
pytest.importorskip("spikeinterface", reason="spikeinterface not installed")
177190
from orcapod.extension_types.spikeinterface_types import SIRecordingHandler
178191

179192
rec = _make_numpy_recording()
@@ -185,6 +198,7 @@ def test_si_recording_handler_in_memory_raises():
185198
def test_register_spikeinterface_types(tmp_path):
186199
"""register_spikeinterface_types() wires LogicalSIRecording and SIRecordingHandler
187200
into the default context so they are found by type lookup."""
201+
pytest.importorskip("spikeinterface", reason="spikeinterface not installed")
188202
from orcapod.extension_types.spikeinterface_types import register_spikeinterface_types
189203
from orcapod.contexts import get_default_context
190204

0 commit comments

Comments
 (0)