Skip to content

Build the iterators on ezmsg-baseproc's producer classes - #6

Merged
cboulay merged 1 commit into
devfrom
cboulay/baseproc-refactor
Sep 4, 2026
Merged

cboulay merged 1 commit into
devfrom
cboulay/baseproc-refactor

Conversation

@cboulay

@cboulay cboulay commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

Stacked on #5 — review/merge that first; the base here is cboulay/chunk-dim-and-fingerprint.

This was the only ezmsg source package still on ez.Unit + GenState:

repo producer base classes
ezmsg-blackrock BaseProducer, BaseStatefulProducer, BaseProducerUnit
ezmsg-lsl BaseStatefulProducer, BaseProducerUnit
ezmsg-neo BaseStatefulProducer, BaseProducerUnit
ezmsg-nwb BaseProducer, BaseStatefulProducer, BaseProducerUnit
ezmsg-xdf none

Four things follow from joining them.

The file open no longer blocks the event loop

pyxdf.load_xdf reads and decodes the entire file, and it ran inside XDFIterator.__init__ → _scan_file(), called from a synchronous initialize(). Graph startup stalled every other unit in the process for however long that took. It is now a state reset, and _areset_state puts it on a worker thread.

This deliberately diverges from the precedent. ezmsg-neo and ezmsg-nwb both call _reset_state() eagerly from __init__, so they still pay the first open on the loop — nwb's own test says so:

# Discard the eager sync invocation from __init__.
seen_tids.clear()

so its _areset_state only covers reopens after a settings change. Nothing in this package's public surface reads stream metadata before the first chunk, so there's nothing to lose by waiting. Two tests pin both halves: construction touches no file, and the load lands off the loop.

Settings can change at runtime

BaseProducerUnit brings INPUT_SETTINGS and delegates to update_settings. Previously settings were read once in initialize and a running unit could not be retargeted at a different file, chunk duration or playback rate.

playback_rate and self_terminating are listed in NONRESET_SETTINGS_FIELDS: they're the unit's pacing, not the reader's, so changing either must not throw away a loaded file. There's a test for that, and one for the converse.

The duplicated unit bodies collapse

XDFIteratorUnit and XDFMultiIteratorUnit had near-identical initialize / construct_generator / publisher bodies. What's left is _XDFUnitBase owning the playback clock and end-of-file handling, plus a four-line publisher each.

One trap worth knowing about: both publishers must be named produce. ezmsg collects publishers per attribute, so keeping the old pub_chunk/pub_multi names left BaseProducerUnit.produce running alongside them — two publishers draining one producer, neither ever stopping. That hung the new graph tests until the names matched.

Templates are built in one place

_build_template and _with_time replace the near-copy in each iterator, so the chunk_dim declaration and the fingerprint priming from #5 exist once rather than twice.

Compatibility

The keyword API is preserved — BaseProducer.__init__ forwards **kwargs to the settings type, so XDFAxisArrayIterator(filepath=..., select=...) still works, alongside settings=. XDFMultiIteratorUnitSettings is kept as an alias of the settings type that moved to iter.py.

XDFIterator itself is unchanged apart from a public exhausted property and print becoming ez.logger.info — a library writing to stdout is a defect regardless.

Testing

27 passed, up from the 20 that #5 added. The seven new ones cover the producer contract (lazy open, off-loop load, both construction forms, which settings reopen the file) and both units end to end in a real ez.run graph — the graph tests are what caught the publisher-name collision.

The async test is driven with asyncio.run rather than written async def: this repo's pytest config names asyncio_mode but pytest-asyncio isn't installed, so an async def test is silently never awaited and passes without running.

Notes

  • New dependency: ezmsg-baseproc>=1.11.0.
  • src/ezmsg/xdf/source.py still fails ruff on trailing whitespace. Pre-existing and untouched.

🤖 Generated with Claude Code


On the missing checks: this PR reports none because its base is a feature branch rather than main/dev, and the workflow's pull_request trigger only matches those. It'll pick up the full matrix once #5 merges and this is retargeted at dev. Locally: 27 passed, ruff clean.

@cboulay
cboulay force-pushed the cboulay/baseproc-refactor branch 2 times, most recently from 2352691 to 027c003 Compare September 3, 2026 23:01
This was the only ezmsg source package still on `ez.Unit` + `GenState`;
blackrock, lsl, neo and nwb all use `BaseStatefulProducer` and
`BaseProducerUnit`. Four things follow from joining them.

**The file open no longer blocks the event loop.** `pyxdf.load_xdf` reads and
decodes the entire file, and it ran inside a synchronous `initialize()`, so
graph startup stalled every other unit in the process for as long as that took.
It is now a state reset, and `_areset_state` puts it on a worker thread.

This deliberately diverges from ezmsg-neo and ezmsg-nwb, which call
`_reset_state()` eagerly from `__init__` and therefore still pay the first open
on the loop -- nwb's own test says as much ("Discard the eager sync invocation
from __init__"), so its `_areset_state` only covers reopens after a settings
change. Nothing in this package's public surface reads stream metadata before
the first chunk, so there is nothing to lose by waiting, and a test asserts
both halves: construction touches no file, and the load lands off the loop.

**Settings can change at runtime.** `BaseProducerUnit` brings `INPUT_SETTINGS`
and delegates to `update_settings`; previously settings were read once in
`initialize` and a running unit could not be retargeted. `playback_rate` and
`self_terminating` are listed in `NONRESET_SETTINGS_FIELDS` because they belong
to the unit's pacing rather than to the reader, so changing them must not throw
away a loaded file.

**The two near-identical unit bodies collapse** into `_XDFUnitBase`, which owns
the playback clock and end-of-file handling. Both publishers are named
`produce`, which matters: ezmsg collects publishers per attribute, so keeping
the old `pub_chunk`/`pub_multi` names left `BaseProducerUnit.produce` running
alongside them -- two publishers draining one producer, neither stopping. That
hung the new graph tests until the names matched.

**Templates are built in one place.** `_build_template` and `_with_time`
replace the copy in each iterator, so the chunk_dim declaration and the
fingerprint priming exist once rather than twice.

`XDFIterator` itself is unchanged except for a public `exhausted` property and
`print` becoming `ez.logger.info` -- a library writing to stdout is a defect
regardless.

The keyword API is preserved: `BaseProducer.__init__` forwards `**kwargs` to
the settings type, so `XDFAxisArrayIterator(filepath=..., select=...)` still
works, alongside `settings=`. `XDFMultiIteratorUnitSettings` is kept as an
alias of the settings type that moved to `iter.py`.

27 tests pass, up from 20. The seven new ones cover the producer contract
(lazy open, off-loop load, both construction forms, which settings reopen the
file) and both units end to end in a real `ez.run` graph -- which is what
caught the publisher-name collision.

The async test is driven with `asyncio.run` rather than written `async def`:
the pytest config names `asyncio_mode` but pytest-asyncio is not installed, so
an async test would be silently never awaited and would pass without running.
@cboulay
cboulay force-pushed the cboulay/baseproc-refactor branch from 027c003 to 4a1207b Compare September 3, 2026 23:03
Base automatically changed from cboulay/chunk-dim-and-fingerprint to dev September 4, 2026 06:01
@cboulay
cboulay merged commit b1000e8 into dev Sep 4, 2026
@cboulay
cboulay deleted the cboulay/baseproc-refactor branch September 4, 2026 06:05
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.

1 participant