Skip to content

Commit dfec0d3

Browse files
committed
feat: add set_object method to update existing objects in DataLab across multiple components
1 parent f35e11e commit dfec0d3

13 files changed

Lines changed: 491 additions & 212 deletions

File tree

datalab/control/baseproxy.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,22 @@ def add_object(
341341
True if object was added successfully, False otherwise
342342
"""
343343

344+
@abc.abstractmethod
345+
def set_object(self, obj: SignalObj | ImageObj) -> None:
346+
"""Set object data in DataLab.
347+
348+
Update an existing object in DataLab with new data from ``obj``.
349+
The object is identified by its UUID (which is carried by ``obj``
350+
from a previous :meth:`get_object` call).
351+
352+
Args:
353+
obj: Signal or image object (must have the same UUID as an
354+
existing object in DataLab)
355+
356+
Raises:
357+
KeyError: if no object with matching UUID is found
358+
"""
359+
344360
@abc.abstractmethod
345361
def add_group(
346362
self, title: str, panel: str | None = None, select: bool = False
@@ -859,6 +875,22 @@ def get_group_titles_with_object_info(
859875
"""
860876
return self._datalab.get_group_titles_with_object_info()
861877

878+
def set_object(self, obj: SignalObj | ImageObj) -> None:
879+
"""Set object data in DataLab.
880+
881+
Update an existing object in DataLab with new data from ``obj``.
882+
The object is identified by its UUID (which is carried by ``obj``
883+
from a previous :meth:`get_object` call).
884+
885+
Args:
886+
obj: Signal or image object (must have the same UUID as an
887+
existing object in DataLab)
888+
889+
Raises:
890+
KeyError: if no object with matching UUID is found
891+
"""
892+
self._datalab.set_object(obj)
893+
862894
def get_object_titles(self, panel: str | None = None) -> list[str]:
863895
"""Get object (signal/image) list for current panel.
864896
Objects are sorted by group number and object index in group.

datalab/control/remote.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class RemoteServer(QC.QThread):
8484
SIG_CLOSE_APP = QC.Signal()
8585
SIG_RAISE_WINDOW = QC.Signal()
8686
SIG_ADD_OBJECT = QC.Signal(object, str, bool)
87+
SIG_SET_OBJECT = QC.Signal(object)
8788
SIG_ADD_GROUP = QC.Signal(str, str, bool)
8889
SIG_LOAD_FROM_FILES = QC.Signal(list)
8990
SIG_LOAD_FROM_DIRECTORY = QC.Signal(str)
@@ -120,6 +121,7 @@ def __init__(self, win: DLMainWindow) -> None:
120121
self.SIG_CLOSE_APP.connect(win.close)
121122
self.SIG_RAISE_WINDOW.connect(win.raise_window)
122123
self.SIG_ADD_OBJECT.connect(win.add_object)
124+
self.SIG_SET_OBJECT.connect(win.set_object)
123125
self.SIG_ADD_GROUP.connect(win.add_group)
124126
self.SIG_LOAD_FROM_FILES.connect(win.load_from_files)
125127
self.SIG_LOAD_FROM_DIRECTORY.connect(win.load_from_directory)
@@ -449,6 +451,26 @@ def add_object(
449451
self.SIG_ADD_OBJECT.emit(obj, group_id, set_current)
450452
return True
451453

454+
@remote_call
455+
def set_object(self, obj_data: list[str]) -> bool:
456+
"""Set object data in DataLab.
457+
458+
Update an existing object in DataLab with new data from ``obj_data``.
459+
The object is identified by its UUID.
460+
461+
Args:
462+
obj_data: Object data (RPC-JSON serialized)
463+
464+
Returns:
465+
True if successful
466+
467+
Raises:
468+
KeyError: if no object with matching UUID is found
469+
"""
470+
obj = utils.rpcjson_to_dataset(obj_data)
471+
self.SIG_SET_OBJECT.emit(obj)
472+
return True
473+
452474
@remote_call
453475
def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]:
454476
"""Return selected objects uuids.
@@ -1068,6 +1090,23 @@ def add_object(
10681090
obj_data = utils.dataset_to_rpcjson(obj)
10691091
self._datalab.add_object(obj_data, group_id, set_current)
10701092

1093+
def set_object(self, obj: SignalObj | ImageObj) -> None:
1094+
"""Set object data in DataLab.
1095+
1096+
Update an existing object in DataLab with new data from ``obj``.
1097+
The object is identified by its UUID (which is carried by ``obj``
1098+
from a previous :meth:`get_object` call).
1099+
1100+
Args:
1101+
obj: Signal or image object (must have the same UUID as an
1102+
existing object in DataLab)
1103+
1104+
Raises:
1105+
KeyError: if no object with matching UUID is found
1106+
"""
1107+
obj_data = utils.dataset_to_rpcjson(obj)
1108+
self._datalab.set_object(obj_data)
1109+
10711110
def calc(self, name: str, param: gds.DataSet | None = None) -> None:
10721111
"""Call computation feature ``name``
10731112

datalab/gui/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2225,6 +2225,27 @@ def add_object(
22252225
else:
22262226
raise TypeError(f"Unsupported object type {type(obj)}")
22272227

2228+
@remote_controlled
2229+
def set_object(self, obj: SignalObj | ImageObj) -> None:
2230+
"""Set object data - update an existing signal or image in-place.
2231+
2232+
The existing object is identified by UUID carried by ``obj``
2233+
(from a previous :meth:`get_object` call).
2234+
2235+
Args:
2236+
obj: object with updated data (signal or image)
2237+
2238+
Raises:
2239+
KeyError: if no object with matching UUID is found
2240+
TypeError: if object type is unsupported
2241+
"""
2242+
if isinstance(obj, SignalObj):
2243+
self.signalpanel.set_object(obj)
2244+
elif isinstance(obj, ImageObj):
2245+
self.imagepanel.set_object(obj)
2246+
else:
2247+
raise TypeError(f"Unsupported object type {type(obj)}")
2248+
22282249
@remote_controlled
22292250
def load_from_files(self, filenames: list[str]) -> None:
22302251
"""Open objects from files in current panel (signals/images)

datalab/gui/panel/base.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,14 @@
8484
insert_processing_parameters,
8585
)
8686
from datalab.gui.roieditor import TypeROIEditor
87-
from datalab.objectmodel import ObjectGroup, get_short_id, get_uuid, set_uuid
87+
from datalab.objectmodel import (
88+
ObjectGroup,
89+
get_number,
90+
get_short_id,
91+
get_uuid,
92+
set_number,
93+
set_uuid,
94+
)
8895
from datalab.utils.qthelpers import (
8996
CallbackWorker,
9097
create_progress_bar,
@@ -1550,6 +1557,33 @@ def add_object(
15501557

15511558
self.objview.update_tree()
15521559

1560+
def set_object(self, obj: TypeObj) -> None:
1561+
"""Update an existing object in-place with data from ``obj``.
1562+
1563+
The existing object is identified by UUID (carried by ``obj`` from a
1564+
previous :meth:`get_object` call). All data attributes are copied from
1565+
``obj`` to the existing object while preserving internal metadata
1566+
(number, group membership).
1567+
1568+
Args:
1569+
obj: SignalObj or ImageObj with the same UUID as an existing object.
1570+
1571+
Raises:
1572+
KeyError: if no object with matching UUID is found
1573+
"""
1574+
obj_uuid = get_uuid(obj)
1575+
existing = self.objmodel[obj_uuid] # KeyError if not found
1576+
# Preserve internal number metadata before overwriting
1577+
number = get_number(existing)
1578+
# Copy all public DataSet item values from obj to existing
1579+
for item in existing._items: # pylint: disable=protected-access
1580+
name = item.get_name()
1581+
if not name.startswith("_"):
1582+
setattr(existing, name, getattr(obj, name))
1583+
set_number(existing, number)
1584+
self.objview.update_tree()
1585+
self.refresh_plot("selected", update_items=True, force=True)
1586+
15531587
def remove_all_objects(self) -> None:
15541588
"""Remove all objects"""
15551589
# iterate over a copy of self.__separate_views dict keys to avoid RuntimeError:

datalab/locale/fr/LC_MESSAGES/datalab.po

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3449,3 +3449,4 @@ msgstr "Merci de sélectionner le fichier à importer."
34493449

34503450
msgid "Example Wizard"
34513451
msgstr "Assistant exemple"
3452+

datalab/tests/features/control/remoteclient_unit.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ def multiple_commands(remote: RemoteProxy):
6060

6161
remote.set_current_panel("signal")
6262
assert remote.get_current_panel() == "signal"
63+
64+
# Test set_object round-trip (get → modify → set → verify)
65+
uuids = remote.get_object_uuids()
66+
obj = remote.get_object(uuids[0])
67+
original_title = obj.title
68+
obj.title = "Modified by set_object"
69+
remote.set_object(obj)
70+
obj2 = remote.get_object(uuids[0])
71+
assert obj2.title == "Modified by set_object", (
72+
f"set_object failed: expected 'Modified by set_object', got '{obj2.title}'"
73+
)
74+
obj2.title = original_title
75+
remote.set_object(obj2)
76+
6377
remote.calc("log10")
6478

6579
param = XYCalibrateParam.create(a=1.2, b=0.1)

datalab/webapi/adapter.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,50 @@ def do_update():
422422

423423
self._executor.run_on_main_thread(do_update)
424424

425+
def set_object(self, name: str, obj: DataObject) -> None:
426+
"""Update an existing object in-place with new data.
427+
428+
This operation replaces all data attributes of the existing object
429+
(identified by name/title) with those from ``obj``, preserving the
430+
object's identity, group membership, and position in the workspace.
431+
432+
This operation is marshaled to the Qt main thread for thread safety.
433+
434+
Args:
435+
name: Object name/title.
436+
obj: Object with updated data (same type as existing).
437+
438+
Raises:
439+
KeyError: If object not found.
440+
"""
441+
self._ensure_main_window()
442+
443+
panel_name = self.get_object_panel(name)
444+
if panel_name is None:
445+
raise KeyError(f"Object '{name}' not found")
446+
447+
if panel_name == "signal":
448+
panel = self._main_window.signalpanel
449+
else:
450+
panel = self._main_window.imagepanel
451+
452+
def do_set():
453+
# Find the existing object by title
454+
for existing in panel.objmodel:
455+
if existing.title == name:
456+
# Copy all public DataSet item values
457+
for item in existing._items: # pylint: disable=protected-access
458+
attr_name = item.get_name()
459+
if not attr_name.startswith("_"):
460+
setattr(existing, attr_name, getattr(obj, attr_name))
461+
# Refresh display
462+
panel.objview.update_tree()
463+
panel.SIG_REFRESH_PLOT.emit("selected", True)
464+
return
465+
raise KeyError(f"Object '{name}' not found")
466+
467+
self._executor.run_on_main_thread(do_set)
468+
425469
def clear(self) -> None:
426470
"""Clear all objects from the workspace.
427471

datalab/webapi/routes.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,66 @@ async def put_object_data(
461461
) from e
462462

463463

464+
@router.put(
465+
"/objects/{name}",
466+
response_model=ObjectMetadata,
467+
responses={
468+
401: {"model": ErrorResponse},
469+
404: {"model": ErrorResponse},
470+
422: {"model": ErrorResponse},
471+
500: {"model": ErrorResponse},
472+
},
473+
)
474+
async def set_object(
475+
name: str,
476+
request: Request,
477+
_token: str = Depends(verify_token),
478+
adapter: WorkspaceAdapter = Depends(get_adapter),
479+
) -> ObjectMetadata:
480+
"""Update an existing object in-place from NPZ data.
481+
482+
The request body must be a NumPy NPZ archive containing the object data
483+
(x.npy/y.npy for signals, data.npy for images) and metadata.json.
484+
485+
Unlike ``PUT /objects/{name}/data`` (which creates or replaces), this
486+
endpoint requires the object to already exist and updates it in-place,
487+
preserving its identity, group membership, and position.
488+
489+
Args:
490+
name: Object name/title (must already exist).
491+
request: FastAPI request (body contains NPZ archive bytes).
492+
493+
Returns:
494+
Updated object metadata.
495+
"""
496+
try:
497+
body = await request.body()
498+
obj = deserialize_object_from_npz(body)
499+
obj.title = name
500+
adapter.set_object(name, obj)
501+
502+
# Return updated metadata
503+
updated = adapter.get_object(name)
504+
meta = object_to_metadata(updated, name)
505+
return ObjectMetadata(**meta)
506+
507+
except KeyError as e:
508+
raise HTTPException(
509+
status_code=status.HTTP_404_NOT_FOUND,
510+
detail=f"Object '{name}' not found",
511+
) from e
512+
except ValueError as e:
513+
raise HTTPException(
514+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
515+
detail=f"Invalid NPZ format: {e}",
516+
) from e
517+
except Exception as e: # pylint: disable=broad-exception-caught
518+
raise HTTPException(
519+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
520+
detail=f"Error updating object '{name}': {e}",
521+
) from e
522+
523+
464524
# =============================================================================
465525
# Computation endpoints
466526
# =============================================================================

doc/locale/fr/LC_MESSAGES/features/advanced/proxy.po

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,18 @@ msgstr ""
441441
msgid "Panel name (valid values: \"signal\", \"image\", \"macro\"))"
442442
msgstr ""
443443

444+
msgid "Set object data in DataLab."
445+
msgstr ""
446+
447+
msgid "Update an existing object in DataLab with new data from ``obj``. The object is identified by its UUID (which is carried by ``obj`` from a previous :meth:`get_object` call)."
448+
msgstr ""
449+
450+
msgid "Signal or image object (must have the same UUID as an existing object in DataLab)"
451+
msgstr ""
452+
453+
msgid "if no object with matching UUID is found"
454+
msgstr ""
455+
444456
msgid "Set XML-RPC port to connect to."
445457
msgstr ""
446458

doc/locale/fr/LC_MESSAGES/features/advanced/remote.po

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,18 @@ msgstr ""
266266
msgid "if True, set the added object as current"
267267
msgstr ""
268268

269+
msgid "Set object data in DataLab."
270+
msgstr ""
271+
272+
msgid "Update an existing object in DataLab with new data from ``obj``. The object is identified by its UUID (which is carried by ``obj`` from a previous :meth:`get_object` call)."
273+
msgstr ""
274+
275+
msgid "Signal or image object (must have the same UUID as an existing object in DataLab)"
276+
msgstr ""
277+
278+
msgid "if no object with matching UUID is found"
279+
msgstr ""
280+
269281
msgid "Call computation feature ``name``"
270282
msgstr ""
271283

0 commit comments

Comments
 (0)