feat(extension_types): add LogicalSIRecording for SpikeInterface BaseRecording support (ITL-459)#203
Conversation
…message (ITL-459)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gister_spikeinterface_types (ITL-459)
…oogle-style single-backtick (ITL-459) Convert all double-backtick code references to single-backtick notation to align with Google-style docstring standards. Remove ReST double-colon literal-block syntax from module docstring and simplify to plain prose with inline code formatting. Move ITL-467 phase 2 note from class docstring to inline TODO comment on the relevant line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cstring (ITL-459)
…ral recordings (ITL-459) Also fix python_to_storage to use SIJsonEncoder (required for preprocessing chains that store numpy arrays in to_dict output) and fix storage_to_python to use si_core.load instead of the removed load_extractor API.
… load() API (ITL-459)
…ace_types (ITL-459)
…I available (ITL-459) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…x stale load_extractor docstring (ITL-459)
There was a problem hiding this comment.
Pull request overview
Adds optional SpikeInterface integration so spikeinterface.core.BaseRecording can be stored as an Arrow extension type (JSON in large_string) and semantically hashed (SHA-256 of the same JSON), with explicit opt-in registration via register_spikeinterface_types().
Changes:
- Add
LogicalSIRecording,SIRecordingHandler, andregister_spikeinterface_types()for SpikeInterfaceBaseRecordingsupport. - Add a new optional dependency extra:
orcapod[spikeinterface]and update lockfile accordingly. - Add tests and update context changelog/spec docs for the new capability.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/orcapod/extension_types/spikeinterface_types.py |
Implements Arrow/Polars logical type, JSON serialization, SHA-256 hashing handler, and explicit registration helper. |
src/orcapod/extension_types/__init__.py |
Conditionally exports SpikeInterface types when SpikeInterface is installed. |
pyproject.toml |
Adds spikeinterface optional-dependencies extra and includes it in all. |
uv.lock |
Locks SpikeInterface and transitive dependencies; adds spikeinterface extra metadata. |
tests/test_extension_types/test_spikeinterface_types.py |
New tests for import guard, in-memory error, round-trips, hash stability/change, and context registration. |
src/orcapod/contexts/data/v0.1.json |
Adds a changelog entry documenting the new SpikeInterface logical type/handler (explicit registration). |
superpowers/specs/2026-07-01-spikeinterface-baserecording-design.md |
Design spec for the feature (needs alignment with implementation details). |
superpowers/plans/2026-07-01-itl-459-spikeinterface-baserecording.md |
Implementation plan documentation for ITL-459. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if saved_si_types is not None: | ||
| sys.modules[si_types_key] = saved_si_types | ||
| else: | ||
| sys.modules.pop(si_types_key, None) | ||
| importlib.import_module(si_types_key) | ||
|
|
There was a problem hiding this comment.
Fixed. The block now wraps the re-import in try/except ImportError: pass so cleanup does not raise when SI is genuinely absent from the environment.
| # All tests below require spikeinterface — skip if not installed | ||
| si = pytest.importorskip("spikeinterface", reason="spikeinterface not installed") | ||
| import spikeinterface.core as si_core # noqa: E402 | ||
|
|
There was a problem hiding this comment.
Fixed. Removed the module-level pytest.importorskip("spikeinterface") and import spikeinterface.core as si_core. Each SI-dependent test now calls pytest.importorskip as its first statement, so only those tests are individually skipped when SI is absent. test_spikeinterface_not_installed_raises_import_error now correctly runs (and passes) in an environment without SpikeInterface installed. _make_numpy_recording() imports spikeinterface.core lazily inside the function body.
| SI imports use `LazyModule` (same pattern as polars/pyarrow in the codebase) so that | ||
| `import orcapod` does not fail when SpikeInterface is not installed. |
There was a problem hiding this comment.
Fixed. The spec now accurately describes the implementation: spikeinterface_types.py uses a module-level try/except ImportError that re-raises with a pip-install message when SI is absent, and extension_types/__init__.py wraps the import in its own try/except so that import orcapod never fails.
| `LogicalSIRecording` is registered in `contexts/data/v0.1.json` alongside | ||
| `LogicalFile`, `LogicalDirectory`, and `LogicalNumpyArray`. The `SIRecordingHandler` | ||
| is registered in the `python_type_handler_registry` in the same context config. |
There was a problem hiding this comment.
Fixed. The Registration section now correctly describes the explicit opt-in approach: SI types are not wired into contexts/data/v0.1.json (which would break startup without SI installed); instead users call register_spikeinterface_types() once at startup.
| - `python_to_storage()` serializes via `recording.to_dict(recursive=True)` → JSON string. | ||
| Raises a clear `ValueError` for in-memory recordings (`check_serializability("json") == False`). | ||
| - `storage_to_python()` reconstructs via `spikeinterface.core.load_extractor(dict)`. | ||
| - A `SIRecordingHandler` registered in the semantic hasher that hashes the JSON string |
There was a problem hiding this comment.
Fixed. Updated all three occurrences: Goals & Success Criteria, the storage_to_python component description, and the Error Handling table — all now correctly reference spikeinterface.core.load() rather than the removed load_extractor API.
- 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
Review round 1 — all comments addressedSingle commit 4a4679e covers all 5 comments: Test fixes (2 comments)
Module-level Verified: without SI installed, Spec fixes (3 comments)
|
… in v0.1.json Add `_optional: true` support to `parse_objectspec` so types backed by optional extras groups (e.g. spikeinterface) can be listed in v0.1.json without breaking startup for users who haven't installed them. When `_optional` is set, ImportError on module import returns None instead of raising; both LogicalTypeRegistry and PythonTypeHandlerRegistry silently skip None entries. Wires LogicalSIRecording and SIRecordingHandler into v0.1.json with _optional: true so they auto-register when spikeinterface is installed. register_spikeinterface_types() is kept for custom contexts and made idempotent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| self._python_class_factories: dict[type, LogicalTypeFactoryProtocol] = {} | ||
| for lt in (logical_types or []): | ||
| self.register_logical_type(lt) | ||
| if lt is not None: |
There was a problem hiding this comment.
why is this not None check suddenly necessary?
There was a problem hiding this comment.
You're right to flag it — the not None check was a code smell. A registry shouldn't need to know about parse_objectspec's sentinel.
Removed. Filtering is now done at the source:
-
parse_objectspeclist processing — when an item in a list is adictwith"_optional": trueand its module fails to import, the result isNoneand is dropped from the list before the caller ever sees it. SoLogicalTypeRegistry.__init__receives a clean list with noNoneentries, and the loop is back to the plainfor lt in (logical_types or []):form. -
Handler pairs — for the SpikeInterface handler pair
[BaseRecording, SIRecordingHandler], each element carries"_optional": true. When SI is absent,parse_objectspecfilters both from the inner list, leaving[].PythonTypeHandlerRegistry.__init__now guards withentry and len(entry) == 2 and all(x is not None for x in entry)— theentrytruthiness check (falsy for[]) prevents the vacuous-truthall()problem that would have caused an unpack error.
Tests for both paths are in tests/test_utils/test_object_spec.py (added in commit 434184ac).
…recording-objects
…ltering to parse_objectspec Fix two missing commas left by the merge conflict resolution in v0.1.json (SI handler entry lacked comma before numpy.ndarray; SI changelog entry lacked comma before pandas entry). Also address review feedback on the `not None` check in LogicalTypeRegistry: move the filtering of optional-missing entries to parse_objectspec (where None is produced) so registries receive clean lists. parse_objectspec now drops None results from "_optional": true dict items when processing lists. For handler pairs (inner 2-element lists), optional filtering makes the inner list empty []; PythonTypeHandlerRegistry guards with `entry and len(entry) == 2` to correctly skip both empty and incomplete pairs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Adds spikeinterface>=0.101 to [dependency-groups] dev so that `uv sync` installs it for all developers and spikeinterface tests run as part of the standard pytest suite rather than being skipped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hips license outside standard metadata quantities (transitive dep of spikeinterface via neo) has a BSD-3-Clause license but stores it in licenses/doc/user/license.rst rather than a standard License: wheel metadata field, so pip-licenses reports UNKNOWN. Verified from source; exclude it from the allow-only check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Review round 2 — eywalker's comment addressedSingle commit Why the
|
Summary
LogicalSIRecording— aBaseLogicalTypethat storesspikeinterface.core.BaseRecordingas Arrowlarge_stringusing SI's ownto_dict()JSON dump (viaSIJsonEncoder)SIRecordingHandler— hashes the same JSON bytes via SHA-256 for content identity, keeping hash/storage representation consistentregister_spikeinterface_types(context=None)— explicit opt-in registration into the default context; not wired intov0.1.jsonto avoid import failure when SI is absentNumpyRecordingraisesValueErrorwith save instructionspip install orcapod[spikeinterface]Test plan
uv run --with spikeinterface pytest tests/test_extension_types/test_spikeinterface_types.py -v→ 10 passeduv run pytest tests/ -q→ 3918 passed, 57 skipped, 6 xfailed, 0 failedFixes ITL-459
🤖 Generated with Claude Code