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
8 changes: 6 additions & 2 deletions src/derzug/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ def load_widget_entrypoints():
"""
return tuple(
sorted(
entry_points(group=constants.WIDGETS_ENTRY),
key=lambda ep: 0 if ep.dist.name.lower() == constants.PKG_NAME else 1,
(
ep
for ep in entry_points(group=constants.WIDGETS_ENTRY)
if ep.dist.name.lower() == constants.PKG_NAME
),
key=lambda ep: ep.name,
)
)

Expand Down
18 changes: 18 additions & 0 deletions src/derzug/utils/spool.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ def normalize_dims_value(value: Any) -> list[str]:

def extract_single_patch(spool: dc.BaseSpool) -> dc.Patch | None:
"""Return the only patch in spool, or None when length is not exactly one."""
get_contents = getattr(spool, "get_contents", None)
if callable(get_contents):
try:
contents = get_contents()
except Exception:
pass
else:
try:
row_count = len(contents)
except Exception:
row_count = 1
if row_count != 1:
return None
try:
return spool[0]
except Exception:
pass

iterator = iter(spool)
try:
first = next(iterator)
Expand Down
4 changes: 3 additions & 1 deletion src/derzug/views/orange_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def filter_registry_for_das(registry: WidgetRegistry) -> WidgetRegistry:
for widget in registry.widgets():
package = (getattr(widget, "package", "") or "").lower()
name = widget.name
is_derzug_widget = package.startswith(constants.PKG_NAME)
is_derzug_widget = package == constants.PKG_NAME or package.startswith(
f"{constants.PKG_NAME}."
)
if not is_derzug_widget and name not in constants.ORANGE_WIDGETS_TO_LOAD:
continue
if "obsolete" in widget.id:
Expand Down
9 changes: 7 additions & 2 deletions src/derzug/widgets/spool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1642,10 +1642,15 @@ def set_patch(self, patch: dc.Patch | None) -> None:

@Inputs.spool
def set_spool(self, spool: dc.BaseSpool | None) -> None:
"""Append an incoming spool to the current spool."""
"""Use an incoming spool as the source for select/chunk transforms."""
if spool is None:
return
self._ingest_input(spool)
self.Error.general.clear()
self._clear_other_inputs("snapshot")
self._source_mode = "snapshot"
self._set_source_controls_enabled(False)
self._set_source_spool(spool)
self.run()

def _ingest_input(self, incoming: dc.BaseSpool) -> None:
"""Merge incoming spool data into the current spool and emit it."""
Expand Down
25 changes: 24 additions & 1 deletion tests/test_utils/test_spool_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
PointGeometry,
SpanGeometry,
)
from derzug.utils.spool import annotation_overlap_mask, filter_contents_by_annotations
from derzug.utils.spool import (
annotation_overlap_mask,
extract_single_patch,
filter_contents_by_annotations,
)


def _contents_df() -> pd.DataFrame:
Expand All @@ -28,6 +32,25 @@ def _contents_df() -> pd.DataFrame:
)


def test_extract_single_patch_uses_metadata_for_multi_row_spool():
"""Multi-row spools should not be iterated just to reject patch unpacking."""

class _LazyMultiRowSpool:
iterated = False

def get_contents(self):
return pd.DataFrame({"tag": ["first", "second"]})

def __iter__(self):
self.iterated = True
raise AssertionError("spool payload was materialized")

spool = _LazyMultiRowSpool()

assert extract_single_patch(spool) is None
assert spool.iterated is False


def test_point_annotations_filter_contents_on_shared_dims():
"""Points should keep rows whose extents contain the point."""
df = _contents_df()
Expand Down
31 changes: 31 additions & 0 deletions tests/test_widgets/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import dascore as dc
import numpy as np
import pandas as pd
import pytest
from derzug.models.annotations import Annotation, AnnotationSet, PointGeometry
from derzug.models.selection import SelectParams
Expand Down Expand Up @@ -732,6 +733,36 @@ def test_multi_patch_spool_emits_no_patch(self, select_widget, monkeypatch):

assert patch_received[-1] is None

def test_multi_row_lazy_spool_unpack_does_not_iterate(
self, select_widget, monkeypatch
):
"""
Unpack should reject multi-row spools from metadata without loading
patches.
"""

class _LazyMultiRowSpool:
iterated = False

def get_contents(self):
return pd.DataFrame({"tag": ["first", "second"]})

def __iter__(self):
self.iterated = True
raise AssertionError("spool payload was materialized")

spool = _LazyMultiRowSpool()
spool_received = capture_output(select_widget.Outputs.spool, monkeypatch)
patch_received = capture_output(select_widget.Outputs.patch, monkeypatch)
select_widget.unpack_single_patch = True

select_widget.set_spool(spool)
wait_for_widget_idle(select_widget, timeout=5.0)

assert spool_received[-1] is spool
assert patch_received[-1] is None
assert spool.iterated is False

def test_clearing_spool_input_clears_patch_output(self, select_widget, monkeypatch):
"""Clearing spool input should clear both spool and patch outputs."""
capture_output(select_widget.Outputs.spool, monkeypatch)
Expand Down
46 changes: 32 additions & 14 deletions tests/test_widgets/test_spool.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
wait_for_widget_idle,
widget_context,
)
from derzug.widgets.spool import Spool, _spool_rows_to_patches
from derzug.widgets.spool import Spool, SpoolTransformTask, _spool_rows_to_patches


def _default_example_name(spool_widget: Spool) -> str | None:
Expand Down Expand Up @@ -1984,21 +1984,37 @@ def test_set_patch_appends_in_memory_spool(self, spool_widget, monkeypatch, qtbo
assert len(list(updated)) == 2
assert len(list(spool_widget._current_spool)) == 2

def test_set_spool_appends_in_memory_spool(self, spool_widget, monkeypatch, qtbot):
"""Spool input appends all incoming patches to an in-memory spool."""
def test_set_spool_uses_input_as_transform_source(
self, spool_widget, monkeypatch, qtbot
):
"""Spool input fronts the incoming spool for select/chunk transforms."""
first = _patch_with_tag("first")
second = _patch_with_tag("second")
third = _patch_with_tag("third")
spool_widget._current_spool = dc.spool([first])
incoming = dc.spool([first, second])
received = capture_output(spool_widget.Outputs.spool, monkeypatch)
monkeypatch.setattr(
spool_widget,
"_write_spool_to_directory",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
AssertionError("spool input should not be persisted")
),
)

spool_widget.set_spool(dc.spool([second, third]))
spool_widget.set_spool(incoming)
wait_for_widget_idle(spool_widget, timeout=5.0)
first_combo, first_edit, _remove = _select_row(spool_widget, 0)
first_combo.setCurrentText("tag")
first_edit.setText("'second'")
first_edit.editingFinished.emit()
wait_for_widget_idle(spool_widget, timeout=5.0)

task = spool_widget.get_task()
assert isinstance(task, SpoolTransformTask)
assert spool_widget._source_spool is incoming
assert spool_widget._source_mode == "snapshot"
assert received
updated = received[-1]
assert len(list(updated)) == 3
assert len(list(spool_widget._current_spool)) == 3
assert _spool_tags(received[-1]) == ["second"]
assert _spool_tags(spool_widget._display_spool) == ["second"]

def test_none_input_is_noop(self, spool_widget, monkeypatch):
"""None on the new inputs leaves the current spool unchanged."""
Expand All @@ -2013,8 +2029,10 @@ def test_none_input_is_noop(self, spool_widget, monkeypatch):
assert received == []
assert spool_widget._current_spool is current

def test_input_uses_put_when_available(self, spool_widget, monkeypatch, qtbot):
"""Input ingestion prefers a spool's put method when it exists."""
def test_patch_input_uses_put_when_available(
self, spool_widget, monkeypatch, qtbot
):
"""Patch input ingestion prefers a spool's put method when it exists."""
called: list[object] = []
updated = dc.spool([_patch_with_tag("put-result")])

Expand All @@ -2026,11 +2044,11 @@ def put(self, value):
spool_widget._current_spool = _PutSpool()
received = capture_output(spool_widget.Outputs.spool, monkeypatch)

incoming = dc.spool([_patch_with_tag("incoming")])
spool_widget.set_spool(incoming)
spool_widget.set_patch(_patch_with_tag("incoming"))
wait_for_widget_idle(spool_widget, timeout=5.0)

assert called == [incoming]
assert len(called) == 1
assert _spool_tags(called[0]) == ["incoming"]
assert received[-1] is updated
assert spool_widget._current_spool is updated

Expand Down
Loading