summaryrefslogtreecommitdiff
path: root/silx/gui/hdf5
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/hdf5')
-rw-r--r--silx/gui/hdf5/Hdf5Formatter.py5
-rw-r--r--silx/gui/hdf5/Hdf5Item.py126
-rw-r--r--silx/gui/hdf5/Hdf5TreeModel.py135
-rw-r--r--silx/gui/hdf5/Hdf5TreeView.py113
-rw-r--r--silx/gui/hdf5/NexusSortFilterProxyModel.py20
-rw-r--r--silx/gui/hdf5/_utils.py51
-rw-r--r--silx/gui/hdf5/test/test_hdf5.py99
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")