Skip to content

Commit d733f9c

Browse files
committed
Fix console freeze when loading/saving HDF5 files from internal console
Fixes #275 (cherry picked from commit 08d4012)
1 parent 051f20e commit d733f9c

9 files changed

Lines changed: 356 additions & 5 deletions

File tree

datalab/control/baseproxy.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,41 @@ def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
206206
reset_all: Reset all application data. Defaults to None.
207207
"""
208208

209+
@abc.abstractmethod
210+
def load_h5_workspace(self, h5files: list[str], reset_all: bool = False) -> None:
211+
"""Load native DataLab HDF5 workspace files without any GUI elements.
212+
213+
This method can be safely called from scripts (e.g., internal console,
214+
macros) as it does not create any Qt widgets, dialogs, or progress bars.
215+
216+
.. warning::
217+
218+
This method only supports native DataLab HDF5 files. For importing
219+
arbitrary HDF5 files (non-native), use :meth:`open_h5_files` or
220+
:meth:`import_h5_file` instead.
221+
222+
Args:
223+
h5files: List of native DataLab HDF5 filenames
224+
reset_all: Reset all application data before importing. Defaults to False.
225+
226+
Raises:
227+
ValueError: If a file is not a valid native DataLab HDF5 file
228+
"""
229+
230+
@abc.abstractmethod
231+
def save_h5_workspace(self, filename: str) -> None:
232+
"""Save current workspace to a native DataLab HDF5 file without GUI elements.
233+
234+
This method can be safely called from scripts (e.g., internal console,
235+
macros) as it does not create any Qt widgets, dialogs, or progress bars.
236+
237+
Args:
238+
filename: HDF5 filename to save to
239+
240+
Raises:
241+
IOError: If file cannot be saved
242+
"""
243+
209244
@abc.abstractmethod
210245
def load_from_files(self, filenames: list[str]) -> None:
211246
"""Open objects from files in current panel (signals/images).

