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
9 changes: 6 additions & 3 deletions .github/workflows/test_pyqt5.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,29 @@ jobs:
python -m pip install --upgrade pip
python -m pip install ruff pytest httpx
pip install PyQt5
if [ "${{ github.ref_name }}" = "develop" ]; then
if [ "${{ github.base_ref || github.ref_name }}" = "develop" ]; then
# Clone and install development versions of key dependencies with editable install
cd ..
git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git
git clone --depth 1 --branch develop https://github.com/PlotPyStack/guidata.git
git clone --depth 1 --branch develop https://github.com/PlotPyStack/plotpy.git
git clone --depth 1 --branch develop https://github.com/DataLab-Platform/sigima.git
DEPENDENCY_BRANCH="${{ github.head_ref || github.ref_name }}"
git clone --depth 1 --branch "$DEPENDENCY_BRANCH" https://github.com/DataLab-Platform/sigima.git || git clone --depth 1 --branch develop https://github.com/DataLab-Platform/sigima.git
git clone --depth 1 --branch "$DEPENDENCY_BRANCH" https://github.com/DataLab-Platform/SigimaX.git || git clone --depth 1 --branch develop https://github.com/DataLab-Platform/SigimaX.git
cd DataLab
pip install -e ../guidata
pip install -e ../PythonQwt
pip install -e ../plotpy
pip install -e ../sigima
pip install -e ../SigimaX --no-deps
# Install tomli for TOML parsing (safe if already present)
pip install tomli
# Extract dependencies and save to file, then install
python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata','PlotPy','Sigima'])]; open('deps.txt','w').write('\n'.join(deps))"
pip install -r deps.txt
# Install DataLab without dependencies
pip install --no-deps .
elif [ "${{ github.ref_name }}" = "release" ]; then
elif [ "${{ github.base_ref || github.ref_name }}" = "release" ]; then
# Clone dependencies from release branches (with fallback to main/master)
cd ..
# Try cloning PythonQwt from main or master
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/test_pyqt6.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,29 @@ jobs:
python -m pip install --upgrade pip
python -m pip install ruff pytest httpx
pip install PyQt6
if [ "${{ github.ref_name }}" = "develop" ]; then
if [ "${{ github.base_ref || github.ref_name }}" = "develop" ]; then
# Clone and install development versions of key dependencies with editable install
cd ..
git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git
git clone --depth 1 --branch develop https://github.com/PlotPyStack/guidata.git
git clone --depth 1 --branch develop https://github.com/PlotPyStack/plotpy.git
git clone --depth 1 --branch develop https://github.com/DataLab-Platform/sigima.git
DEPENDENCY_BRANCH="${{ github.head_ref || github.ref_name }}"
git clone --depth 1 --branch "$DEPENDENCY_BRANCH" https://github.com/DataLab-Platform/sigima.git || git clone --depth 1 --branch develop https://github.com/DataLab-Platform/sigima.git
git clone --depth 1 --branch "$DEPENDENCY_BRANCH" https://github.com/DataLab-Platform/SigimaX.git || git clone --depth 1 --branch develop https://github.com/DataLab-Platform/SigimaX.git
cd DataLab
pip install -e ../guidata
pip install -e ../PythonQwt
pip install -e ../plotpy
pip install -e ../sigima
pip install -e ../SigimaX --no-deps
# Install tomli for TOML parsing (safe if already present)
pip install tomli
# Extract dependencies and save to file, then install
python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata','PlotPy','Sigima'])]; open('deps.txt','w').write('\n'.join(deps))"
pip install -r deps.txt
# Install DataLab without dependencies
pip install --no-deps .
elif [ "${{ github.ref_name }}" = "release" ]; then
elif [ "${{ github.base_ref || github.ref_name }}" = "release" ]; then
# Clone dependencies from release branches (with fallback to main/master)
cd ..
# Try cloning PythonQwt from main or master
Expand Down
68 changes: 56 additions & 12 deletions datalab/gui/panel/history/interactive_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import guidata.dataset as gds
from qtpy import QtWidgets as QW
from sigima.objects.base import BaseROIParam

