Skip to content

Commit 37490e9

Browse files
committed
Fix: Apply history session policies consistently to plugin inputs
Plugin-created objects did not consistently honor the configured session policy, and multi-object loads could prompt repeatedly or consume their decision before an object was accepted. Resolve persisted policies at the insertion boundary and carry one lazy, panel-specific decision across each plugin batch. * [FIX] : Apply live general and plugin ask/yes/no settings while preserving explicit per-call choices * [NEW] : Add a validated multi-load scope that decides on the first accepted object and leaves empty or cancelled batches untouched * [FIX] : Decide before insertion, suppress duplicate prompts, report memory refusals, and debounce signal and image panels independently * [CHG] : Batch Test Data plugin loads using the matching data panel * [NEW] : Cover plugin policy behavior and add French settings translations
1 parent 218729a commit 37490e9

10 files changed

Lines changed: 925 additions & 86 deletions

File tree

datalab/control/proxy.py

Lines changed: 134 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,39 @@
7575

7676
from collections.abc import Generator
7777
from contextlib import contextmanager
78+
from dataclasses import dataclass
79+
from typing import TYPE_CHECKING, Literal
7880

7981
import guidata.dataset as gds
8082
import numpy as np
8183
from sigima import ImageObj, SignalObj
8284

85+
from datalab.config import Conf
8386
from datalab.control.baseproxy import BaseProxy
8487
from datalab.control.remote import RemoteClient
8588
from datalab.utils import qthelpers as qth
8689

90+
if TYPE_CHECKING:
91+
from datalab.gui.historysession_ops import SessionBehavior
92+
from datalab.gui.main import DLMainWindow
93+
94+
95+
@dataclass
96+
class MultiLoadState:
97+
"""Track a local proxy multi-object load."""
98+
99+
panel: Literal["signal", "image"]
100+
behavior: SessionBehavior | None
101+
decision_applied: bool = False
102+
103+
def behavior_for(self, panel: Literal["signal", "image"]) -> SessionBehavior | None:
104+
"""Return the session behavior for an object added to ``panel``."""
105+
if panel != self.panel:
106+
raise ValueError(
107+
f"Cannot add a {panel} object during a {self.panel} multiload session"
108+
)
109+
return "no" if self.decision_applied else self.behavior
110+
87111

88112
class RemoteProxy(RemoteClient):
89113
"""DataLab remote proxy class.
@@ -137,8 +161,20 @@ class LocalProxy(BaseProxy):
137161
138162
Args:
139163
datalab (DLMainWindow): DLMainWindow instance.
164+
input_source: Source of objects added through this proxy.
140165
"""
141166

