From 1b0b0b9401622cb96cab9c113469b499225dc708 Mon Sep 17 00:00:00 2001 From: Sergey Yaroslavtsev Date: Fri, 24 Jul 2026 17:18:41 +0200 Subject: [PATCH 1/5] close instance on close file; catch error and notify the user --- src/PyMca5/PyMcaCore/NexusDataSource.py | 9 +++++ src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py | 12 ++++++- src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py | 14 ++++++++ src/PyMca5/PyMcaGui/pymca/QDispatcher.py | 39 ++++++++++++--------- 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/PyMca5/PyMcaCore/NexusDataSource.py b/src/PyMca5/PyMcaCore/NexusDataSource.py index d0835655e..b75025898 100644 --- a/src/PyMca5/PyMcaCore/NexusDataSource.py +++ b/src/PyMca5/PyMcaCore/NexusDataSource.py @@ -227,6 +227,15 @@ def refresh(self): phynxInstance._sourceName = pattern self.__lastKeyInfo = {} + def close(self): + """Close every open HDF5 handle held by this source.""" + for instance in self._sourceObjectList: + try: + instance.close() + except Exception as e: + _logger.debug("Error closing HDF5 source: %s", e) + self._sourceObjectList = [] + def getSourceInfo(self): """ Returns a dictionary with the key "KeyList" (list of all available keys diff --git a/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py b/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py index f327a1143..f668b00be 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py @@ -425,11 +425,14 @@ class FileModel(qt.QAbstractItemModel): """ sigFileUpdated = qt.pyqtSignal(object) sigFileAppended = qt.pyqtSignal(object) + sigReadError = qt.pyqtSignal(object) def __init__(self, parent=None): qt.QAbstractItemModel.__init__(self, parent) self.rootItem = RootItem(['File/Group/Dataset', 'Description', 'Shape', 'DType']) self._idMap = {qt.QModelIndex().internalId(): self.rootItem} + # warn only once per instance + self._readErrorReported = False def sort(self, column, order): #print("FileModel sort called with ", column, order) @@ -572,7 +575,14 @@ def parent(self, index): return self.createIndex(parent.row, 0, parent) def rowCount(self, index): - return len(self.getProxyFromIndex(index)) + try: + # the `len(self.children)` can fail. + return len(self.getProxyFromIndex(index)) + except Exception: + if not self._readErrorReported: + self._readErrorReported = True + self.sigReadError.emit({"event": "readError"}) + return 0 def openFile(self, filename, weakreference=False): gc.collect() diff --git a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py index a9425e10d..8ff178420 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py @@ -492,6 +492,7 @@ def dataSourceDestroyed(weakrefReference): # stays visible and the tree does not collapse. try: newModel = HDF5Widget.FileModel() + newModel.sigReadError.connect(self._hdf5ReadError) for source in self.data._sourceObjectList: newModel.appendPhynxFile(source, weakreference=True) self._modelDict[ref] = newModel @@ -515,6 +516,19 @@ def dataSourceDestroyed(weakrefReference): if hasattr(self.hdf5Widget, "expandToDepth"): self.hdf5Widget.expandToDepth(0) + def _hdf5ReadError(self, ddict): + def _show(): + msg = qt.QMessageBox(self) + msg.setIcon(qt.QMessageBox.Warning) + msg.setWindowTitle("Cannot read HDF5 file") + msg.setText("The selected file could not be read.") + msg.setInformativeText( + "It may be being written now. Refresh (F5) it later; " + "if that does not help, close and open the file again.") + msg.exec() + + qt.QTimer.singleShot(0, _show) + def _autoRefreshDatasets(self, source=None, moveToLastSlice=True): """ Auto-refresh: re-read datasets and re-plot without re-building the tree. diff --git a/src/PyMca5/PyMcaGui/pymca/QDispatcher.py b/src/PyMca5/PyMcaGui/pymca/QDispatcher.py index 244693dce..f8ba25168 100644 --- a/src/PyMca5/PyMcaGui/pymca/QDispatcher.py +++ b/src/PyMca5/PyMcaGui/pymca/QDispatcher.py @@ -365,23 +365,30 @@ def _sourceSelectorSlot(self, ddict): if not found: _logger.debug("WARNING: source not found") return - sourceType = source.sourceType - del self.sourceList[self.sourceList.index(source)] - for source in self.sourceList: - if sourceType == source.sourceType: + closedSource = source + sourceType = closedSource.sourceType + del self.sourceList[self.sourceList.index(closedSource)] + try: + for source in self.sourceList: + if sourceType == source.sourceType: + self.selectorWidget[sourceType].setDataSource(source) + self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType]) + return + #there is no other selection of that type + if len(self.sourceList): + source = self.sourceList[0] + sourceType = source.sourceType self.selectorWidget[sourceType].setDataSource(source) - self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType]) - return - #there is no other selection of that type - if len(self.sourceList): - source = self.sourceList[0] - sourceType = source.sourceType - self.selectorWidget[sourceType].setDataSource(source) - else: - self.selectorWidget[sourceType].setDataSource(None) - self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType]) - elif ddict["event"] == "SourceClosed": - _logger.debug("not implemented yet") + else: + self.selectorWidget[sourceType].setDataSource(None) + self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType]) + finally: + # Without this the file stays "open" in the process + # and could not be reopened until PyMca restarted. + try: + closedSource.close() + except Exception as e: + _logger.debug("Error closing source: %s", e) def _selectionUpdatedSlot(self, ddict): _logger.debug("_selectionUpdatedSlot(self, dict=%s)", ddict) From f1b38684e90a5032eaeb54e4c773d0d33d459c0d Mon Sep 17 00:00:00 2001 From: Sergey Yaroslavtsev Date: Mon, 27 Jul 2026 12:00:19 +0200 Subject: [PATCH 2/5] error to failed --- src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py | 4 ++-- src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py b/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py index f668b00be..a83cf23a9 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py @@ -425,7 +425,7 @@ class FileModel(qt.QAbstractItemModel): """ sigFileUpdated = qt.pyqtSignal(object) sigFileAppended = qt.pyqtSignal(object) - sigReadError = qt.pyqtSignal(object) + sigReadFailed = qt.pyqtSignal(object) def __init__(self, parent=None): qt.QAbstractItemModel.__init__(self, parent) @@ -581,7 +581,7 @@ def rowCount(self, index): except Exception: if not self._readErrorReported: self._readErrorReported = True - self.sigReadError.emit({"event": "readError"}) + self.sigReadFailed.emit({"event": "readError"}) return 0 def openFile(self, filename, weakreference=False): diff --git a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py index 8ff178420..3af68e54f 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py @@ -492,7 +492,7 @@ def dataSourceDestroyed(weakrefReference): # stays visible and the tree does not collapse. try: newModel = HDF5Widget.FileModel() - newModel.sigReadError.connect(self._hdf5ReadError) + newModel.sigReadFailed.connect(self._hdf5ReadFailed) for source in self.data._sourceObjectList: newModel.appendPhynxFile(source, weakreference=True) self._modelDict[ref] = newModel @@ -516,7 +516,7 @@ def dataSourceDestroyed(weakrefReference): if hasattr(self.hdf5Widget, "expandToDepth"): self.hdf5Widget.expandToDepth(0) - def _hdf5ReadError(self, ddict): + def _hdf5ReadFailed(self, ddict): def _show(): msg = qt.QMessageBox(self) msg.setIcon(qt.QMessageBox.Warning) From 0d3a3c0d87cdaaa1aafb5a076b5716345585dd86 Mon Sep 17 00:00:00 2001 From: Sergey Yaroslavtsev Date: Mon, 27 Jul 2026 15:51:45 +0200 Subject: [PATCH 3/5] open+flag instead of QTimer --- src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py index 3af68e54f..691f9dd7f 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py @@ -517,17 +517,15 @@ def dataSourceDestroyed(weakrefReference): self.hdf5Widget.expandToDepth(0) def _hdf5ReadFailed(self, ddict): - def _show(): - msg = qt.QMessageBox(self) - msg.setIcon(qt.QMessageBox.Warning) - msg.setWindowTitle("Cannot read HDF5 file") - msg.setText("The selected file could not be read.") - msg.setInformativeText( - "It may be being written now. Refresh (F5) it later; " - "if that does not help, close and open the file again.") - msg.exec() - - qt.QTimer.singleShot(0, _show) + msg = qt.QMessageBox(self) + msg.setIcon(qt.QMessageBox.Warning) + msg.setWindowTitle("Cannot read HDF5 file") + msg.setText("The selected file could not be read.") + msg.setInformativeText( + "It may be being written now. Refresh (F5) it later; " + "if that does not help, close and open the file again.") + msg.setAttribute(qt.Qt.WA_DeleteOnClose) + msg.open() def _autoRefreshDatasets(self, source=None, moveToLastSlice=True): """ From 4422c37cd2c8d81df4a47a416d70ffe5f8f2b142 Mon Sep 17 00:00:00 2001 From: Sergey Yaroslavtsev Date: Tue, 4 Aug 2026 21:18:02 +0200 Subject: [PATCH 4/5] collapse on fail so reexpand will fail again --- src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py | 4 ++-- src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py b/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py index a83cf23a9..e738552ef 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py @@ -431,7 +431,7 @@ def __init__(self, parent=None): qt.QAbstractItemModel.__init__(self, parent) self.rootItem = RootItem(['File/Group/Dataset', 'Description', 'Shape', 'DType']) self._idMap = {qt.QModelIndex().internalId(): self.rootItem} - # warn only once per instance + # to warn only once about read error, can be reset intentionally self._readErrorReported = False def sort(self, column, order): @@ -581,7 +581,7 @@ def rowCount(self, index): except Exception: if not self._readErrorReported: self._readErrorReported = True - self.sigReadFailed.emit({"event": "readError"}) + self.sigReadFailed.emit({"event": "readError", "index": index}) return 0 def openFile(self, filename, weakreference=False): diff --git a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py index 691f9dd7f..f51f31a9f 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py @@ -517,6 +517,19 @@ def dataSourceDestroyed(weakrefReference): self.hdf5Widget.expandToDepth(0) def _hdf5ReadFailed(self, ddict): + self._recoverFromReadError(ddict.get("index")) + self._showReadFailedMessage() + + def _recoverFromReadError(self, index): + model = self.hdf5Widget.model() + # Collapse the node that failed to be read + if index is not None and index.isValid(): + self.hdf5Widget.collapse(index) + # A later failed expansion should warn again + if hasattr(model, "_readErrorReported"): + model._readErrorReported = False + + def _showReadFailedMessage(self): msg = qt.QMessageBox(self) msg.setIcon(qt.QMessageBox.Warning) msg.setWindowTitle("Cannot read HDF5 file") @@ -524,6 +537,8 @@ def _hdf5ReadFailed(self, ddict): msg.setInformativeText( "It may be being written now. Refresh (F5) it later; " "if that does not help, close and open the file again.") + # `open()` (not `exec()`) blocks the window but not the code + # `WA_DeleteOnClose` to delete itself on close (to avoid a leak) msg.setAttribute(qt.Qt.WA_DeleteOnClose) msg.open() From 97635871475bf7831f76ea5899d82a92f0d064fd Mon Sep 17 00:00:00 2001 From: Sergey Yaroslavtsev Date: Tue, 4 Aug 2026 21:33:45 +0200 Subject: [PATCH 5/5] clean the message --- src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py index f51f31a9f..044376925 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py @@ -535,8 +535,8 @@ def _showReadFailedMessage(self): msg.setWindowTitle("Cannot read HDF5 file") msg.setText("The selected file could not be read.") msg.setInformativeText( - "It may be being written now. Refresh (F5) it later; " - "if that does not help, close and open the file again.") + "It may be being written now. Try again later." \ + "If it is stuck, try to refresh (F5) or close and open the file again.") # `open()` (not `exec()`) blocks the window but not the code # `WA_DeleteOnClose` to delete itself on close (to avoid a leak) msg.setAttribute(qt.Qt.WA_DeleteOnClose)