diff options
Diffstat (limited to 'silx/gui/hdf5')
-rw-r--r-- | silx/gui/hdf5/Hdf5Formatter.py | 5 | ||||
-rw-r--r-- | silx/gui/hdf5/Hdf5Item.py | 126 | ||||
-rw-r--r-- | silx/gui/hdf5/Hdf5TreeModel.py | 135 | ||||
-rw-r--r-- | silx/gui/hdf5/Hdf5TreeView.py | 113 | ||||
-rw-r--r-- | silx/gui/hdf5/NexusSortFilterProxyModel.py | 20 | ||||
-rw-r--r-- | silx/gui/hdf5/_utils.py | 51 | ||||
-rw-r--r-- | silx/gui/hdf5/test/test_hdf5.py | 99 |
7 files changed, 341 insertions, 208 deletions
diff --git a/silx/gui/hdf5/Hdf5Formatter.py b/silx/gui/hdf5/Hdf5Formatter.py index 3a4c1c1..0e3697f 100644 --- a/silx/gui/hdf5/Hdf5Formatter.py +++ b/silx/gui/hdf5/Hdf5Formatter.py @@ -27,7 +27,7 @@ text.""" __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "27/09/2017" +__date__ = "23/01/2018" import numpy from silx.third_party import six @@ -153,7 +153,8 @@ class Hdf5Formatter(qt.QObject): if not full: return "compound" else: - compound = [d[0] for d in dtype.fields.values()] + fields = sorted(dtype.fields.items(), key=lambda e: e[1][1]) + compound = [d[1][0] for d in fields] compound = [self.humanReadableDType(d) for d in compound] return "compound(%s)" % ", ".join(compound) elif numpy.issubdtype(dtype, numpy.integer): diff --git a/silx/gui/hdf5/Hdf5Item.py b/silx/gui/hdf5/Hdf5Item.py index f131f61..9804907 100644 --- a/silx/gui/hdf5/Hdf5Item.py +++ b/silx/gui/hdf5/Hdf5Item.py @@ -25,7 +25,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "26/09/2017" +__date__ = "10/10/2017" import logging @@ -40,12 +40,6 @@ from ..hdf5.Hdf5Formatter import Hdf5Formatter _logger = logging.getLogger(__name__) -try: - import h5py -except ImportError as e: - _logger.error("Module %s requires h5py", __name__) - raise e - _formatter = TextFormatter() _hdf5Formatter = Hdf5Formatter(textFormatter=_formatter) # FIXME: The formatter should be an attribute of the Hdf5Model @@ -57,15 +51,15 @@ class Hdf5Item(Hdf5Node): tree structure. """ - def __init__(self, text, obj, parent, key=None, h5pyClass=None, linkClass=None, populateAll=False): + def __init__(self, text, obj, parent, key=None, h5Class=None, linkClass=None, populateAll=False): """ :param str text: text displayed - :param object obj: Pointer to h5py data. See the `obj` attribute. + :param object obj: Pointer to a h5py-link object. See the `obj` attribute. """ self.__obj = obj self.__key = key - self.__h5pyClass = h5pyClass - self.__isBroken = obj is None and h5pyClass is None + self.__h5Class = h5Class + self.__isBroken = obj is None and h5Class is None self.__error = None self.__text = text self.__linkClass = linkClass @@ -74,7 +68,7 @@ class Hdf5Item(Hdf5Node): @property def obj(self): if self.__key: - self.__initH5pyObject() + self.__initH5Object() return self.__obj @property @@ -82,6 +76,20 @@ class Hdf5Item(Hdf5Node): return self.__text @property + def h5Class(self): + """Returns the class of the stored object. + + When the object is in lazy loading, this method should be able to + return the type of the futrue loaded object. It allows to delay the + real load of the object. + + :rtype: silx.io.utils.H5Type + """ + if self.__h5Class is None and self.obj is not None: + self.__h5Class = silx.io.utils.get_h5_class(self.obj) + return self.__h5Class + + @property def h5pyClass(self): """Returns the class of the stored object. @@ -91,15 +99,14 @@ class Hdf5Item(Hdf5Node): :rtype: h5py.File or h5py.Dataset or h5py.Group """ - if self.__h5pyClass is None and self.obj is not None: - self.__h5pyClass = silx.io.utils.get_h5py_class(self.obj) - return self.__h5pyClass + type_ = self.h5Class + return silx.io.utils.h5type_to_h5py_class(type_) @property def linkClass(self): """Returns the link class object of this node - :type: h5py.SoftLink or h5py.HardLink or h5py.ExternalLink or None + :rtype: H5Type """ return self.__linkClass @@ -109,16 +116,16 @@ class Hdf5Item(Hdf5Node): :rtype: bool """ - if self.h5pyClass is None: + if self.h5Class is None: return False - return issubclass(self.h5pyClass, h5py.Group) + return self.h5Class in [silx.io.utils.H5Type.GROUP, silx.io.utils.H5Type.FILE] def isBrokenObj(self): """Returns true if the stored HDF5 object is broken. - The stored object is then an h5py link (external or not) which point - to nowhere (tbhe external file is not here, the expected dataset is - still not on the file...) + The stored object is then an h5py-like link (external or not) which + point to nowhere (tbhe external file is not here, the expected + dataset is still not on the file...) :rtype: bool """ @@ -137,7 +144,7 @@ class Hdf5Item(Hdf5Node): return len(self.obj) return 0 - def __initH5pyObject(self): + def __initH5Object(self): """Lazy load of the HDF5 node. It is reached from the parent node with the key of the node.""" parent_obj = self.parent.obj @@ -145,7 +152,9 @@ class Hdf5Item(Hdf5Node): try: obj = parent_obj.get(self.__key) except Exception as e: - _logger.debug("Internal h5py error", exc_info=True) + lib_name = self.obj.__class__.__module__.split(".")[0] + _logger.debug("Internal %s error", lib_name, exc_info=True) + _logger.debug("Backtrace", exc_info=True) try: self.__obj = parent_obj.get(self.__key, getlink=True) except Exception: @@ -168,9 +177,11 @@ class Hdf5Item(Hdf5Node): if not hasattr(self.__obj, "file"): self.__obj.file = parent_obj.file - if isinstance(self.__obj, h5py.ExternalLink): + class_ = silx.io.utils.get_h5_class(self.__obj) + + if class_ == silx.io.utils.H5Type.EXTERNAL_LINK: message = "External link broken. Path %s::%s does not exist" % (self.__obj.filename, self.__obj.path) - elif isinstance(self.__obj, h5py.SoftLink): + elif class_ == silx.io.utils.H5Type.SOFT_LINK: message = "Soft link broken. Path %s does not exist" % (self.__obj.path) else: name = self.obj.__class__.__name__.split(".")[-1].capitalize() @@ -204,14 +215,25 @@ class Hdf5Item(Hdf5Node): try: class_ = self.obj.get(name, getclass=True) link = self.obj.get(name, getclass=True, getlink=True) - except Exception as e: - _logger.warn("Internal h5py error", exc_info=True) + link = silx.io.utils.get_h5_class(class_=link) + except Exception: + lib_name = self.obj.__class__.__module__.split(".")[0] + _logger.warning("Internal %s error", lib_name, exc_info=True) + _logger.debug("Backtrace", exc_info=True) class_ = None try: link = self.obj.get(name, getclass=True, getlink=True) - except Exception as e: - link = h5py.HardLink - item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5pyClass=class_, linkClass=link) + link = silx.io.utils.get_h5_class(class_=link) + except Exception: + _logger.debug("Backtrace", exc_info=True) + link = silx.io.utils.H5Type.HARD_LINK + + h5class = None + if class_ is not None: + h5class = silx.io.utils.get_h5_class(class_=class_) + if h5class is None: + _logger.error("Class %s unsupported", class_) + item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5Class=h5class, linkClass=link) self.appendChild(item) def hasChildren(self): @@ -234,16 +256,16 @@ class Hdf5Item(Hdf5Node): if self.__isBroken: icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical) return icon - class_ = self.h5pyClass - if issubclass(class_, h5py.File): + class_ = self.h5Class + if class_ == silx.io.utils.H5Type.FILE: return style.standardIcon(qt.QStyle.SP_FileIcon) - elif issubclass(class_, h5py.Group): + elif class_ == silx.io.utils.H5Type.GROUP: return style.standardIcon(qt.QStyle.SP_DirIcon) - elif issubclass(class_, h5py.SoftLink): + elif class_ == silx.io.utils.H5Type.SOFT_LINK: return style.standardIcon(qt.QStyle.SP_DirLinkIcon) - elif issubclass(class_, h5py.ExternalLink): + elif class_ == silx.io.utils.H5Type.EXTERNAL_LINK: return style.standardIcon(qt.QStyle.SP_FileLinkIcon) - elif issubclass(class_, h5py.Dataset): + elif class_ == silx.io.utils.H5Type.DATASET: if obj.shape is None: name = "item-none" elif len(obj.shape) < 4: @@ -262,28 +284,28 @@ class Hdf5Item(Hdf5Node): """ attributeDict = collections.OrderedDict() - if issubclass(self.h5pyClass, h5py.Dataset): + if self.h5Class == silx.io.utils.H5Type.DATASET: attributeDict["#Title"] = "HDF5 Dataset" attributeDict["Name"] = self.basename attributeDict["Path"] = self.obj.name attributeDict["Shape"] = self._getFormatter().humanReadableShape(self.obj) attributeDict["Value"] = self._getFormatter().humanReadableValue(self.obj) attributeDict["Data type"] = self._getFormatter().humanReadableType(self.obj, full=True) - elif issubclass(self.h5pyClass, h5py.Group): + elif self.h5Class == silx.io.utils.H5Type.GROUP: attributeDict["#Title"] = "HDF5 Group" attributeDict["Name"] = self.basename attributeDict["Path"] = self.obj.name - elif issubclass(self.h5pyClass, h5py.File): + elif self.h5Class == silx.io.utils.H5Type.FILE: attributeDict["#Title"] = "HDF5 File" attributeDict["Name"] = self.basename attributeDict["Path"] = "/" - elif isinstance(self.obj, h5py.ExternalLink): + elif self.h5Class == silx.io.utils.H5Type.EXTERNAL_LINK: attributeDict["#Title"] = "HDF5 External Link" attributeDict["Name"] = self.basename attributeDict["Path"] = self.obj.name attributeDict["Linked path"] = self.obj.path attributeDict["Linked file"] = self.obj.filename - elif isinstance(self.obj, h5py.SoftLink): + elif self.h5Class == silx.io.utils.H5Type.SOFT_LINK: attributeDict["#Title"] = "HDF5 Soft Link" attributeDict["Name"] = self.basename attributeDict["Path"] = self.obj.name @@ -331,8 +353,8 @@ class Hdf5Item(Hdf5Node): if role == qt.Qt.DisplayRole: if self.__error is not None: return "" - class_ = self.h5pyClass - if issubclass(class_, h5py.Dataset): + class_ = self.h5Class + if class_ == silx.io.utils.H5Type.DATASET: text = self._getFormatter().humanReadableType(self.obj) else: text = "" @@ -349,8 +371,8 @@ class Hdf5Item(Hdf5Node): if role == qt.Qt.DisplayRole: if self.__error is not None: return "" - class_ = self.h5pyClass - if not issubclass(class_, h5py.Dataset): + class_ = self.h5Class + if class_ != silx.io.utils.H5Type.DATASET: return "" return self._getFormatter().humanReadableShape(self.obj) return None @@ -364,7 +386,7 @@ class Hdf5Item(Hdf5Node): if role == qt.Qt.DisplayRole: if self.__error is not None: return "" - if not issubclass(self.h5pyClass, h5py.Dataset): + if self.h5Class != silx.io.utils.H5Type.DATASET: return "" return self._getFormatter().humanReadableValue(self.obj) return None @@ -387,7 +409,7 @@ class Hdf5Item(Hdf5Node): if role == qt.Qt.ToolTipRole: if self.__error is not None: self.obj # lazy loading of the object - self.__initH5pyObject() + self.__initH5Object() return self.__error if "desc" in self.obj.attrs: text = self.obj.attrs["desc"] @@ -405,11 +427,11 @@ class Hdf5Item(Hdf5Node): if role == qt.Qt.DisplayRole: if self.isBrokenObj(): return "" - class_ = self.h5pyClass + class_ = self.obj.__class__ text = class_.__name__.split(".")[-1] return text if role == qt.Qt.ToolTipRole: - class_ = self.h5pyClass + class_ = self.obj.__class__ if class_ is None: return "" return "Class name: %s" % self.__class__ @@ -430,11 +452,11 @@ class Hdf5Item(Hdf5Node): link = self.linkClass if link is None: return "" - elif link is h5py.ExternalLink: + elif link == silx.io.utils.H5Type.EXTERNAL_LINK: return "External" - elif link is h5py.SoftLink: + elif link == silx.io.utils.H5Type.SOFT_LINK: return "Soft" - elif link is h5py.HardLink: + elif link == silx.io.utils.H5Type.HARD_LINK: return "" else: return link.__name__ diff --git a/silx/gui/hdf5/Hdf5TreeModel.py b/silx/gui/hdf5/Hdf5TreeModel.py index 41fa91c..2d62429 100644 --- a/silx/gui/hdf5/Hdf5TreeModel.py +++ b/silx/gui/hdf5/Hdf5TreeModel.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -25,11 +25,12 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "22/09/2017" +__date__ = "29/11/2017" import os import logging +import functools from .. import qt from .. import icons from .Hdf5Node import Hdf5Node @@ -130,7 +131,6 @@ class LoadingItemRunnable(qt.QRunnable): item = Hdf5Item(text=text, obj=h5obj, parent=oldItem.parent, populateAll=True) return item - @qt.Slot() def run(self): """Process the file loading. The worker is used as holder of the data and the signal. The result is sent as a signal. @@ -237,25 +237,32 @@ class Hdf5TreeModel(qt.QAbstractItemModel): self.__openedFiles = [] """Store the list of files opened by the model itself.""" - # FIXME: It should managed one by one by Hdf5Item itself - - def __del__(self): - self._closeOpened() - s = super(Hdf5TreeModel, self) - if hasattr(s, "__del__"): - # else it fail on Python 3 - s.__del__() + # FIXME: It should be managed one by one by Hdf5Item itself + + # It is not possible to override the QObject destructor nor + # to access to the content of the Python object with the `destroyed` + # signal cause the Python method was already removed with the QWidget, + # while the QObject still exists. + # We use a static method plus explicit references to objects to + # release. The callback do not use any ref to self. + onDestroy = functools.partial(self._closeFileList, self.__openedFiles) + self.destroyed.connect(onDestroy) + + @staticmethod + def _closeFileList(fileList): + """Static method to close explicit references to internal objects.""" + _logger.debug("Clear Hdf5TreeModel") + for obj in fileList: + _logger.debug("Close file %s", obj.filename) + obj.close() + fileList[:] = [] def _closeOpened(self): """Close files which was opened by this model. - This function may be removed in the future. - File are opened by the model when it was inserted using `insertFileAsync`, `insertFile`, `appendFile`.""" - for h5file in self.__openedFiles: - h5file.close() - self.__openedFiles = [] + self._closeFileList(self.__openedFiles) def __updateLoadingItems(self, icon): for i in range(self.__root.childCount()): @@ -283,6 +290,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): self.__root.removeChildAtIndex(row) self.endRemoveRows() if newItem is not None: + rootIndex = qt.QModelIndex() self.__openedFiles.append(newItem.obj) self.beginInsertRows(rootIndex, row, row) self.__root.insertChild(row, newItem) @@ -325,7 +333,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): Returns an object that contains serialized items of data corresponding to the list of indexes specified. - :param list(qt.QModelIndex) indexes: List of indexes + :param List[qt.QModelIndex] indexes: List of indexes :rtype: qt.QMimeData """ if not self.__fileMoveEnabled or len(indexes) == 0: @@ -512,6 +520,16 @@ class Hdf5TreeModel(qt.QAbstractItemModel): def nodeFromIndex(self, index): return index.internalPointer() if index.isValid() else self.__root + def _closeFileIfOwned(self, node): + """"Close the file if it was loaded from a filename or a + drag-and-drop""" + obj = node.obj + for f in self.__openedFiles: + if f in obj: + _logger.debug("Close file %s", obj.filename) + obj.close() + self.__openedFiles.remove(obj) + def synchronizeIndex(self, index): """ Synchronize a file a given its index. @@ -524,9 +542,8 @@ class Hdf5TreeModel(qt.QAbstractItemModel): if node.parent is not self.__root: return - self.removeIndex(index) filename = node.obj.filename - node.obj.close() + self.removeIndex(index) self.insertFileAsync(filename, index.row()) def synchronizeH5pyObject(self, h5pyObject): @@ -555,6 +572,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): node = self.nodeFromIndex(index) if node.parent is not self.__root: return + self._closeFileIfOwned(node) self.beginRemoveRows(qt.QModelIndex(), index.row(), index.row()) self.__root.removeChildAtIndex(index.row()) self.endRemoveRows() @@ -587,6 +605,9 @@ class Hdf5TreeModel(qt.QAbstractItemModel): row = self.__root.childCount() self.insertNode(row, Hdf5Item(text=text, obj=h5pyObject, parent=self.__root)) + def hasPendingOperations(self): + return len(self.__runnerSet) > 0 + def insertFileAsync(self, filename, row=-1): if not os.path.isfile(filename): raise IOError("Filename '%s' must be a file path" % filename) @@ -599,9 +620,9 @@ class Hdf5TreeModel(qt.QAbstractItemModel): # start loading the real one runnable = LoadingItemRunnable(filename, item) runnable.itemReady.connect(self.__itemReady) - self.__runnerSet.add(runnable) runnable.runnerFinished.connect(self.__releaseRunner) - qt.QThreadPool.globalInstance().start(runnable) + self.__runnerSet.add(runnable) + qt.silxGlobalThreadPool().start(runnable) def __releaseRunner(self, runner): self.__runnerSet.remove(runner) @@ -621,3 +642,75 @@ class Hdf5TreeModel(qt.QAbstractItemModel): def appendFile(self, filename): self.insertFile(filename, -1) + + def indexFromH5Object(self, h5Object): + """Returns a model index from an h5py-like object. + + :param object h5Object: An h5py-like object + :rtype: qt.QModelIndex + """ + if h5Object is None: + return qt.QModelIndex() + + filename = h5Object.file.filename + + # Seach for the right roots + rootIndices = [] + for index in range(self.rowCount(qt.QModelIndex())): + index = self.index(index, 0, qt.QModelIndex()) + obj = self.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE) + if obj.file.filename == filename: + # We can have many roots with different subtree of the same + # root + rootIndices.append(index) + + if len(rootIndices) == 0: + # No root found + return qt.QModelIndex() + + path = h5Object.name + "/" + path = path.replace("//", "/") + + # Search for the right node + found = False + foundIndices = [] + for _ in range(1000 * len(rootIndices)): + # Avoid too much iterations, in case of recurssive links + if len(foundIndices) == 0: + if len(rootIndices) == 0: + # Nothing found + break + # Start fron a new root + foundIndices.append(rootIndices.pop(0)) + + obj = self.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE) + p = obj.name + "/" + p = p.replace("//", "/") + if path == p: + found = True + break + + parentIndex = foundIndices[-1] + for index in range(self.rowCount(parentIndex)): + index = self.index(index, 0, parentIndex) + obj = self.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE) + + p = obj.name + "/" + p = p.replace("//", "/") + if path == p: + foundIndices.append(index) + found = True + break + elif path.startswith(p): + foundIndices.append(index) + break + else: + # Nothing found, start again with another root + foundIndices = [] + + if found: + break + + if found: + return foundIndices[-1] + return qt.QModelIndex() diff --git a/silx/gui/hdf5/Hdf5TreeView.py b/silx/gui/hdf5/Hdf5TreeView.py index 0a4198e..78b5c19 100644 --- a/silx/gui/hdf5/Hdf5TreeView.py +++ b/silx/gui/hdf5/Hdf5TreeView.py @@ -25,7 +25,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "20/09/2017" +__date__ = "20/02/2018" import logging @@ -114,12 +114,12 @@ class Hdf5TreeView(qt.QTreeView): callback(event) except KeyboardInterrupt: raise - except: + except Exception: # make sure no user callback crash the application _logger.error("Error while calling callback", exc_info=True) pass - if len(menu.children()) > 0: + if not menu.isEmpty(): for action in actions: menu.addAction(action) menu.popup(self.viewport().mapToGlobal(pos)) @@ -194,6 +194,38 @@ class Hdf5TreeView(qt.QTreeView): continue yield _utils.H5Node(item) + def __intermediateModels(self, index): + """Returns intermediate models from the view model to the + model of the index.""" + models = [] + targetModel = index.model() + model = self.model() + while model is not None: + if model is targetModel: + # found + return models + models.append(model) + if isinstance(model, qt.QAbstractProxyModel): + model = model.sourceModel() + else: + break + raise RuntimeError("Model from the requested index is not reachable from this view") + + def mapToModel(self, index): + """Map an index from any model reachable by the view to an index from + the very first model connected to the view. + + :param qt.QModelIndex index: Index from the Hdf5Tree model + :rtype: qt.QModelIndex + :return: Index from the model connected to the view + """ + if not index.isValid(): + return index + models = self.__intermediateModels(index) + for model in reversed(models): + index = model.mapFromSource(index) + return index + def setSelectedH5Node(self, h5Object): """ Select the specified node of the tree using an h5py node. @@ -203,77 +235,22 @@ class Hdf5TreeView(qt.QTreeView): - If the item is not found, the selection do not change. - A none argument allow to deselect everything - :param h5py.Npde h5Object: The node to select + :param h5py.Node h5Object: The node to select """ if h5Object is None: self.setCurrentIndex(qt.QModelIndex()) return - filename = h5Object.file.filename - - # Seach for the right roots - rootIndices = [] - model = self.model() - for index in range(model.rowCount(qt.QModelIndex())): - index = model.index(index, 0, qt.QModelIndex()) - obj = model.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE) - if obj.file.filename == filename: - # We can have many roots with different subtree of the same - # root - rootIndices.append(index) - - if len(rootIndices) == 0: - # No root found - return - - path = h5Object.name + "/" - path = path.replace("//", "/") - - # Search for the right node - found = False - foundIndices = [] - for _ in range(1000 * len(rootIndices)): - # Avoid too much iterations, in case of recurssive links - if len(foundIndices) == 0: - if len(rootIndices) == 0: - # Nothing found - break - # Start fron a new root - foundIndices.append(rootIndices.pop(0)) - - obj = model.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE) - p = obj.name + "/" - p = p.replace("//", "/") - if path == p: - found = True - break - - parentIndex = foundIndices[-1] - for index in range(model.rowCount(parentIndex)): - index = model.index(index, 0, parentIndex) - obj = model.data(index, Hdf5TreeModel.H5PY_OBJECT_ROLE) - - p = obj.name + "/" - p = p.replace("//", "/") - if path == p: - foundIndices.append(index) - found = True - break - elif path.startswith(p): - foundIndices.append(index) - break - else: - # Nothing found, start again with another root - foundIndices = [] - - if found: - break - - if found: + model = self.findHdf5TreeModel() + index = model.indexFromH5Object(h5Object) + index = self.mapToModel(index) + if index.isValid(): # Update the GUI - for index in foundIndices[:-1]: - self.expand(index) - self.setCurrentIndex(foundIndices[-1]) + i = index + while i.isValid(): + self.expand(i) + i = i.parent() + self.setCurrentIndex(index) def mousePressEvent(self, event): """Override mousePressEvent to provide a consistante compatible API diff --git a/silx/gui/hdf5/NexusSortFilterProxyModel.py b/silx/gui/hdf5/NexusSortFilterProxyModel.py index 49a22d3..9a27968 100644 --- a/silx/gui/hdf5/NexusSortFilterProxyModel.py +++ b/silx/gui/hdf5/NexusSortFilterProxyModel.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -25,7 +25,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "16/06/2017" +__date__ = "10/10/2017" import logging @@ -33,14 +33,8 @@ import re import numpy from .. import qt from .Hdf5TreeModel import Hdf5TreeModel +import silx.io.utils -_logger = logging.getLogger(__name__) - -try: - import h5py -except ImportError as e: - _logger.error("Module %s requires h5py", __name__) - raise e _logger = logging.getLogger(__name__) @@ -86,8 +80,8 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel): def __isNXentry(self, node): """Returns true if the node is an NXentry""" - class_ = node.h5pyClass - if class_ is None or not issubclass(node.h5pyClass, h5py.Group): + class_ = node.h5Class + if class_ is None or class_ != silx.io.utils.H5Type.GROUP: return False nxClass = node.obj.attrs.get("NX_class", None) return nxClass == "NXentry" @@ -100,7 +94,7 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel): `["aaa", 10, "bbb", 50, ".", 30]`. :param str name: A name - :rtype: list + :rtype: List """ words = self.__split.findall(name) result = [] @@ -148,6 +142,6 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel): return left_time < right_time except KeyboardInterrupt: raise - except Exception as e: + except Exception: _logger.debug("Exception occurred", exc_info=True) return None diff --git a/silx/gui/hdf5/_utils.py b/silx/gui/hdf5/_utils.py index 048aa20..ddf4db5 100644 --- a/silx/gui/hdf5/_utils.py +++ b/silx/gui/hdf5/_utils.py @@ -28,7 +28,7 @@ package `silx.gui.hdf5` package. __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "29/09/2017" +__date__ = "20/12/2017" import logging @@ -38,12 +38,6 @@ from silx.utils.html import escape _logger = logging.getLogger(__name__) -try: - import h5py -except ImportError as e: - _logger.error("Module %s requires h5py", __name__) - raise e - class Hdf5ContextMenuEvent(object): """Hold information provided to context menu callbacks.""" @@ -168,12 +162,13 @@ class H5Node(object): e = elements.pop(0) path = path + "/" + e link = obj.parent.get(path, getlink=True) + classlink = silx.io.utils.get_h5_class(link) - if isinstance(link, h5py.ExternalLink): + if classlink == silx.io.utils.H5Type.EXTERNAL_LINK: subpath = "/".join(elements) external_obj = obj.parent.get(self.basename + "/" + subpath) return self.__get_target(external_obj) - elif silx.io.utils.is_softlink(link): + elif classlink == silx.io.utils.H5Type.SOFT_LINK: # Restart from this stat path = "" root_elements = link.path.split("/") @@ -202,13 +197,22 @@ class H5Node(object): return self.__h5py_object @property + def h5type(self): + """Returns the node type, as an H5Type. + + :rtype: H5Node + """ + return silx.io.utils.get_h5_class(self.__h5py_object) + + @property def ntype(self): """Returns the node type, as an h5py class. :rtype: :class:`h5py.File`, :class:`h5py.Group` or :class:`h5py.Dataset` """ - return silx.io.utils.get_h5py_class(self.__h5py_object) + type_ = self.h5type + return silx.io.utils.h5type_to_h5py_class(type_) @property def basename(self): @@ -269,13 +273,13 @@ class H5Node(object): """ item = self.__h5py_item while item.parent.parent is not None: - class_ = item.h5pyClass - if class_ is not None and issubclass(class_, h5py.File): + class_ = silx.io.utils.get_h5_class(class_=item.h5pyClass) + if class_ == silx.io.utils.H5Type.FILE: break item = item.parent - class_ = item.h5pyClass - if class_ is not None and issubclass(class_, h5py.File): + class_ = silx.io.utils.get_h5_class(class_=item.h5pyClass) + if class_ == silx.io.utils.H5Type.FILE: return item.obj else: return item.obj.file @@ -313,8 +317,8 @@ class H5Node(object): :rtype: str """ - class_ = self.__h5py_item.h5pyClass - if class_ is not None and issubclass(class_, h5py.File): + class_ = self.__h5py_item.h5Class + if class_ is not None and class_ == silx.io.utils.H5Type.FILE: return "" return self.__h5py_item.basename @@ -327,10 +331,11 @@ class H5Node(object): :rtype: h5py.File :raises RuntimeError: If no file are found """ - if isinstance(self.__h5py_object, h5py.ExternalLink): + class_ = silx.io.utils.get_h5_class(self.__h5py_object) + if class_ == silx.io.utils.H5Type.EXTERNAL_LINK: # It means the link is broken raise RuntimeError("No file node found") - if isinstance(self.__h5py_object, h5py.SoftLink): + if class_ == silx.io.utils.H5Type.SOFT_LINK: # It means the link is broken return self.local_file @@ -347,10 +352,11 @@ class H5Node(object): :rtype: str """ - if isinstance(self.__h5py_object, h5py.ExternalLink): + class_ = silx.io.utils.get_h5_class(self.__h5py_object) + if class_ == silx.io.utils.H5Type.EXTERNAL_LINK: # It means the link is broken return self.__h5py_object.path - if isinstance(self.__h5py_object, h5py.SoftLink): + if class_ == silx.io.utils.H5Type.SOFT_LINK: # It means the link is broken return self.__h5py_object.path @@ -367,10 +373,11 @@ class H5Node(object): :rtype: str """ - if isinstance(self.__h5py_object, h5py.ExternalLink): + class_ = silx.io.utils.get_h5_class(self.__h5py_object) + if class_ == silx.io.utils.H5Type.EXTERNAL_LINK: # It means the link is broken return self.__h5py_object.filename - if isinstance(self.__h5py_object, h5py.SoftLink): + if class_ == silx.io.utils.H5Type.SOFT_LINK: # It means the link is broken return self.local_file.filename diff --git a/silx/gui/hdf5/test/test_hdf5.py b/silx/gui/hdf5/test/test_hdf5.py index 8e375f2..44c4456 100644 --- a/silx/gui/hdf5/test/test_hdf5.py +++ b/silx/gui/hdf5/test/test_hdf5.py @@ -26,7 +26,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "22/09/2017" +__date__ = "20/02/2018" import time @@ -40,6 +40,7 @@ from silx.gui import qt from silx.gui.test.utils import TestCaseQt from silx.gui import hdf5 from silx.io import commonh5 +import weakref try: import h5py @@ -69,6 +70,14 @@ class TestHdf5TreeModel(TestCaseQt): if h5py is None: self.skipTest("h5py is not available") + def waitForPendingOperations(self, model): + for i in range(10): + if not model.hasPendingOperations(): + break + self.qWait(10) + else: + raise RuntimeError("Still waiting for a pending operation") + @contextmanager def h5TempFile(self): # create tmp file @@ -96,7 +105,9 @@ class TestHdf5TreeModel(TestCaseQt): # clean up index = model.index(0, 0, qt.QModelIndex()) h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) - h5File.close() + ref = weakref.ref(model) + model = None + self.qWaitForDestroy(ref) def testAppendBadFilename(self): model = hdf5.Hdf5TreeModel() @@ -104,32 +115,35 @@ class TestHdf5TreeModel(TestCaseQt): def testInsertFilename(self): with self.h5TempFile() as filename: - model = hdf5.Hdf5TreeModel() - self.assertEquals(model.rowCount(qt.QModelIndex()), 0) - model.insertFile(filename) - self.assertEquals(model.rowCount(qt.QModelIndex()), 1) - # clean up - index = model.index(0, 0, qt.QModelIndex()) - h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) - h5File.close() + try: + model = hdf5.Hdf5TreeModel() + self.assertEquals(model.rowCount(qt.QModelIndex()), 0) + model.insertFile(filename) + self.assertEquals(model.rowCount(qt.QModelIndex()), 1) + # clean up + index = model.index(0, 0, qt.QModelIndex()) + h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) + self.assertIsNotNone(h5File) + finally: + ref = weakref.ref(model) + model = None + self.qWaitForDestroy(ref) def testInsertFilenameAsync(self): with self.h5TempFile() as filename: - model = hdf5.Hdf5TreeModel() - self.assertEquals(model.rowCount(qt.QModelIndex()), 0) - model.insertFileAsync(filename) - index = model.index(0, 0, qt.QModelIndex()) - self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5LoadingItem.Hdf5LoadingItem) - time.sleep(0.1) - self.qapp.processEvents() - time.sleep(0.1) - index = model.index(0, 0, qt.QModelIndex()) - self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5Item.Hdf5Item) - self.assertEquals(model.rowCount(qt.QModelIndex()), 1) - # clean up - index = model.index(0, 0, qt.QModelIndex()) - h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) - h5File.close() + try: + model = hdf5.Hdf5TreeModel() + self.assertEquals(model.rowCount(qt.QModelIndex()), 0) + model.insertFileAsync(filename) + index = model.index(0, 0, qt.QModelIndex()) + self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5LoadingItem.Hdf5LoadingItem) + self.waitForPendingOperations(model) + index = model.index(0, 0, qt.QModelIndex()) + self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5Item.Hdf5Item) + finally: + ref = weakref.ref(model) + model = None + self.qWaitForDestroy(ref) def testInsertObject(self): h5 = commonh5.File("/foo/bar/1.mock", "w") @@ -156,6 +170,10 @@ class TestHdf5TreeModel(TestCaseQt): index = model.index(0, 0, qt.QModelIndex()) node1 = model.nodeFromIndex(index) model.synchronizeH5pyObject(h5) + # Now h5 was loaded from it's filename + # Another ref is owned by the model + h5.close() + index = model.index(0, 0, qt.QModelIndex()) node2 = model.nodeFromIndex(index) self.assertIsNot(node1, node2) @@ -168,7 +186,12 @@ class TestHdf5TreeModel(TestCaseQt): # clean up index = model.index(0, 0, qt.QModelIndex()) h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) - h5File.close() + self.assertIsNotNone(h5File) + h5File = None + # delete the model + ref = weakref.ref(model) + model = None + self.qWaitForDestroy(ref) def testFileMoveState(self): model = hdf5.Hdf5TreeModel() @@ -206,15 +229,17 @@ class TestHdf5TreeModel(TestCaseQt): model.dropMimeData(mimeData, qt.Qt.CopyAction, 0, 0, qt.QModelIndex()) self.assertEquals(model.rowCount(qt.QModelIndex()), 1) # after sync - time.sleep(0.1) - self.qapp.processEvents() - time.sleep(0.1) + self.waitForPendingOperations(model) index = model.index(0, 0, qt.QModelIndex()) self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5Item.Hdf5Item) # clean up index = model.index(0, 0, qt.QModelIndex()) h5File = model.data(index, role=hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) - h5File.close() + self.assertIsNotNone(h5File) + h5File = None + ref = weakref.ref(model) + model = None + self.qWaitForDestroy(ref) def getRowDataAsDict(self, model, row): displayed = {} @@ -503,7 +528,9 @@ class TestH5Node(TestCaseQt): @classmethod def tearDownClass(cls): + ref = weakref.ref(cls.model) cls.model = None + cls.qWaitForDestroy(ref) cls.h5File.close() shutil.rmtree(cls.tmpDirectory) super(TestH5Node, cls).tearDownClass() @@ -696,6 +723,18 @@ class TestHdf5TreeView(TestCaseQt): view = hdf5.Hdf5TreeView() view._createContextMenu(qt.QPoint(0, 0)) + def testSelection_OriginalModel(self): + tree = commonh5.File("/foo/bar/1.mock", "w") + item = tree.create_group("a/b/c/d") + item.create_group("e").create_group("f") + + view = hdf5.Hdf5TreeView() + view.findHdf5TreeModel().insertH5pyObject(tree) + view.setSelectedH5Node(item) + + selected = list(view.selectedH5Nodes())[0] + self.assertIs(item, selected.h5py_object) + def testSelection_Simple(self): tree = commonh5.File("/foo/bar/1.mock", "w") item = tree.create_group("a/b/c/d") |