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
36 changes: 36 additions & 0 deletions datalab/tests/webapi_extra_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =============================================================================
Expand Down
15 changes: 11 additions & 4 deletions datalab/webapi/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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":
Expand All @@ -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.
Expand Down
Loading