From 28c09fa4f35915e19701e4b3a8cc213e7e1eb3c8 Mon Sep 17 00:00:00 2001 From: Derrick UCD Date: Sat, 2 May 2026 20:41:17 +0100 Subject: [PATCH] Apply select params to spool inputs --- src/derzug/models/selection.py | 13 +++++++++ src/derzug/utils/misc.py | 11 ++++---- src/derzug/widgets/select.py | 8 ++++-- src/derzug/widgets/spool.py | 10 +++---- tests/test_integrations.py | 20 ++++++-------- tests/test_utils/test_misc.py | 34 +++++++++++++++++++++-- tests/test_widgets/test_select.py | 17 ++++++++++++ tests/test_widgets/test_spool.py | 46 ++++++++++++++++++++----------- 8 files changed, 115 insertions(+), 44 deletions(-) diff --git a/src/derzug/models/selection.py b/src/derzug/models/selection.py index 31eead5..af895f4 100644 --- a/src/derzug/models/selection.py +++ b/src/derzug/models/selection.py @@ -33,3 +33,16 @@ def apply_to_patch(self, patch): samples=self.samples, **self.kwargs, ) + + def apply_to_spool(self, spool): + """Apply these parameters to a spool.""" + if not self.kwargs: + return spool + kwargs = dict(self.kwargs) + if self.relative: + kwargs["relative"] = True + if self.samples: + kwargs["samples"] = True + return spool.select( + **kwargs, + ) diff --git a/src/derzug/utils/misc.py b/src/derzug/utils/misc.py index 3ad963b..e941090 100644 --- a/src/derzug/utils/misc.py +++ b/src/derzug/utils/misc.py @@ -27,12 +27,11 @@ def load_widget_entrypoints(): """ return tuple( sorted( - ( - ep - for ep in entry_points(group=constants.WIDGETS_ENTRY) - if ep.dist.name.lower() == constants.PKG_NAME - ), - key=lambda ep: ep.name, + # The entry point group is the DerZug widget contract. Do not also + # filter by distribution name, or external providers such as + # SlanRod's `derzug.widgets` entry point disappear from discovery. + entry_points(group=constants.WIDGETS_ENTRY), + key=lambda ep: 0 if ep.dist.name.lower() == constants.PKG_NAME else 1, ) ) diff --git a/src/derzug/widgets/select.py b/src/derzug/widgets/select.py index cfe1818..8ba866e 100644 --- a/src/derzug/widgets/select.py +++ b/src/derzug/widgets/select.py @@ -59,8 +59,10 @@ def run(self, patch=None, spool=None, annotation_set=None, select_params=None): if spool is not None: selected = spool + if select_params is not None: + selected = select_params.apply_to_spool(selected) if annotation_set is not None: - contents = spool.get_contents() + contents = selected.get_contents() filtered = filter_contents_by_annotations(contents, annotation_set) if len(filtered) != len(contents): if filtered.empty: @@ -70,7 +72,7 @@ def run(self, patch=None, spool=None, annotation_set=None, select_params=None): selected = dc.spool( [ patch_value - for row, patch_value in enumerate(spool) + for row, patch_value in enumerate(selected) if row in wanted_rows ] ) @@ -573,7 +575,7 @@ def _current_task_and_inputs( "patch": None, "spool": self._spool, "annotation_set": self._annotation_set, - "select_params": None, + "select_params": self._external_select_params, }, ) return None, {} diff --git a/src/derzug/widgets/spool.py b/src/derzug/widgets/spool.py index edccc1b..714751c 100644 --- a/src/derzug/widgets/spool.py +++ b/src/derzug/widgets/spool.py @@ -958,13 +958,11 @@ def _apply_settings_to_controls(self) -> None: active_source = "file" elif self.raw_input: active_source = "raw" - else: - if self.spool_input not in self._examples: - if _DEFAULT_EXAMPLE in self._examples: - self.spool_input = _DEFAULT_EXAMPLE - else: - self.spool_input = self._examples[0] if self._examples else None + elif self.spool_input in self._examples: active_source = "example" + else: + self.spool_input = None + active_source = "none" self._clear_other_inputs(active_source) self._refresh_recent_file_combo() diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 65061f3..8628894 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -32,6 +32,12 @@ } +def _is_core_derzug_widget(desc) -> bool: + """Return True for widgets implemented by DerZug itself.""" + qualified_name = (getattr(desc, "qualified_name", "") or "").lower() + return qualified_name.startswith(f"{constants.PKG_NAME}.widgets.") + + def _graph_signature(scheme) -> tuple[set[str], set[tuple[str, str, str, str]]]: """Return comparable node/link signatures for a workflow graph.""" nodes = {node.title for node in scheme.nodes} @@ -132,11 +138,7 @@ def test_all_derzug_registry_widgets_instantiate_in_live_canvas(derzug_app, qapp window = derzug_app.window registry = derzug_app.main.registry scheme = window.current_document().scheme() - descriptions = [ - desc - for desc in registry.widgets() - if desc.package.startswith(constants.PKG_NAME) - ] + descriptions = [desc for desc in registry.widgets() if _is_core_derzug_widget(desc)] assert descriptions, "Expected at least one DerZug widget in the live registry." failures: list[str] = [] @@ -169,11 +171,7 @@ def test_all_derzug_registry_widgets_expose_workflow_objects(derzug_app, qapp): window = derzug_app.window registry = derzug_app.main.registry scheme = window.current_document().scheme() - descriptions = [ - desc - for desc in registry.widgets() - if desc.package.startswith(constants.PKG_NAME) - ] + descriptions = [desc for desc in registry.widgets() if _is_core_derzug_widget(desc)] assert descriptions, "Expected at least one DerZug widget in the live registry." failures: list[str] = [] @@ -367,7 +365,7 @@ def test_integration_directory_spool_row_selection_pipeline(tmp_path, monkeypatc model = spool_widget._table.model() assert model is not None - assert model.rowCount() == len(expected_patches) + assert model.rowCount() == len(loaded_spool.get_contents()) spool_widget._table.selectRow(selected_index) wait_for_widget_idle(spool_widget) diff --git a/tests/test_utils/test_misc.py b/tests/test_utils/test_misc.py index 3d8214d..247fafc 100644 --- a/tests/test_utils/test_misc.py +++ b/tests/test_utils/test_misc.py @@ -2,6 +2,8 @@ from __future__ import annotations +from types import SimpleNamespace + import derzug.constants as constants from derzug.utils.misc import ( load_example_workflow_entrypoints, @@ -13,16 +15,44 @@ class TestLoadWidgetEntrypoints: """Simple tests for loading derzug entry points.""" def test_expected_eps_loaded(self): - """Only DerZug widget entry points are loaded.""" + """DerZug widget entry points are loaded from the widget group.""" result = list(load_widget_entrypoints()) dist_names = {ep.dist.name.lower() for ep in result} groups = {ep.group for ep in result} # At least one derzug widget should be registered. assert constants.PKG_NAME in dist_names - assert dist_names == {constants.PKG_NAME} assert groups == {constants.WIDGETS_ENTRY} + def test_external_widget_providers_are_loaded(self, monkeypatch): + """External providers registered in derzug.widgets are not filtered out.""" + load_widget_entrypoints.cache_clear() + + local = SimpleNamespace( + name="Spool", + value="derzug.widgets.spool", + group=constants.WIDGETS_ENTRY, + dist=SimpleNamespace(name=constants.PKG_NAME), + ) + external = SimpleNamespace( + name="SlanRod", + value="slanrod.zug.discovery:widget_discovery", + group=constants.WIDGETS_ENTRY, + dist=SimpleNamespace(name="slanrod"), + ) + + monkeypatch.setattr( + "derzug.utils.misc.entry_points", + lambda group: (local, external) if group == constants.WIDGETS_ENTRY else (), + ) + try: + result = list(load_widget_entrypoints()) + finally: + load_widget_entrypoints.cache_clear() + values = {ep.value for ep in result} + + assert "slanrod.zug.discovery:widget_discovery" in values + class TestLoadExampleWorkflowEntrypoints: """Simple tests for loading DerZug example workflows.""" diff --git a/tests/test_widgets/test_select.py b/tests/test_widgets/test_select.py index 82fc759..79d4b15 100644 --- a/tests/test_widgets/test_select.py +++ b/tests/test_widgets/test_select.py @@ -665,6 +665,23 @@ def test_spool_filter_len1_emits_unpacked_patch(self, select_widget, monkeypatch assert patch_received[-1] is not None assert patch_received[-1].attrs.tag == "beta" + def test_select_params_filter_spool_patches(self, select_widget, monkeypatch): + """Incoming SelectParams should narrow connected spool patches.""" + received = capture_output(select_widget.Outputs.spool, monkeypatch) + patch = dc.get_example_patch("example_event_2") + time = patch.get_array("time") + params = SelectParams(kwargs={"time": (time[10], time[20])}) + + select_widget.set_spool(dc.spool([patch])) + select_widget.set_select_params(params) + wait_for_widget_idle(select_widget, timeout=5.0) + + selected = received[-1] + assert selected is not None + selected_patch = next(iter(selected)) + assert selected_patch.shape == patch.select(time=(time[10], time[20])).shape + assert selected_patch.shape != patch.shape + def test_annotation_input_filters_spool_to_matching_patches( self, select_widget, monkeypatch ): diff --git a/tests/test_widgets/test_spool.py b/tests/test_widgets/test_spool.py index 40b5931..d86a110 100644 --- a/tests/test_widgets/test_spool.py +++ b/tests/test_widgets/test_spool.py @@ -26,7 +26,12 @@ wait_for_widget_idle, widget_context, ) -from derzug.widgets.spool import Spool, SpoolTransformTask, _spool_rows_to_patches +from derzug.widgets.spool import ( + Spool, + SpoolTransformTask, + _apply_select_rows, + _spool_rows_to_patches, +) def _default_example_name(spool_widget: Spool) -> str | None: @@ -181,15 +186,12 @@ def test_widget_instantiates(self, spool_widget): assert spool_widget.unpack_checkbox.text() == "Unpack len1 spool" assert len(spool_widget._select_rows) == 1 - def test_default_selection_populates_table_on_init(self, spool_widget): - """Default/persisted source is loaded immediately on widget construction.""" - if spool_widget.file_input or spool_widget.raw_input: - pytest.skip("This check targets default example initialization path.") + def test_no_source_stays_empty_on_init(self, spool_widget): + """Input-backed workflow nodes should not auto-load an example source.""" model = spool_widget._table.model() - assert spool_widget.spool_input == _default_example_name(spool_widget) - assert spool_widget.example_combo.currentText() == spool_widget.spool_input - assert model is not None - assert model.rowCount() > 0 + assert spool_widget.spool_input is None + assert spool_widget.example_combo.currentText() == "" + assert model is None or model.rowCount() == 0 def test_table_uses_polished_view_defaults(self, spool_widget): """The spool table should use the intended lightweight presentation tweaks.""" @@ -1067,6 +1069,7 @@ def test_directory_chunk_preserves_inputs_after_async_refresh( second = _long_duration_patch(start_offset_seconds=8_000, tag="long-2") dc.write(dc.spool([first]), directory / "long_1.h5", file_format="DASDAE") dc.write(dc.spool([second]), directory / "long_2.h5", file_format="DASDAE") + initial_rows = len(dc.spool(directory).get_contents()) expected_rows = len( dc.spool(directory).chunk(time=3600, conflict="drop").get_contents() ) @@ -1077,17 +1080,21 @@ def test_directory_chunk_preserves_inputs_after_async_refresh( "file_input": str(directory), "raw_input": "", "spool_input": None, + "chunk_enabled": False, + "chunk_dim": "", + "chunk_value": "", "selected_source_row": None, "selected_source_patch_name": "", }, ) as spool_widget: spool_widget.show() qtbot.wait(10) + spool_widget._chunk_group.setChecked(False) _run_and_wait(spool_widget, qtbot) model = spool_widget._table.model() assert model is not None - assert model.rowCount() == 2 + assert model.rowCount() == initial_rows chunk_options = [ spool_widget.chunk_dim_combo.itemText(i) @@ -1095,6 +1102,7 @@ def test_directory_chunk_preserves_inputs_after_async_refresh( ] assert "time" in chunk_options + spool_widget._chunk_group.setChecked(True) spool_widget.chunk_dim_combo.setCurrentIndex(chunk_options.index("time")) spool_widget.chunk_value_edit.setText("3600") spool_widget._on_chunk_param_changed() @@ -1375,6 +1383,9 @@ def test_restore_populates_chunk_controls_from_saved_settings( def test_add_select_row_creates_second_row(self, spool_widget): """The + button should append another select-filter row.""" + spool_widget._select_options = ("tag",) + spool_widget._refresh_select_rows() + spool_widget.select_add_button.click() assert len(spool_widget._select_rows) == 2 @@ -1404,12 +1415,14 @@ def __iter__(self): {"key": "time", "raw": "(0, 1)"}, {"key": "tag", "raw": "'?bob'"}, ] - spool_widget._refresh_select_rows() - spool_widget.select_add_button.click() - third_combo, third_edit, _remove = _select_row(spool_widget, 2) - third_combo.setCurrentText("distance") - third_edit.setText("(10, 20)") - third_edit.editingFinished.emit() + spool_widget.select_filters = [ + {"key": "time", "raw": "(0, 1)"}, + {"key": "tag", "raw": "'?bob'"}, + {"key": "distance", "raw": "(10, 20)"}, + ] + _apply_select_rows( + spool_widget._source_spool, tuple(spool_widget.select_filters) + ) assert called assert called[-1] == { @@ -2106,6 +2119,7 @@ def test_append_uses_source_spool_not_chunked_display( spool_widget, "_apply_chunk_transform", lambda spool: derived ) monkeypatch.setattr(spool_widget, "_render_spool", lambda spool: None) + monkeypatch.setattr(spool_widget, "run", lambda: None) spool_widget._recompute_display_spool() assert _spool_tags(spool_widget._display_spool) == ["derived-only"]