from datalab.config import _
from datalab.env import execenv
Expand Down Expand Up @@ -220,9 +221,51 @@ def _entry_still_in_history(
return hchain.find_parent_session(panel, entry) is not None


def action_has_roi_params(action: HistoryAction) -> bool:
"""Return whether ``action`` was recorded with region-of-interest parameters.

Args:
action: Recorded action to inspect

Returns:
True if at least one recorded parameter is a ROI parameter.
"""
values: list[Any] = []
for key in ("param", "params"):
value = action.kwargs.get(key)
if isinstance(value, (list, tuple)):
values.extend(value)
elif value is not None:
values.append(value)
return any(isinstance(value, BaseROIParam) for value in values)


def inform_roi_edit_unsupported(panel: HistoryPanel) -> None:
"""Tell the user that ROI parameters cannot be edited from the history.

Args:
panel: History panel instance
"""
if execenv.unattended:
return
QW.QMessageBox.information(
panel.mainwindow,
_("Recompute regions of interest"),
_(
"Regions of interest cannot be edited from the History panel: "
"the ROI editor cannot be reopened with the recorded parameters. "
"The recorded regions of interest are kept as is."
),
)


def prepare_action_param_edit(action: HistoryAction) -> ActionParamEdit | None:
"""Prepare the editable parameter copy for ``action``."""
result = None
if action_has_roi_params(action):
# ROIs are defined with the interactive ROI editor, which cannot be
# reopened with the recorded parameters.
return None
if (
action.kind == HistoryAction.KIND_UI
and action.method_name in HistoryAction.UI_CREATION_METHODS
Expand Down Expand Up @@ -445,6 +488,10 @@ def run_replay_actions(
ordered = order_selected_actions(panel, actions)
if not ordered:
return
# Non-editable actions (ROIs, interactive fits) only report their refusal
# when a single action was selected: replaying a session or a batch keeps
# their recorded parameters silently.
report_non_editable = prompt and len(ordered) == 1
with panel.runtime.execution.replaying_edits() as started:
if not started:
return
Expand Down Expand Up @@ -483,6 +530,12 @@ def run_replay_actions(
continue
if prompt:
result = prompt_edit_action_params(panel, action)
if (
result is None
and report_non_editable
and action_has_roi_params(action)
):
inform_roi_edit_unsupported(panel)
if result is False:
for selected_action in ordered:
kwargs, saved_kwargs = entry_states[selected_action.uuid]
Expand Down Expand Up @@ -572,10 +625,11 @@ def run_replay_actions(
if is_load_action
else None
)
payload_before = action.kwargs.get("payload")
with panel.replaying(), panel.output_suppressed():
action.replay(
panel.mainwindow, restore_selection=True, edit=prompt
panel.mainwindow,
restore_selection=True,
edit=report_non_editable,
)
if before_ids is not None:
new_uuids = [
Expand All @@ -590,16 +644,6 @@ def run_replay_actions(
# recorded outputs would break duplicate detection
# and downstream reconnection.
panel.register_action_outputs(action, new_uuids)
if (
prompt
and action.kind == HistoryAction.KIND_MUTATION
and action.kwargs.get("payload") is not payload_before
):
# The mutation payload was edited in the dialog:
# recompute the downstream closure (seeded from the
# mutation targets, see ``get_downstream_actions``).
panel.tree.refresh_action_item(action)
hrec.recompute_cascade(panel, action)
continue
if hchain.action_consumes_any(action, blocked_outputs):
blocked_outputs.update(
Expand Down
23 changes: 23 additions & 0 deletions datalab/gui/panel/history/recompute.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
insert_processing_parameters,
)
from datalab.history import HistoryAction
from datalab.history.core import decode_roi
from datalab.history.effects import AnalysisEffects, capture_effects, merge_effects
from datalab.objectmodel import get_uuid

Expand Down Expand Up @@ -855,6 +856,10 @@ def recompute_1_to_0_in_place(panel: HistoryPanel, action: HistoryAction) -> boo
copied and a failed attempt rolls back exactly those keys (plus any key
the attempt created), leaving unrelated metadata untouched. Legacy
actions without a manifest fall back to a full-metadata snapshot.
Sources whose manifest records a pre-analysis ROI (``roi_before``) are
restored to it and re-run with first-run semantics, regenerating
detection ROIs; user ROI edits recorded as later mutation actions are
then re-applied by the replay/cascade sequence.
On success, the freshly captured effects are merged into the manifest.
"""
panel_data = hchain.resolve_panel_for_action(panel, action)
Expand All @@ -877,17 +882,34 @@ def recompute_1_to_0_in_place(panel: HistoryPanel, action: HistoryAction) -> boo
for uuid, obj in zip(sources, source_objs)
]
captured: dict[str, AnalysisEffects] = {}
roi_snapshots: dict[str, Any] = {}

def rollback() -> None:
for uuid, obj, (saved, absent) in zip(sources, source_objs, snapshots):
_restore_analysis_source(obj, saved, absent, captured.get(uuid))
# Undo the pre-analysis ROI restoration after the metadata restore so
# the ROI setter leaves both metadata and cache consistent.
for uuid, obj in zip(sources, source_objs):
if uuid in roi_snapshots:
obj.roi = roi_snapshots[uuid]

try:
for uuid, src_obj in zip(sources, source_objs):
analysis_parameters = extract_analysis_parameters(src_obj)
plugin_origin = action.plugin_origin or (
analysis_parameters.plugin_origin if analysis_parameters else None
)
# Restore the recorded pre-analysis ROI so the detection re-runs
# on the same region as the first run, with ROI creation enabled
# ("" encodes "no ROI before", None means legacy/not recorded).
roi_before = AnalysisEffects.from_dict(
(action.effects or {}).get(uuid) or {}
).roi_before
if roi_before is not None:
roi_snapshots[uuid] = (
src_obj.roi.copy() if src_obj.roi is not None else None
)
src_obj.roi = decode_roi(roi_before) if roi_before else None
with capture_effects(src_obj) as effects:
# Register the (mutable) effects before running so rollback
# sees them even when the recompute raises
Expand All @@ -897,6 +919,7 @@ def rollback() -> None:
src_obj,
param,
plugin_origin=plugin_origin,
first_run_side_effects=roi_before is not None,
)
if not success:
rollback()
Expand Down
8 changes: 7 additions & 1 deletion datalab/gui/processor/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1608,6 +1608,7 @@ def recompute_1_to_0(
obj: SignalObj | ImageObj,
param: gds.DataSet | None = None,
plugin_origin: dict[str, Any] | None = None,
first_run_side_effects: bool = False,
) -> bool:
"""Recompute a 1-to-0 analysis on ``obj`` in place.

Expand All @@ -1620,14 +1621,19 @@ def recompute_1_to_0(
obj: Object whose analysis must be refreshed.
param: Analysis parameters (optional).
plugin_origin: Optional plugin origin descriptor.
first_run_side_effects: If True, keep first-run-only side effects
enabled (e.g. ``create_rois``) so detection ROIs are regenerated.
Used by the history replay engine after restoring the object's
pre-analysis ROI; the default (False) protects user-edited ROIs.

Returns:
True if the analysis result was refreshed successfully.
"""
# Work on a local copy so callers' kwargs are never mutated, and
# disable side effects that must only run on first execution
param = copy.deepcopy(param)
disable_first_run_side_effects(param)
if not first_run_side_effects:
disable_first_run_side_effects(param)
paramclass_name = type(param).__name__ if param is not None else None
feature = self.get_feature(
func_name,
Expand Down
30 changes: 16 additions & 14 deletions datalab/history/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@

import sigima.proc.image
import sigima.proc.signal
from guidata.dataset.datatypes import DataSet, DataSetGroup
from guidata.dataset.datatypes import DataSet
from qtpy import QtWidgets as QW

from datalab.config import _
from datalab.env import execenv
Expand Down Expand Up @@ -572,11 +573,10 @@ def replay_mutation(

Args:
mainwindow: DataLab's main window
edit: If True (and not in unattended mode), open the ROI parameter
dialog before applying so the recorded payload can be modified.
Deletion payloads (None) have nothing to edit and are applied
directly. If the dialog is cancelled, the recorded payload is
applied as-is.
edit: If True, the replay was requested in edit mode. Regions of
interest are defined with the interactive ROI editor, which
cannot be reopened with the recorded payload, so nothing is
edited and the recorded ROI is re-applied as is.
refresh: If True (default), refresh the panel selection and plot
after applying the mutation. The cascade engine passes False as
it refreshes each target itself.
Expand Down Expand Up @@ -610,14 +610,16 @@ def replay_mutation(
if not targets:
return []
if edit and payload is not None and not execenv.unattended:
# Edit mode: let the user adjust the ROI payload before applying.
obj = panel_data.objmodel[targets[0]]
params = payload.to_params(obj)
group = DataSetGroup(params, title=_("Regions of Interest"))
if group.edit(parent=mainwindow):
payload = payload.__class__.from_params(obj, params)
self.snapshot_kwargs()
self.kwargs["payload"] = payload
QW.QMessageBox.information(
mainwindow,
_("Recompute regions of interest"),
_(
"Regions of interest cannot be edited from the History "
"panel: the ROI editor cannot be reopened with the "
"recorded parameters. The recorded regions of interest "
"are kept as is."
),
)
for uuid in targets:
obj = panel_data.objmodel[uuid]
obj.roi = payload.copy() if payload is not None else None
Expand Down
Loading
Loading