Build the iterators on ezmsg-baseproc's producer classes - #6
Merged
Merged
Conversation
cboulay
force-pushed
the
cboulay/baseproc-refactor
branch
2 times, most recently
from
September 3, 2026 23:01
2352691 to
027c003
Compare
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
force-pushed
the
cboulay/baseproc-refactor
branch
from
September 3, 2026 23:03
027c003 to
4a1207b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:BaseProducer,BaseStatefulProducer,BaseProducerUnitBaseStatefulProducer,BaseProducerUnitBaseStatefulProducer,BaseProducerUnitBaseProducer,BaseStatefulProducer,BaseProducerUnitFour things follow from joining them.
The file open no longer blocks the event loop
pyxdf.load_xdfreads and decodes the entire file, and it ran insideXDFIterator.__init__→_scan_file(), called from a synchronousinitialize(). Graph startup stalled every other unit in the process for however long that took. It is now a state reset, and_areset_stateputs 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:so its
_areset_stateonly 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
BaseProducerUnitbringsINPUT_SETTINGSand delegates toupdate_settings. Previously settings were read once ininitializeand a running unit could not be retargeted at a different file, chunk duration or playback rate.playback_rateandself_terminatingare listed inNONRESET_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
XDFIteratorUnitandXDFMultiIteratorUnithad near-identicalinitialize/construct_generator/ publisher bodies. What's left is_XDFUnitBaseowning 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 oldpub_chunk/pub_multinames leftBaseProducerUnit.producerunning 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_templateand_with_timereplace the near-copy in each iterator, so thechunk_dimdeclaration and the fingerprint priming from #5 exist once rather than twice.Compatibility
The keyword API is preserved —
BaseProducer.__init__forwards**kwargsto the settings type, soXDFAxisArrayIterator(filepath=..., select=...)still works, alongsidesettings=.XDFMultiIteratorUnitSettingsis kept as an alias of the settings type that moved toiter.py.XDFIteratoritself is unchanged apart from a publicexhaustedproperty andprintbecomingez.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.rungraph — the graph tests are what caught the publisher-name collision.The async test is driven with
asyncio.runrather than writtenasync def: this repo's pytest config namesasyncio_modebut pytest-asyncio isn't installed, so anasync deftest is silently never awaited and passes without running.Notes
ezmsg-baseproc>=1.11.0.src/ezmsg/xdf/source.pystill failsruffon 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'spull_requesttrigger only matches those. It'll pick up the full matrix once #5 merges and this is retargeted atdev. Locally: 27 passed, ruff clean.