diff --git a/datalab/tests/webapi_extra_test.py b/datalab/tests/webapi_extra_test.py index b8e7093ab..9fd8e96f8 100644 --- a/datalab/tests/webapi_extra_test.py +++ b/datalab/tests/webapi_extra_test.py @@ -145,6 +145,42 @@ def test_workspace_adapter_resolves_uuid_and_rejects_ambiguous_titles() -> None: image_panel.SIG_OBJECT_REMOVED.emit.assert_called_once_with() +def test_workspace_adapter_uploaded_copy_gets_a_new_identity() -> None: + """An object downloaded then uploaded under another name is a new object. + + Regression: the copy carried the UUID of the original object, the panel + refused it and showed the error in a modal dialog, so that the request hung + until the main-thread timeout (DataLab-Kernel ``rename``). + """ + signal = _make_signal("Original", n=10) + signal_panel = make_panel([signal]) + + def add_object(obj: DataObject) -> None: + if get_uuid(obj) in [get_uuid(other) for other in signal_panel.objmodel]: + raise ValueError("Object already in panel") + signal_panel.objmodel.append(obj) + + signal_panel._add_object = add_object # pylint: disable=protected-access + adapter = WorkspaceAdapter( + SimpleNamespace(signalpanel=signal_panel, imagepanel=make_panel([])) + ) + copy = adapter.get_object(get_uuid(signal)) + assert get_uuid(copy) == get_uuid(signal) + copy.title = "Renamed" + adapter.add_object(copy) + assert len(signal_panel.objmodel) == 2 + assert get_uuid(signal_panel.objmodel[1]) != get_uuid(signal) + assert get_uuid(signal) == get_uuid(signal_panel.objmodel[0]) + + # Errors of the panel are raised to the caller, not shown in a dialog + def failing_add(_obj: DataObject) -> None: + raise ValueError("refused") + + signal_panel._add_object = failing_add # pylint: disable=protected-access + with pytest.raises(ValueError, match="refused"): + adapter.add_object(_make_signal("Other")) + + # ============================================================================= # Serialization helpers — _to_python_scalar # ============================================================================= diff --git a/datalab/webapi/adapter.py b/datalab/webapi/adapter.py index de3a156d7..16431c0f6 100644 --- a/datalab/webapi/adapter.py +++ b/datalab/webapi/adapter.py @@ -34,7 +34,7 @@ from qtpy.QtCore import QCoreApplication, QObject, QThread, Signal, Slot -from datalab.objectmodel import UUID_REGEX, get_uuid +from datalab.objectmodel import UUID_REGEX, get_uuid, set_uuid if TYPE_CHECKING: from sigima.objects import ImageObj, SignalObj @@ -342,7 +342,13 @@ def add_object(self, obj: DataObject, overwrite: bool = False) -> None: self._add_object_sync(obj) def _add_object_sync(self, obj: DataObject) -> None: - """Add object (called from main thread or marshaled via executor).""" + """Add object (called from main thread or marshaled via executor). + + The object is a new one: it gets its own identity, whatever UUID its + metadata carry (e.g. an object downloaded from the API and uploaded + again under another name). Errors are raised to the caller, never shown + in a dialog. + """ obj_type = type(obj).__name__ if obj_type == "SignalObj": @@ -352,8 +358,9 @@ def _add_object_sync(self, obj: DataObject) -> None: else: raise TypeError(f"Unsupported object type: {obj_type}") - # Use executor to run on main thread if necessary - self._executor.run_on_main_thread(lambda: panel.add_object(obj)) + set_uuid(obj) + # pylint: disable-next=protected-access + self._executor.run_on_main_thread(lambda: panel._add_object(obj)) def remove_object(self, name: str) -> None: """Remove an object from the workspace.