Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/derzug/models/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
11 changes: 5 additions & 6 deletions src/derzug/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
)

Expand Down
8 changes: 5 additions & 3 deletions src/derzug/widgets/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
]
)
Expand Down Expand Up @@ -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, {}
Expand Down
10 changes: 4 additions & 6 deletions src/derzug/widgets/spool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
20 changes: 9 additions & 11 deletions tests/test_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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] = []
Expand Down Expand Up @@ -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] = []
Expand Down Expand Up @@ -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)

Expand Down
34 changes: 32 additions & 2 deletions tests/test_utils/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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."""
Expand Down
17 changes: 17 additions & 0 deletions tests/test_widgets/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
46 changes: 30 additions & 16 deletions tests/test_widgets/test_spool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
)
Expand All @@ -1077,24 +1080,29 @@ 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)
for i in range(spool_widget.chunk_dim_combo.count())
]
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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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] == {
Expand Down Expand Up @@ -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"]
Expand Down
Loading