167+
def __init__(
168+
self,
169+
datalab: DLMainWindow | None = None,
170+
input_source: Literal["local", "plugin"] = "local",
171+
) -> None:
172+
if input_source not in ("local", "plugin"):
173+
raise ValueError(f"Invalid local proxy input source: {input_source!r}")
174+
super().__init__(datalab)
175+
self.input_source = input_source
176+
self.multiload_state: MultiLoadState | None = None
177+
142178
def add_signal(
143179
self,
144180
title: str,
@@ -150,6 +186,7 @@ def add_signal(
150186
ylabel: str = "",
151187
group_id: str = "",
152188
set_current: bool = True,
189+
new_session_behavior: SessionBehavior | None = None,
153190
) -> bool: # pylint: disable=too-many-arguments
154191
"""Add signal data to DataLab.
155192
@@ -163,6 +200,7 @@ def add_signal(
163200
ylabel: Y label. Defaults to ""
164201
group_id: group id in which to add the signal. Defaults to ""
165202
set_current: if True, set the added signal as current
203+
new_session_behavior: Optional history session creation policy
166204
167205
Returns:
168206
True if signal was added successfully, False otherwise
@@ -171,9 +209,28 @@ def add_signal(
171209
ValueError: Invalid xdata dtype
172210
ValueError: Invalid ydata dtype
173211
"""
174-
return self._datalab.add_signal(
175-
title, xdata, ydata, xunit, yunit, xlabel, ylabel, group_id, set_current
212+
multiload_state = self.multiload_state
213+
if multiload_state is None:
214+
behavior = new_session_behavior
215+
if behavior is None and self.input_source == "plugin":
216+
behavior = Conf.proc.history_plugin_new_session_behavior.get()
217+
else:
218+
behavior = multiload_state.behavior_for("signal")
219+
added = self._datalab.add_signal(
220+
title,
221+
xdata,
222+
ydata,
223+
xunit,
224+
yunit,
225+
xlabel,
226+
ylabel,
227+
group_id,
228+
set_current,
229+
new_session_behavior=behavior,
176230
)
231+
if added and multiload_state is not None:
232+
multiload_state.decision_applied = True
233+
return added
177234

178235
def add_image(
179236
self,
@@ -187,6 +244,7 @@ def add_image(
187244
zlabel: str = "",
188245
group_id: str = "",
189246
set_current: bool = True,
247+
new_session_behavior: SessionBehavior | None = None,
190248
) -> bool: # pylint: disable=too-many-arguments
191249
"""Add image data to DataLab.
192250
@@ -201,14 +259,22 @@ def add_image(
201259
zlabel: Z label. Defaults to ""
202260
group_id: group id in which to add the image. Defaults to ""
203261
set_current: if True, set the added image as current
262+
new_session_behavior: Optional history session creation policy
204263
205264
Returns:
206265
True if image was added successfully, False otherwise
207266
208267
Raises:
209268
ValueError: Invalid data dtype
210269
"""
211-
return self._datalab.add_image(
270+
multiload_state = self.multiload_state
271+
if multiload_state is None:
272+
behavior = new_session_behavior
273+
if behavior is None and self.input_source == "plugin":
274+
behavior = Conf.proc.history_plugin_new_session_behavior.get()
275+
else:
276+
behavior = multiload_state.behavior_for("image")
277+
added = self._datalab.add_image(
212278
title,
213279
data,
214280
xunit,
@@ -219,19 +285,81 @@ def add_image(
219285
zlabel,
220286
group_id,
221287
set_current,
288+
new_session_behavior=behavior,
222289
)
290+
if added and multiload_state is not None:
291+
multiload_state.decision_applied = True
292+
return added
223293

224294
def add_object(
225-
self, obj: SignalObj | ImageObj, group_id: str = "", set_current: bool = True
226-
) -> None:
295+
self,
296+
obj: SignalObj | ImageObj,
297+
group_id: str = "",
298+
set_current: bool = True,
299+
new_session_behavior: SessionBehavior | None = None,
300+
) -> bool:
227301
"""Add object to DataLab.
228302
229303
Args:
230304
obj: Signal or image object
231305
group_id: group id in which to add the object. Defaults to ""
232306
set_current: if True, set the added object as current
307+
new_session_behavior: Optional history session creation policy
308+
309+
Returns:
310+
True if the object was added successfully, False otherwise
233311
"""
234-
self._datalab.add_object(obj, group_id, set_current)
312+
multiload_state = self.multiload_state
313+
if multiload_state is None:
314+
behavior = new_session_behavior
315+
if behavior is None and self.input_source == "plugin":
316+
behavior = Conf.proc.history_plugin_new_session_behavior.get()
317+
else:
318+
if isinstance(obj, SignalObj):
319+
panel = "signal"
320+
elif isinstance(obj, ImageObj):
321+
panel = "image"
322+
else:
323+
raise TypeError(f"Unsupported object type {type(obj)}")
324+
behavior = multiload_state.behavior_for(panel)
325+
added = self._datalab.add_object(
326+
obj, group_id, set_current, new_session_behavior=behavior
327+
)
328+
if added and multiload_state is not None:
329+
multiload_state.decision_applied = True
330+
return added
331+
332+
@contextmanager
333+
def multiload_session(
334+
self,
335+
panel: Literal["signal", "image"],
336+
new_session_behavior: SessionBehavior | None = None,
337+
) -> Generator[None, None, None]:
338+
"""Apply one lazy session decision to a multi-object load.
339+
340+
Args:
341+
panel: Target data panel ("signal" or "image")
342+
new_session_behavior: Optional history session creation policy
343+
344+
Raises:
345+
ValueError: If the panel or session behavior is invalid
346+
RuntimeError: If another multiload session is already active
347+
"""
348+
if panel not in ("signal", "image"):
349+
raise ValueError(f"Invalid data panel: {panel!r}")
350+
behavior = new_session_behavior
351+
if behavior is None and self.input_source == "plugin":
352+
behavior = Conf.proc.history_plugin_multiload_behavior.get()
353+
if behavior is not None and behavior not in ("ask", "yes", "no"):
354+
raise ValueError(f"Invalid session behavior: {behavior!r}")
355+
if self.multiload_state is not None:
356+
raise RuntimeError("Nested multiload sessions are not supported")
357+
previous_state = self.multiload_state
358+
self.multiload_state = MultiLoadState(panel, behavior)
359+
try:
360+
yield
361+
finally:
362+
self.multiload_state = previous_state
235363

236364
def calc(self, name: str, param: gds.DataSet | None = None) -> None:
237365
"""Call computation feature ``name``

datalab/gui/historysession_ops.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from qtpy import QtWidgets as QW
1212

13-
from datalab.config import _
13+
from datalab.config import Conf, _
1414
from datalab.env import execenv
1515
from datalab.gui.panel.history import chain as hchain
1616
from datalab.history import HistoryAction, HistorySession, WorkspaceState
@@ -56,7 +56,7 @@ def maybe_start_session_for_input(
5656
panel_str: str | None = None,
5757
*,
5858
load: bool = False,
59-
behavior: SessionBehavior = "ask",
59+
behavior: SessionBehavior | None = None,
6060
) -> bool:
6161
"""Offer to start a new history session before a creation/load is recorded.
6262
@@ -71,14 +71,16 @@ def maybe_start_session_for_input(
7171
load: True when triggered by a file/workspace load, False for an object
7272
creation. Only affects the prompt wording.
7373
behavior: Session creation policy: ask, always create ("yes"), or keep
74-
the current session ("no").
74+
the current session ("no"). Defaults to the live general policy.
7575
7676
Returns:
7777
True if a new session was created.
7878
7979
Raises:
8080
ValueError: If ``behavior`` is unsupported.
8181
"""
82+
if behavior is None:
83+
behavior = Conf.proc.history_new_session_behavior.get()
8284
if behavior not in SESSION_BEHAVIORS:
8385
raise ValueError(f"Invalid session behavior: {behavior!r}")
8486
if not panel.record_mode_enabled or panel.is_replaying():
@@ -96,7 +98,7 @@ def maybe_start_session_for_input(
9698
return True
9799
# Debounce: a synchronous burst of creations (plugin/macro) must prompt only
98100
# once. The guard is reset on the next event-loop turn.
99-
if not panel.runtime.execution.start_session_input_prompt():
101+
if not panel.runtime.execution.start_session_input_prompt(target_panel_str):
100102
return False
101103
if execenv.unattended:
102104
# Headless runs: honor the accept_dialogs flag (default False -> "No"),

datalab/gui/main.py

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
if TYPE_CHECKING:
9595
from typing import Literal
9696

97+
from datalab.gui.historysession_ops import SessionBehavior
9798
from datalab.gui.panel.base import AbstractPanel, BaseDataPanel
9899
from datalab.gui.panel.image import ImagePanel
99100
from datalab.gui.panel.macro import MacroPanel
@@ -2336,36 +2337,51 @@ def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
23362337
# (see TODO regarding RemoteClient.add_object method)
23372338
# @remote_controlled
23382339
def add_object(
2339-
self, obj: SignalObj | ImageObj, group_id: str = "", set_current=True
2340-
) -> None:
2340+
self,
2341+
obj: SignalObj | ImageObj,
2342+
group_id: str = "",
2343+
set_current=True,
2344+
new_session_behavior: SessionBehavior | None = None,
2345+
) -> bool:
23412346
"""Add object - signal or image
23422347
23432348
Args:
23442349
obj: object to add (signal or image)
23452350
group_id: group ID (optional)
23462351
set_current: True to set the object as current object
2352+
new_session_behavior: Optional history session creation policy
2353+
2354+
Returns:
2355+
True if the object was added successfully, False otherwise
23472356
"""
2348-
if self.confirm_memory_state():
2349-
if isinstance(obj, SignalObj):
2350-
self.signalpanel.add_object(obj, group_id, set_current)
2351-
panel_str = "signal"
2352-
elif isinstance(obj, ImageObj):
2353-
self.imagepanel.add_object(obj, group_id, set_current)
2354-
panel_str = "image"
2355-
else:
2356-
raise TypeError(f"Unsupported object type {type(obj)}")
2357-
# Record a creation entry so objects added programmatically (plugins,
2358-
# macros, remote control) appear in the history. ``panel.add_object``
2359-
# deliberately does not record, so creations entering through this
2360-
# proxy boundary would otherwise be lost (notably the very first one).
2357+
if not self.confirm_memory_state():
2358+
return False
2359+
if isinstance(obj, SignalObj):
2360+
panel = self.signalpanel
2361+
panel_str = "signal"
2362+
elif isinstance(obj, ImageObj):
2363+
panel = self.imagepanel
2364+
panel_str = "image"
2365+
else:
2366+
raise TypeError(f"Unsupported object type {type(obj)}")
2367+
self.historypanel.maybe_start_session_for_input(
2368+
panel_str=panel_str, behavior=new_session_behavior
2369+
)
2370+
panel.add_object(obj, group_id, set_current)
2371+
# Record a creation entry so objects added programmatically (plugins,
2372+
# macros, remote control) appear in the history. ``panel.add_object``
2373+
# deliberately does not record, so creations entering through this
2374+
# proxy boundary would otherwise be lost (notably the very first one).
2375+
with self.historypanel.session_prompt_suppressed():
23612376
action = self.historypanel.add_ui_entry(
23622377
_("New %s") % panel_str,
23632378
target=panel_str + "panel",
23642379
method_name="new_object",
23652380
save_state=False,
23662381
)
2367-
if action is not None:
2368-
self.historypanel.register_action_outputs(action, [get_uuid(obj)])
2382+
if action is not None:
2383+
self.historypanel.register_action_outputs(action, [get_uuid(obj)])
2384+
return True
23692385

23702386
@remote_controlled
23712387
def set_object(self, obj: SignalObj | ImageObj) -> None:
@@ -2436,6 +2452,7 @@ def add_signal(
24362452
ylabel: str = "",
24372453
group_id: str = "",
24382454
set_current: bool = True,
2455+
new_session_behavior: SessionBehavior | None = None,
24392456
) -> bool: # pylint: disable=too-many-arguments
24402457
"""Add signal data to DataLab.
24412458
@@ -2449,6 +2466,7 @@ def add_signal(
24492466
ylabel: Y label. Defaults to ""
24502467
group_id: group id in which to add the signal. Defaults to ""
24512468
set_current: if True, set the added signal as current
2469+
new_session_behavior: Optional history session creation policy
24522470
24532471
Returns:
24542472
True if signal was added successfully, False otherwise
@@ -2464,8 +2482,7 @@ def add_signal(
24642482
units=(xunit, yunit),
24652483
labels=(xlabel, ylabel),
24662484
)
2467-
self.add_object(obj, group_id, set_current)
2468-
return True
2485+
return self.add_object(obj, group_id, set_current, new_session_behavior)
24692486

24702487
# This API mirrors the image metadata accepted by create_image, so the
24712488
# argument count is part of the stable public interface rather than noise.
@@ -2481,6 +2498,7 @@ def add_image( # pylint: disable=too-many-arguments
24812498
zlabel: str = "",
24822499
group_id: str = "",
24832500
set_current: bool = True,
2501+
new_session_behavior: SessionBehavior | None = None,
24842502
) -> bool:
24852503
"""Add image data to DataLab.
24862504
@@ -2495,6 +2513,7 @@ def add_image( # pylint: disable=too-many-arguments
24952513
zlabel: Z label. Defaults to ""
24962514
group_id: group id in which to add the image. Defaults to ""
24972515
set_current: if True, set the added image as current
2516+
new_session_behavior: Optional history session creation policy
24982517
24992518
Returns:
25002519
True if image was added successfully, False otherwise
@@ -2508,8 +2527,7 @@ def add_image( # pylint: disable=too-many-arguments
25082527
units=(xunit, yunit, zunit),
25092528
labels=(xlabel, ylabel, zlabel),
25102529
)
2511-
self.add_object(obj, group_id, set_current)
2512-
return True
2530+
return self.add_object(obj, group_id, set_current, new_session_behavior)
25132531

25142532
# ------?
25152533
def __about(self) -> None: # pragma: no cover

0 commit comments

Comments
 (0)