datalab/control/proxy.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,38 @@ def add_annotations_from_items(
300300
"""
301301
self._datalab.add_annotations_from_items(items, refresh_plot, panel)
302302

303+
def load_h5_workspace(
304+
self, h5files: list[str] | str, reset_all: bool = True
305+
) -> None:
306+
"""Load HDF5 workspace files without showing file dialog.
307+
308+
This method loads one or more DataLab native HDF5 files directly, bypassing
309+
the file dialog. It is safe to call from the internal console or any context
310+
where Qt dialogs would cause threading issues.
311+
312+
Args:
313+
h5files: Path(s) to HDF5 file(s). Can be a single path string or a list
314+
of paths
315+
reset_all: If True (default), reset workspace before loading.
316+
If False, append to existing workspace
317+
318+
Raises:
319+
ValueError: if file is not a DataLab native HDF5 file
320+
"""
321+
self._datalab.load_h5_workspace(h5files, reset_all)
322+
323+
def save_h5_workspace(self, filename: str) -> None:
324+
"""Save workspace to HDF5 file without showing file dialog.
325+
326+
This method saves the current workspace to a DataLab native HDF5 file
327+
directly, bypassing the file dialog. It is safe to call from the internal
328+
console or any context where Qt dialogs would cause threading issues.
329+
330+
Args:
331+
filename: Path to the output HDF5 file
332+
"""
333+
self._datalab.save_h5_workspace(filename)
334+
303335

304336
@contextmanager
305337
def proxy_context(what: str) -> Generator[LocalProxy | RemoteProxy, None, None]:

datalab/control/remote.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ class RemoteServer(QC.QThread):
9999
SIG_SAVE_TO_H5 = QC.Signal(str)
100100
SIG_OPEN_H5 = QC.Signal(list, bool, bool)
101101
SIG_IMPORT_H5 = QC.Signal(str, bool)
102+
SIG_LOAD_H5_WORKSPACE = QC.Signal(list, bool)
103+
SIG_SAVE_H5_WORKSPACE = QC.Signal(str)
102104
SIG_CALC = QC.Signal(str, object)
103105
SIG_RUN_MACRO = QC.Signal(str)
104106
SIG_STOP_MACRO = QC.Signal(str)
@@ -133,6 +135,8 @@ def __init__(self, win: DLMainWindow) -> None:
133135
self.SIG_SAVE_TO_H5.connect(win.save_to_h5_file)
134136
self.SIG_OPEN_H5.connect(win.open_h5_files)
135137
self.SIG_IMPORT_H5.connect(win.import_h5_file)
138+
self.SIG_LOAD_H5_WORKSPACE.connect(win.load_h5_workspace)
139+
self.SIG_SAVE_H5_WORKSPACE.connect(win.save_h5_workspace)
136140
self.SIG_CALC.connect(win.calc)
137141
self.SIG_RUN_MACRO.connect(win.run_macro)
138142
self.SIG_STOP_MACRO.connect(win.stop_macro)
@@ -297,6 +301,37 @@ def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
297301
reset_all = False if reset_all is None else reset_all
298302
self.SIG_IMPORT_H5.emit(filename, reset_all)
299303

304+
@remote_call
305+
def load_h5_workspace(self, h5files: list[str], reset_all: bool = False) -> None:
306+
"""Load native DataLab HDF5 workspace files without any GUI elements.
307+
308+
This method can be safely called from scripts as it does not create
309+
any Qt widgets, dialogs, or progress bars.
310+
311+
Args:
312+
h5files: List of native DataLab HDF5 filenames
313+
reset_all: Reset all application data before importing. Defaults to False.
314+
315+
Raises:
316+
ValueError: If a file is not a valid native DataLab HDF5 file
317+
"""
318+
self.SIG_LOAD_H5_WORKSPACE.emit(h5files, reset_all)
319+
320+
@remote_call
321+
def save_h5_workspace(self, filename: str) -> None:
322+
"""Save current workspace to a native DataLab HDF5 file without GUI elements.
323+
324+
This method can be safely called from scripts as it does not create
325+
any Qt widgets, dialogs, or progress bars.
326+
327+
Args:
328+
filename: HDF5 filename to save to
329+
330+
Raises:
331+
IOError: If file cannot be saved
332+
"""
333+
self.SIG_SAVE_H5_WORKSPACE.emit(filename)
334+
300335
@remote_call
301336
def load_from_files(self, filenames: list[str]) -> None:
302337
"""Open objects from files in current panel (signals/images).

datalab/gui/h5io.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,31 @@ def save_file(self, filename: str) -> None:
5454
panel.serialize_to_hdf5(writer)
5555
writer.close()
5656

57+
def open_file_headless(self, filename: str, reset_all: bool) -> bool:
58+
"""Open native DataLab HDF5 file without any GUI elements.
59+
60+
This method can be safely called from any thread (e.g., the console thread)
61+
as it does not create any Qt widgets or dialogs.
62+
63+
Args:
64+
filename: HDF5 filename
65+
reset_all: Reset all application data before importing
66+
67+
Returns:
68+
True if file was successfully opened as a native DataLab file,
69+
False if the file format is not compatible (KeyError was raised)
70+
"""
71+
try:
72+
reader = NativeH5Reader(filename)
73+
if reset_all:
74+
self.mainwindow.reset_all()
75+
for panel in self.mainwindow.panels:
76+
panel.deserialize_from_hdf5(reader, reset_all)
77+
reader.close()
78+
return True
79+
except KeyError:
80+
return False
81+
5782
def open_file(self, filename: str, import_all: bool, reset_all: bool) -> None:
5883
"""Open HDF5 file"""
5984
progress = None

datalab/gui/main.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,10 @@ def set_modified(self, state: bool = True) -> None:
14741474
title += f" [{datalab.__version__}]"
14751475
self.setWindowTitle(title)
14761476

1477+
def is_modified(self) -> bool:
1478+
"""Return True if mainwindow is modified"""
1479+
return self.__is_modified
1480+
14771481
def __add_dockwidget(self, child, title: str) -> QW.QDockWidget:
14781482
"""Add QDockWidget and toggleViewAction"""
14791483
dockwidget, location = child.create_dockwidget(title)
@@ -1683,9 +1687,7 @@ def save_to_h5_file(self, filename=None) -> None:
16831687
if not filename:
16841688
return
16851689
with qth.qt_try_loadsave_file(self, filename, "save"):
1686-
filename = self.__check_h5file(filename, "save")
1687-
self.h5inputoutput.save_file(filename)
1688-
self.set_modified(False)
1690+
self.save_h5_workspace(filename)
16891691

16901692
@remote_controlled
16911693
def open_h5_files(
@@ -1791,6 +1793,59 @@ def browse_h5_files(self, filenames: list[str], reset_all: bool) -> None:
17911793
self.__check_h5file(filename, "load")
17921794
self.h5inputoutput.import_files(filenames, False, reset_all)
17931795

1796+
@remote_controlled
1797+
def load_h5_workspace(self, h5files: list[str], reset_all: bool = False) -> None:
1798+
"""Load native DataLab HDF5 workspace files without any GUI elements.
1799+
1800+
This method can be safely called from the internal console as it does not
1801+
create any Qt widgets, dialogs, or progress bars. It is designed for
1802+
programmatic use when loading DataLab workspace files.
1803+
1804+
.. warning::
1805+
1806+
This method only supports native DataLab HDF5 files. For importing
1807+
arbitrary HDF5 files (non-native), use the GUI menu or macros with
1808+
:class:`datalab.control.proxy.RemoteProxy`.
1809+
1810+
Args:
1811+
h5files: List of native DataLab HDF5 filenames
1812+
reset_all: Reset all application data before importing. Defaults to False.
1813+
1814+
Raises:
1815+
ValueError: If a file is not a valid native DataLab HDF5 file
1816+
"""
1817+
for idx, filename in enumerate(h5files):
1818+
filename = self.__check_h5file(filename, "load")
1819+
success = self.h5inputoutput.open_file_headless(
1820+
filename, reset_all=(reset_all and idx == 0)
1821+
)
1822+
if not success:
1823+
raise ValueError(
1824+
f"File '{filename}' is not a native DataLab HDF5 file. "
1825+
f"Use the GUI menu or a macro with RemoteProxy to import "
1826+
f"arbitrary HDF5 files."
1827+
)
1828+
# Refresh panel trees after loading
1829+
self.repopulate_panel_trees()
1830+
1831+
@remote_controlled
1832+
def save_h5_workspace(self, filename: str) -> None:
1833+
"""Save current workspace to a native DataLab HDF5 file without GUI elements.
1834+
1835+
This method can be safely called from the internal console as it does not
1836+
create any Qt widgets, dialogs, or progress bars. It is designed for
1837+
programmatic use when saving DataLab workspace files.
1838+
1839+
Args:
1840+
filename: HDF5 filename to save to
1841+
1842+
Raises:
1843+
IOError: If file cannot be saved
1844+
"""
1845+
filename = self.__check_h5file(filename, "save")
1846+
self.h5inputoutput.save_file(filename)
1847+
self.set_modified(False)
1848+
17941849
@remote_controlled
17951850
def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
17961851
"""Import HDF5 file into DataLab
@@ -2151,7 +2206,7 @@ def close_properly(self) -> bool:
21512206
Returns:
21522207
True if closed properly, False otherwise
21532208
"""
2154-
if not env.execenv.unattended and self.__is_modified:
2209+
if not env.execenv.unattended and self.is_modified():
21552210
answer = QW.QMessageBox.warning(
21562211
self,
21572212
_("Quit"),
@@ -2163,7 +2218,7 @@ def close_properly(self) -> bool:
21632218
)
21642219
if answer == QW.QMessageBox.Yes:
21652220
self.save_to_h5_file()
2166-
if self.__is_modified:
2221+
if self.is_modified():
21672222
return False
21682223
elif answer == QW.QMessageBox.Cancel:
21692224
return False

datalab/tests/features/control/remoteclient_unit.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ def multiple_commands(remote: RemoteProxy):
4848
remote.reset_all()
4949
remote.open_h5_files([fname], True, False)
5050
remote.import_h5_file(fname, True)
51+
52+
# Test new headless workspace API methods (Issue #275)
53+
fname_workspace = osp.join(tmpdir, "workspace_test.h5")
54+
remote.save_h5_workspace(fname_workspace)
55+
assert osp.exists(fname_workspace), "Workspace file was not created"
56+
remote.reset_all()
57+
remote.load_h5_workspace([fname_workspace], reset_all=True)
58+
# Verify objects were restored
59+
assert len(remote.get_object_titles()) > 0, "No objects after load_h5_workspace"
60+
5161
remote.set_current_panel("signal")
5262
assert remote.get_current_panel() == "signal"
5363
remote.calc("log10")

0 commit comments

Comments
 (0)