Refactor: tidy library organization (session/, vhi/, slim core.py, persistence dedup)#8
Merged
Merged
Conversation
…slim core, persistence dedup)
…-> ml
Per Codex review: one namespace for swappable first-party recipes.
- myogestic/recipes/{features.py, estimators.py} replaces myogestic.contrib
(features) and the vestigial single-file myogestic.models (estimators).
- Estimator recipes lose save_model/load_model; the single joblib persistence
implementation now lives in myogestic.ml (save_pickle/load_pickle).
- Update all callers (examples + tests + docs); rename test_models -> test_recipes.
- recipes is named for purpose (swap-me starters), not provenance like contrib.
Match the sources/outputs/recipes convention: __init__ is a docstring + re-export shell, implementation lives in a named module. Import paths (from myogestic.bridges import Bridge/WebCamBridge/CustomBridge) unchanged.
- core.py: drop now-unused Path/_assets_folder imports and sort the block (fallout from extracting helpers into _platform.py). - pyproject: remove the stale per-file-ignore for the moved session.py; exclude the generated vhi/_proto stubs from ruff.
- edge_trigger: PEP 695 generic syntax (class EdgeTrigger[T]). - grid: Union[Px, Fr] -> Px | Fr. - stream: move numpy/RingBuffer imports above the browser-detect code (E402). - widgets/__init__, the 6 synthetic examples, test_interfaces: import-sort / unused-import auto-fixes (fallout from the reorg import rewrites). - pyproject: exclude the Pyodide playground demo from ruff (load-bearing mid-file import order; it's a docs asset, not library source). ruff check . is now clean; 163 tests pass; properdocs build clean.
os.popen is soft-deprecated. Replace the macOS dark-mode check with subprocess.run (capture_output, check=False); absent AppleInterfaceStyle key (Light mode) -> empty stdout -> not dark. OSError -> not dark.
The acquire loop appended to self._data/_timestamps (RingBuffer | None) relying on the _connected flag, which the type checker can't tie to non-None. Add an explicit None guard matching get_window/get_display, silencing the warning and adding harmless defence.
Bind each M4 scratch buffer to a local *before* its lazy-alloc guard so the type checker narrows the local (it won't narrow an Optional instance attribute across the loop + downsampler calls); also saves repeated attribute lookups in the per-frame hot path. Mark the hasattr-guarded optional Source.reconnect() call with type: ignore[attr-defined].
…ore) _m4_downsampler was typed 'object | None' with a # type: ignore on .downsample, which strict type-checkers flag as 'object is not callable'. Type it as the real tsdownsample.M4Downsampler via a TYPE_CHECKING import and bind it to a narrowed local; no suppression needed.
… numpy Convert all Google-style docstrings in the myogestic/ package to NumPy (numpydoc) format (Parameters/Returns/Raises/Attributes/Notes/Examples with underlines; param types come from annotations via Griffe) and flip the mkdocstrings handler to docstring_style: numpy. Docs build clean (0 warnings).
…ubpackages
Group the 26 flat widget modules by responsibility:
- signals/ viewer, raw, stream_panel, scan, plot, controls, state, transforms
- plots/ scatter, heatmap, line_plot
- panels/ process_launcher, recording, log_panel, popout, app_logo,
filter_controls, log_box
- training/ feature_selector, session_manager, session_state, template_inspector,
trial_preview, prediction_label
- vhi/ palette, panel
De-underscore the public-in-practice helpers (_common -> common, _log_box ->
panels/log_box, etc.) and extract apply_display_filter into signals/transforms.
Public API unchanged (facade __init__ re-exports the same 26 names); all
in-repo callers + docs autodoc paths updated. No back-compat stubs.
The two-audiences split is already obvious from the module names + their own docstrings; a package-dir README that isn't rendered in the docs site earns nothing.
…_init__ ml/__init__.py held the entire ~300-line Pipeline + PipelineState. Extract them into ml/pipeline.py and reduce __init__ to a facade (docstring + re-export Pipeline, PipelineState, save_pickle, load_pickle). Matches the sources/outputs/ recipes/bridges convention. Public API (from myogestic.ml import Pipeline, ...) unchanged.
…_init__ outputs/__init__.py held the entire ~140-line Output base class. Extract it to outputs/base.py and reduce __init__ to a facade (re-export Output, LSLOutlet, UDPOutput). Concrete outputs now import Output from outputs.base (not the package) to avoid an init-order cycle. Public API unchanged.
Match the codebase convention (session/_core, otb/_base) for modules with no
importers outside their own subpackage:
- signals/{plot,controls,state,scan} -> _plot/_controls/_state/_scan
- training/session_state -> _session_state
Public entry points (viewer/raw/stream_panel, session_manager) and
cross-subpackage-shared helpers (signals/transforms, panels/log_box) keep their
bare names. Internal cross-imports updated.
Two surgical root tidies (Codex-reviewed): - filters.py -> outputs/filters.py: it is output-side post-processing of prediction/control vectors, not a core primitive. outputs/ already owns 'vectors leaving the app'. Re-exported from myogestic.outputs (OneEuroFilter, GaussianFilter, IdentityFilter, VectorFilter, make_filter). Callers + docs autodoc/xrefs repointed to myogestic.outputs.filters. - theme.py -> _theme.py: only core.py imports it, not re-exported, runtime styling plumbing -> private per convention. Root now holds only the public spine (core, stream, contracts, grid, edge_trigger) plus private runtime support (_browser, _platform, _theme).
The Astral 'ty' type checker doesn't recognize mypy/pyright-specific ignore
codes ('# type: ignore[arg-type]', '# pyright: ignore[...]', '[union-attr]'),
so it kept reporting errors that were already suppressed for the other checkers.
A bare '# type: ignore' is honored by mypy, pyright, AND ty, so collapse the
checker-specific comments to it on the affected lines:
- StreamOutlet.push_sample(np.ndarray) — over-strict mne_lsl stub wants its
private ScalarArray subclass (6 examples + outputs/lsl.py)
- portable_file_dialogs result polling — pfd has no stubs, object is untyped
(2 examples + widgets/training/session_manager.py)
Verified ty + pyright + ruff + pytest all clean.
Streams were pinned to float32 (hardcoded in LSLSource), even though the buffers and Zarr recording already honor StreamInfo.dtype. Open it up: - StreamInfo: add SUPPORTED_DTYPES (float16/32/64, int8/16/32/64 — every LSL wire format plus float16 as a storage cast; no unsigned, none on the LSL wire). __post_init__ normalises str/type/np.dtype and validates. Default stays float32. - LSLSource(stream_name, dtype='float32'): cast incoming samples to the chosen dtype; dtype=None honors the outlet's native wire format (lossless for int amps). Fixes the old connect() docstring that claimed dtype came from metadata while hardcoding float32. - The window handed to @pipeline.extract is ALWAYS upcast to float32 — at the live boundary (Stream.get_window) and the training boundary (iter_labeled_ windows / iter_aligned_windows) — so feature/ML code never branches on dtype, while buffers + recording stay compact (e.g. int16 = half memory/disk). Net win: 16-bit EMG amps can stream/record losslessly at half the size. Recording already used info.dtype, so no change needed there. Also: bare '# type: ignore' on the reconnect() call so ty honors it.
…nerator mne_lsl's pull_chunk is typed to return list[list[str]] but yields a numpy array for numeric streams. float(data[-1, 0]) is valid at runtime but trips both pyright and ty (can't 2D-index a list). Wrap in np.asarray() — a no-op on the real ndarray, but it gives the checker a genuine array. Real fix, no suppression.
Sample conversion of the four tools CLIs. Typer is type-hint driven: main()'s typed params become the CLI, so 'args.X: Any' (an opaque hole for ty/pyright) is gone, --help is auto-generated, and there's far less boilerplate. Adds typer as a runtime dep (myogestic-install-vhi is a console script). - argparse block -> typed main(...) signature with Annotated[..., typer.Option] - python -m entry switched to typer.run(main); '# -m' invocation unchanged - renamed the loop's local sample-array 'chunk' -> 'samples' so it no longer collides with the new --chunk parameter (the collision overwrote the param and crashed standard_normal() on the 2nd loop iteration) Pure helpers (_class_pattern/_read_mode) and control_outlet() are untouched, so examples + tests that import them are unaffected.
…am) to Typer Finishes the argparse -> Typer migration across all four tool CLIs. Type-hint driven: each main()'s typed params become the CLI, killing the 'args.X: Any' hole and auto-generating --help. - lsl_dummy: typed main() + typer.run; loop's data var 'chunk' -> 'samples' so it no longer shadows the --chunk param (same collision class as emg_generator) - webcam: required --zarr / --lsl-name become required typer Options; the --zarr param would shadow , so the module is aliased to - install_vhi (console script): logic moved to _install(); main() is now a thin typer.run(_install) wrapper so the existing 'install_vhi:main' entry point and both keep working. -> . - webcam: bare '# type: ignore' on three pre-existing stub/optional-dep issues (cv2 not installed, zarr.open()'s Array|Group union, push_sample stub) Verified: ruff + ty + pyright clean on all four; --help renders; console-script entry resolves; lsl_dummy/emg_generator run; 176 tests.
The signal-synthesis block used bare 0.02 / 0.15 literals (repeated 6x), and 0.15 was doing two conceptually distinct jobs that happened to share a value. Extract named module constants so intent is explicit and they can diverge: - REST_NOISE (0.02) - idle noise floor (rest / no active DoF) - ACTIVE_NOISE (0.15) - background noise while a gesture is active - ENVELOPE_GAIN (0.15)- per-DoF activation-envelope amplitude Behaviour identical (same values). The _class_pattern shape numbers (Gaussian centre/width/2-sigma) and the resolve_streams timeouts are left inline - those are either standard math or self-describing.
…lasses myogestic_vhi_pb2 builds its message classes dynamically at import time, so pyright/ty/Pylance couldn't see members like SetMovementRequest, flagging every pb2.* usage in vhi/_client.py. - generate myogestic_vhi_pb2.pyi (protoc --pyi_out) and commit it; checkers now resolve all the message types with real signatures - gen_proto.py: add --pyi_out so future regens keep the stub in sync - _client.py set_control_mode: the new stub surfaced a real mismatch — ControlMode.Value() returns int but the enum field is typed ControlMode | str. Pass the validated enum *name* string instead (identical wire bytes), keeping .Value() purely for input validation. Verified: pyright + ty clean on _client.py, ruff clean, 176 tests.
EdgeTrigger gates a side effect to fire only when a prediction changes. Per a Codex review: it is not an ml primitive (it knows nothing about models, features, or pipeline state) — it shapes a *downstream effect*, so it belongs with the send-side. outputs/ already owns "things leaving the app": outputs.filters handles continuous vector outputs, EdgeTrigger handles discrete event outputs. - git mv edge_trigger.py -> outputs/edge_trigger.py - re-exported from myogestic.outputs and kept in the top-level facade, so both `from myogestic import EdgeTrigger` and `from myogestic.outputs import EdgeTrigger` work - updated tests + docs to the facade import path Package root now holds only the core spine (core, stream, contracts, grid) plus private runtime support (_browser/_platform/_theme). Verified: ruff + ty + pyright clean, 176 tests, docs 0 warnings.
…>*_cutoff_hz) Naming-standardization sweep (1/4). Per the agreed convention (rates use hz / *_hz; no ambiguous bare names): - OneEuroFilter(freq=) -> hz= (it is the call/sample rate, like every other rate param in the lib) - min_cutoff -> min_cutoff_hz, d_cutoff -> derivative_cutoff_hz (units explicit; derivative_cutoff spelled out, no vowel-dropping) - beta stays (unitless gain) Updated make_filter's one_euro defaults, FilterControl's tuning dict + slider readers, tests, and docs (post-process-output, api-cheatsheet). ruff + ty + pyright clean, 181 tests, docs 0 warnings.
Naming-standardization sweep (2/4). iter_labeled_windows and iter_aligned_windows took win_seconds / hop_seconds (abbreviated, seconds) — out of step with the new ms policy for acquisition/feature durations. Rename to window_ms / hop_ms and convert internally (samples = ms/1000 * fs). - session/_windows.py: both iterators. - examples: unified on WINDOW_MS / HOP_MS constants (dropped the per-example WIN_SECONDS seconds knob + the * 1000 at call sites), matching the RaulNet one. - tests/test_session_pack.py: kwargs, positional args, and match= strings; the window-count assertions confirm 0.2 s -> 200 ms yields identical sample counts. - docs: every iter_* example/signature/prose (also collapsed the win_s/win_seconds variants that had crept into the docs into the single window_ms spelling). ruff + ty + pyright clean, 181 tests, docs 0 warnings.
Naming-standardization sweep (3/4). The viewer's display-zoom window stays in seconds (multi-second zoom reads better as seconds), but the suffix is normalized from the verbose _seconds to _s per the convention (no _seconds in public API). Renamed in widgets/signals/viewer.py + _state.py; no callers passed it explicitly, so nothing else changed. (_state.py's pre-existing pyright/ty diagnostics in the data path are unrelated and untouched.) ruff clean, viz + full suite (181) pass.
Naming-standardization sweep (4/4). InterfaceSpec's output_channels / control_channels / control_pose_channels are counts, so prefix them with n_ to match n_channels / n_classes elsewhere: n_output_channels, n_control_channels, n_control_pose_channels. Updated the dataclass, outlet()/control_outlet() readers, virtual_hand() constructor, and test_interfaces.py. No doc references. ruff + ty + pyright clean, 181 tests, docs 0 warnings.
Naming sweep tier 1 (Codex-reviewed inventory): - Session.init_stream(name) / append(name) -> stream_name (matches the getters get_continuous/get_trials/stream_info already using stream_name). - Session.get_trials(pre, post) -> pre_s / post_s (they are seconds — unit was hidden). - iter_aligned_windows: primary_stream -> primary_stream_name, aligned_streams -> aligned_stream_names, align_window_samples -> n_alignment_samples (names not Stream objects; count gets n_). Callers were mostly positional; updated the kwarg/match/signature/prose sites in examples, tests, and docs. (_core.py:211 zarr.storage pyright error is pre-existing, unrelated.) ruff clean, 181 tests, docs 0 warnings.
Naming sweep tiers 2-4 (Codex-reviewed inventory). Breaking, pre-release.
Tier 2 (widgets):
- uid -> widget_id (template_inspector, trial_preview, session_manager,
process_launcher + internal helpers)
- heading label -> title (prediction_label, session_manager, vhi_movement_palette,
VhiMovementPanel)
- id-role label -> widget_id (process_launcher, FilterControl.ui)
- prediction_label: key -> class_key, proba_key -> probability_key
- trial_preview: window -> as_window (it's a bool "wrap in an ImGui window")
Tier 3:
- GaussianFilter(window) -> n_vectors (count of history vectors)
- EdgeTrigger(stable_ticks) -> n_stable_ticks
- constant_classifier(class_idx) -> class_index
Tier 4:
- InterfaceSpec.{output,control,control_pose}_stream -> *_stream_name (+ the
examples reading vhi.control_stream -> vhi.control_stream_name)
- virtual_hand(mode) -> launch_mode
- VhiMovementPanel(refresh_min_interval_s) -> min_interval_s
- emg_generator/lsl_dummy main(): channels/classes/chunk/control ->
n_channels/n_classes/chunk_size/control_stream_name, with explicit Typer
flags so the CLI (--channels/--classes/--chunk/--control) is unchanged
Also: prediction_label proba index ignore -> bare '# type: ignore' (ty).
Callers (examples/tests/docs) updated; CLI flags preserved. ruff + ty + pyright
clean, 181 tests, docs 0 warnings.
API_PARAMETERS.md + tools/gen_param_inventory.py were a one-off cross-reference aid for the naming-standardization review; they got swept into the tier-1 commit by git add -A. They're not part of the library — remove them.
The VectorFilter.__call__ timestamp arg was a bare one-letter `t` with no docstring anywhere — no way to know it's the per-sample LSL-clock time that OneEuroFilter uses for the real dt. Rename `t` -> `timestamp` (matches the codebase's timestamp/timestamps), and document the __call__ contract (x, timestamp, returns) on the VectorFilter protocol. No caller passed it. ruff + ty + pyright clean, filter tests pass.
The OneEuroFilter t->timestamp rename missed two t= kwargs in test_one_euro_with_explicit_timestamps. Fixed; 16 filter tests pass.
Make docstring coverage/format a checked standard (decided with Codex), so the next cryptic-`t`-style gap is caught in CI rather than by eye. - ruff: add `D` to select, `[tool.ruff.lint.pydocstyle] convention = "numpy"`, ignore D105 (magic methods), D107 (ctor docs live on the class — mkdocstrings merge_init_into_class), D401 (descriptive API summaries). tests/ + examples/ are D-exempt. - Filled every resulting gap (~61): undocumented public methods/functions, module + package docstrings, and D205 summary-blank-line formatting. Moved Pipeline's __init__ Parameters onto the class docstring (house style: constructor docs on the class; dataclasses use Attributes). - Documented the filter __call__ contract, the source read/connect/disconnect methods, the ml widgets, and the pipeline transition methods. ruff + ty + pyright clean, 181 tests, docs 0 warnings.
Last cryptic-arg cleanup: 'on' was a bare 2-letter bool, not an established idiom. Renamed the param to 'active' (the method is set_active). No external callers. A full <=3-char public-param audit found everything else is an accepted idiom (fn/fs/hz/ctx/app/emg/key/x), so this completes it.
The t->timestamp filter-param rename updated filters.py + tests but missed 7 hand-written doc examples still calling `pose_filter(pose, t=...)`, `add_label(class_idx, t=local_clock())`, and the api-cheatsheet `filter(x, t=None)` signature (add_label and the filters both take `timestamp` now). Fixed. A full re-scan of docs for every renamed param (window_ms, *_hz cutoffs, n_stable_ticks, n_vectors, stream_name, *_stream_name, n_alignment_samples, class_key/probability_key, launch_mode, min_interval_s, etc.) found no other stale references — the autodoc pages render live signatures, and the prose was updated during each sweep. docs build 0 warnings.
- FilterControl.__call__: t -> timestamp (the filter-rename missed this wrapper; FilterControl()(x, timestamp=...) used to TypeError). Added a regression test. - type-ignore standard refined and applied: bare `# type: ignore` only where ty itself errors (the `import serial` unresolved-import + a couple attr accesses); keep the specific code for pyright/mypy-only suppressions (implot.Cond_, Popen type-arg) — bare there triggers ty's unused-blanket warning. Net: fewer ty diagnostics than before. - packaging: package-data proto path _proto/*.proto -> vhi/_proto/*.proto (moved). - add_label(): added docstring (public, rendered in docs); internal t -> ts. - internal-name consistency: VhiMovementPanel._refresh_interval -> _min_interval_s, _ConstantClassifier.class_idx -> class_index, OneEuroFilter._t_prev -> _timestamp_prev, add_recorded_session(label) -> title (matches session_manager). - signal_viewer docstring buffer_seconds -> buffer_ms. - sklearn _require hint: --extra dev -> examples (consistent with catboost). ruff clean; 182 tests; docs 0 warnings. (Remaining ty data-path diagnostics in _state.py/_plot.py/stream_panel are pre-existing, pyright/ty not CI-gated.)
- contrib -> recipes: README, docs/api/index, properdocs nav; renamed use-contrib-features.md -> use-recipe-features.md (the module became myogestic.recipes.features long ago). - Stale filter `t=` examples/prose fixed (enable-recording, post-process-output, widget-gallery, troubleshooting) after t->timestamp. - Moved-file references updated to the post-reorg signals/ layout (concepts/widgets, add-a-widget, design-principles); process_launcher docstring import fixed. - api-cheatsheet: corrected stale signatures (template_inspector/trial_preview widget_id, plot data/channel_names) and softened the "every public symbol" claim. - CHANGELOG [Unreleased]: full breaking-change migration note (module moves + old->new parameter map) for the reorg + naming standardization. docs build 0 warnings.
These are doc code blocks that compile as prose but would fail at runtime — structural API misuse the param-name greps couldn't catch: - record-and-replay.md / add-a-model.md: iter_aligned_windows used old primary=/aligned= kwargs, 2-value unpack, and sw.data. It takes primary_stream_name/aligned_stream_names and yields (window, aligned, ts). - record-and-replay.md: get_trials(pre=/post=) -> pre_s/post_s; r.ts -> r.timestamps; corrected the Recording.data layout claim (it is sample-major (n_samples, n_channels), not channels-first). - anatomy.md: iter_labeled_windows yields the window ndarray directly, not an object with .data (sw.data -> window). - api-cheatsheet: SerialSource/SerialOutput are direct-import only (not on the facades) — noted the import path. Nice-to-haves: dropped "every public symbol" claim in README + reference/index; persistence is under ml not recipes (api/index, architecture); uid -> widget_id wording (troubleshooting, template_inspector docstring); WIN_SECONDS/HOP_SECONDS -> WINDOW_MS/HOP_MS (record-good-training-data, troubleshooting). Codex confirmed examples/synthetic/*.py call the current API correctly and the CHANGELOG migration map matches real signatures. docs build 0 warnings.
Doc code blocks rotted silently (the FilterControl/iter_*_windows examples) because nothing ran them and CI was docs-only. Close that gap: - tests/test_docs.py: (1) parse-checks every ```python block in docs/ + README (catches syntax/indentation); (2) executes blocks tagged `<!--docs:run-->` against a synthetic session in a per-file namespace — a stand-in pipeline whose @train decorator invokes immediately, so a tagged @pipeline.train block really drives iter_*_windows. This catches wrong-kwarg / wrong-attribute / wrong-unpack bugs a parse check can't. `<!--docs:skip-->` drops a block from both layers. - Tagged the data-API examples that previously broke (record-and-replay's iter_labeled/iter_aligned, add-a-model's iter_aligned). Verified the run-layer catches a reintroduced `window.data` regression. - Fixed anatomy.md's illustrative `recording_controls, ...` import to a real, parseable import (no behavior/intent change). - .github/workflows/tests.yml: run ruff (incl. pydocstyle D) + the full pytest suite on push/PR. Lean extras (dev/grpc/serial); sklearn/catboost recipe assertions importorskip without pulling torch. Local: `uv run pytest tests/test_docs.py` (`-k run` for just the executed blocks). 314 tests pass; docs build 0 warnings.
Codex's "examples as source of truth" review (option B of B+D+C): run each examples/synthetic/*.py via runpy with App.run monkeypatched to a no-op, so all module-level + __main__ wiring (App/Stream/Pipeline construction, decorator registration, widget hookup, virtual_hand/VhiControlClient config) executes against the current public API without opening a window. Catches renamed kwargs / moved imports / wrong widget-factory signatures in the real example files — stronger than the AST-only pass. Examples that top-import an absent optional dep (torch/myoverse: the RaulNet / MyoVerse examples) skip cleanly, so this runs lean in CI (the 2 dep-free examples) and fully wherever --extra examples is installed. All 6 pass locally.
The example smoke test failed in CI: examples call vhi.launcher() at module level, which raises FileNotFoundError unless the VHI binary is installed (an environment dependency, not API surface). Stub InterfaceSpec.launcher -> [] in the test; a renamed/removed method would still AttributeError and fail. Locally (VHI installed) all 6 pass; lean CI now runs the 2 dep-free examples and skips the 4 torch/myoverse ones.
Codex's "examples as source of truth" (option D). The tutorial had structurally drifted — it showed a torch/MyoVerse `rms_transform`/`mav_transform` extract, but the real example uses a FeatureSelector over myogestic.recipes.features. Replace the hand-copied blocks (filter/features/setup/extract/train/predict) with pymdownx `--8<--` snippet includes from sectioned markers in examples/synthetic/emg_classification.py, and fix the now-stale prose/intro. The tutorial code now IS the runnable, smoke-tested example code and can't drift. - example: added `# --8<-- [start:NAME]/[end:NAME]` markers (comments, no behaviour change; smoke test + ruff still pass). - tests/test_docs.py: skip `--8<--` include blocks (not literal python; the source file is covered by tests/test_examples.py). - docs build 0 warnings; include verified rendered in site/.
Apply the source-of-truth pattern to the regression tutorial's one clean full-function block: marker in emg_regression.py + `--8<--` include. Its iter_aligned_windows / iter_labeled_windows fragments are deliberately split for teaching and already match the current API (positional args), so they stay inline (parse-checked by tests/test_docs.py).
…ocks - feature-extraction-cookbook: section 1 claimed to be "the emg_classification.py baseline" but showed the OLD torch/MyoVerse extract — same drift the tutorial had. Reframed: the shipped baseline now includes the real FeatureSelector + extract from the example; the MyoVerse version stays as a clearly-labeled "bring your own" alternative. - classification tutorial: vhi/HAND_REST/HAND_FIST poses block -> :poses include. - regression tutorial: VHI_DOF_INDICES -> :dofs, grid layout -> :grid includes. That covers every doc block that's a verbatim copy of a contiguous example section. Left inline (by design): aggregated import+usage snippets, the regression train's two deliberately-split teaching fragments (the real train has extra session-splitting/logging that would bloat the lesson), and the generic illustrative fragments on how-to/concept pages (no single source example) — all still parse-checked by tests/test_docs.py. docs build 0 warnings; 310 tests pass.
…ex check) The snippet includes surfaced stale prose the conversion didn't touch: - classification tutorial: removed the non-existent "Launch VHI" button copy (the example's PROCESSES only launches the EMG generator; VHI runs separately), corrected the left-column panel list, and replaced the stale Grid(6,3) / VHI_PROCESS layout block with a `:layout` include of the real Grid(8,3) + demo_ui. Dropped the brittle exact line count. - regression tutorial: fixed "both iterators ignore class chips" — only the labeled fallback filters by class; the kinematics path regresses every window. - cookbook + tests/test_docs.py comment: softened now-inaccurate wording. All 13 includes resolve (check_paths would fail the build otherwise); docs build 0 warnings, ruff clean, example smoke + full suite (309) pass.
…lot check) Copilot's read-only review found no must-fix issues but flagged two remaining hand-copied fragments as drift candidates — and one had already drifted (the labeled-loop dropped the example's dtype= args). Close them: - Added nested section markers in emg_regression.py: :kin_loop, :label_loop (inside train), :expand (inside predict). - Enabled `dedent_subsections: true` in the pymdownx.snippets config so these inside-function fragments render at column 0 instead of the function's indent. - Converted the three regression-tutorial fragments to those includes. The nested :expand markers are stripped from the surrounding :predict include (verified 0 stray --8<-- in the rendered page). Now every verbatim code copy in the two tutorials + the cookbook is a snippet include from the smoke-tested examples. Both Codex and Copilot reviews come back clean. docs build 0 warnings; ruff clean; example smoke + full suite (306) pass.
…ange) ty went from 20 errors -> 0; pyright from 29 -> 0 (6 benign stub warnings). Real fixes (narrowing / annotations / casts / imports), no runtime change: - popout._make_dockable_window -> typed DockableWindow (fixes core.py dockable list assignment at the source). - session/_core.py + _io.py: `import zarr.storage` so zarr.storage.ZipStore resolves; declared Session._zip_store (a write-only ZipStore lifetime anchor) in __init__ with a comment. - _state.py: ViewerState.frozen_ts/frozen_data typed np.ndarray|None (were object|None); paused-render guard also checks frozen_ts (set together with frozen_data, so behavior-equivalent); assert info before .fs. - _plot.py: assert stream.info before .fs (caller already returns on None). - raw.py: cast the heterogeneous buffer dict's known keys. - stream_panel._connect_buttons: param typed Stream (only called post-isinstance). - _session_state.py: cast label dict for .get; log_box: bool(still_open). Genuine third-party-stub gaps -> bare `# type: ignore`: AppKit/pyobjc members (_platform.py), and the optional pyserial / dvg-ringbuffer lines (config rule unused-type-ignore-comment="ignore" since "needed" depends on installed extras). Generated proto excluded from ty + pyright (mirrors the ruff exclude). ty + pyright + ruff clean, 306 tests, docs 0 warnings, wheel still imports.
Reported: saving (pack to .session.zip) fails on Windows. The finalize path assumed POSIX semantics — you can delete/rename files with open handles, and GC releases zarr chunk handles in time. Windows refuses both (WinError 32), so the rmtree/rename after writing the zip throws and the save appears to fail. - pack_to_zip: gc.collect() after dropping zarr array refs to release chunk-file handles; shutil.rmtree -> _robust_rmtree (retry + clear read-only bit, the standard Windows pattern); Path.rename -> os.replace (atomic overwrite; Windows rename raises if the destination exists). - Session.close() + context-manager support to close the ZipStore opened by open_session_store (an open ZipStore locks the .session.zip on Windows); open_session_store now always sets _zip_store (None for folders) so close() is safe despite the __new__ bypass. - Tests: os.replace overwrite, close() idempotency + context manager, folder session close-safety. CI: add a windows-latest job to the Tests matrix (fail-fast: false) so this path is actually exercised on Windows. Lint runs once on Linux. ruff + ty clean, 309 tests on macOS/Linux.
The new windows-latest CI job caught two real things: - iter_labeled_windows / iter_aligned_windows opened a session per path via open_session_store but never closed it — leaking the ZipStore handle, which on Windows locks the .session.zip (can't delete/move it after training). Wrapped each per-path body in try/finally: sess.close(). - test_pack_to_zip_roundtrip leaked a ZipStore handle (open_session_store without close) → tempdir cleanup hit WinError 32; now context-managed. - test_pack_to_zip_failure_keeps_folder used chmod(0o555) failure injection, which is POSIX-only (Windows ignores the read-only bit on dirs) → skipif win32. ruff + ty clean, 309 tests on macOS/Linux.
…leaks Leak audit (all open_session_store callers): - ReplaySource opened a session in connect() but disconnect() only reset _pos, never closing it — a replayed .session.zip stayed locked until GC (Windows can't delete/re-record it). Now holds the session and closes it on disconnect (and on the stream-missing error path). Added a .session.zip disconnect test. - examples/synthetic/emg_regression.py + emg_regression_raulnet.py opened a session just to check `"vhi_control" in sess.stores` then dropped it, leaking a ZipStore per session during training — now close right after the check. - webcam.py's zarr store runs in the bridge *subprocess*, reclaimed by the OS on exit — not a main-process lock, left as-is. (iter_*_windows + pack_to_zip + open_session_store were fixed in the prior commits.) ruff + ty clean, 310 tests on macOS/Linux.
RaulSimpetru
added a commit
that referenced
this pull request
Jun 7, 2026
Merged main (PR #8: library reorg, naming standardization, ruff D / ty-clean, Windows session fix, doc/example test harness) into the OTB device-sources branch and brought the PR up to date: - API: window_seconds= -> window_ms= in the OTB example + connect-otb-devices.md (StreamInfo/Source usage was unchanged, so the sources needed no edits). - ruff (new D + rules now apply to OTB code): package docstring for sources/otb, D400 period fix in _constants, disconnect() docstring in muovi; gave tests the same E402 import-placement latitude examples already have; dropped an unused import. - ty (repo is ty-clean now): narrowed the socket/StreamInfo Optionals in _base/muovi/quattrocento (local-bind in read(); asserts before use) — no behaviour change. - test_docs harness: handle code blocks nested in list items (indented closing fence + dedent) so connect-otb-devices.md parses. ruff + ty clean, 337 tests, docs 0 warnings. Version stays 2.0.2.
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.
Reorganizes the package for clearer boundaries. Behavior-preserving (moves, re-exports, docs only) — verified by the full suite staying green (163 passed) after every task. Backwards compatibility intentionally not preserved (no shims); all in-repo callers updated.
Changes (one commit per item)
session/subpackage —_session_core/_session_io/_session_windows+ the facade grouped intomyogestic/session/(_core.py/_io.py/_windows.py+__init__.py). Publicmyogestic.session.*unchanged.core.py(610→528) — asset/icon/macOS-dock helpers extracted tomyogestic/_platform.py;App/Context/AppStatestay put.myogestic.models);myogestic.ml.save_pickle/load_picklenow thin wrappers (keep the parent-dir-creation convenience) over it. Cross-reference docstrings added soml(lifecycle) vsmodels(recipes) is clear.tools/docs —myogestic/tools/README.mddocuments the two audiences (end-userinstall_vhivs dev/test signal generators). No file moves.vhi/subpackage (breaking) —interfaces.py,_vhi_client.py(→_client.py), and_proto/grouped intomyogestic/vhi/.myogestic.interfacesis removed; all callers (6 examples +test_interfaces+install_vhi+ widgets) now usemyogestic.vhi/myogestic.vhi.interfaces.gen_proto.pyoutput path updated. Fixed_default_install_root's parent-chain for the deeper location (the modernized test caught it).Result
_session_*,_vhi_client,interfacesat top level).myogestic/session/,myogestic/vhi/.git diff --stat: 32 files changed, +500/-128.Notes
main(includes PR Fix stale VHI interface + public-API tests (pre-existing, since v2.0.0) #7). Independent of PR Native OTB device sources (Muovi/Muovi+ & Quattrocento) #6 (OTB) — different files, no conflict.myogestic.interfaces(→myogestic.vhi.interfaces). Therefactor!commit marks it.Plan:
docs/superpowers/plans/2026-06-04-library-reorganization.md.