diff options
author | Picca Frédéric-Emmanuel <picca@debian.org> | 2017-10-07 07:59:01 +0200 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@debian.org> | 2017-10-07 07:59:01 +0200 |
commit | bfa4dba15485b4192f8bbe13345e9658c97ecf76 (patch) | |
tree | fb9c6e5860881fbde902f7cbdbd41dc4a3a9fb5d /silx/gui/hdf5 | |
parent | f7bdc2acff3c13a6d632c28c4569690ab106eed7 (diff) |
New upstream version 0.6.0+dfsg
Diffstat (limited to 'silx/gui/hdf5')
-rw-r--r-- | silx/gui/hdf5/Hdf5Formatter.py | 229 | ||||
-rw-r--r-- | silx/gui/hdf5/Hdf5HeaderView.py | 29 | ||||
-rw-r--r-- | silx/gui/hdf5/Hdf5Item.py | 182 | ||||
-rw-r--r-- | silx/gui/hdf5/Hdf5Node.py | 29 | ||||
-rw-r--r-- | silx/gui/hdf5/Hdf5TreeModel.py | 78 | ||||
-rw-r--r-- | silx/gui/hdf5/Hdf5TreeView.py | 85 | ||||
-rw-r--r-- | silx/gui/hdf5/NexusSortFilterProxyModel.py | 5 | ||||
-rw-r--r-- | silx/gui/hdf5/_utils.py | 184 | ||||
-rw-r--r-- | silx/gui/hdf5/test/_mock.py | 130 | ||||
-rw-r--r-- | silx/gui/hdf5/test/test_hdf5.py | 454 |
10 files changed, 1091 insertions, 314 deletions
diff --git a/silx/gui/hdf5/Hdf5Formatter.py b/silx/gui/hdf5/Hdf5Formatter.py new file mode 100644 index 0000000..3a4c1c1 --- /dev/null +++ b/silx/gui/hdf5/Hdf5Formatter.py @@ -0,0 +1,229 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017 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 +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""This package provides a class sharred by widgets to format HDF5 data as +text.""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "27/09/2017" + +import numpy +from silx.third_party import six +from silx.gui import qt +from silx.gui.data.TextFormatter import TextFormatter + +try: + import h5py +except ImportError: + h5py = None + + +class Hdf5Formatter(qt.QObject): + """Formatter to convert HDF5 data to string. + """ + + formatChanged = qt.Signal() + """Emitted when properties of the formatter change.""" + + def __init__(self, parent=None, textFormatter=None): + """ + Constructor + + :param qt.QObject parent: Owner of the object + :param TextFormatter formatter: Text formatter + """ + qt.QObject.__init__(self, parent) + if textFormatter is not None: + self.__formatter = textFormatter + else: + self.__formatter = TextFormatter(self) + self.__formatter.formatChanged.connect(self.__formatChanged) + + def textFormatter(self): + """Returns the used text formatter + + :rtype: TextFormatter + """ + return self.__formatter + + def setTextFormatter(self, textFormatter): + """Set the text formatter to be used + + :param TextFormatter textFormatter: The text formatter to use + """ + if textFormatter is None: + raise ValueError("Formatter expected but None found") + if self.__formatter is textFormatter: + return + self.__formatter.formatChanged.disconnect(self.__formatChanged) + self.__formatter = textFormatter + self.__formatter.formatChanged.connect(self.__formatChanged) + self.__formatChanged() + + def __formatChanged(self): + self.formatChanged.emit() + + def humanReadableShape(self, dataset): + if dataset.shape is None: + return "none" + if dataset.shape == tuple(): + return "scalar" + shape = [str(i) for i in dataset.shape] + text = u" \u00D7 ".join(shape) + return text + + def humanReadableValue(self, dataset): + if dataset.shape is None: + return "No data" + + dtype = dataset.dtype + if dataset.dtype.type == numpy.void: + if dtype.fields is None: + return "Raw data" + + if dataset.shape == tuple(): + numpy_object = dataset[()] + text = self.__formatter.toString(numpy_object, dtype=dataset.dtype) + else: + if dataset.size < 5 and dataset.compression is None: + numpy_object = dataset[0:5] + text = self.__formatter.toString(numpy_object, dtype=dataset.dtype) + else: + dimension = len(dataset.shape) + if dataset.compression is not None: + text = "Compressed %dD data" % dimension + else: + text = "%dD data" % dimension + return text + + def humanReadableType(self, dataset, full=False): + dtype = dataset.dtype + return self.humanReadableDType(dtype, full) + + def humanReadableDType(self, dtype, full=False): + if dtype == six.binary_type or numpy.issubdtype(dtype, numpy.string_): + text = "string" + if full: + text = "ASCII " + text + return text + elif dtype == six.text_type or numpy.issubdtype(dtype, numpy.unicode_): + text = "string" + if full: + text = "UTF-8 " + text + return text + elif dtype.type == numpy.object_: + ref = h5py.check_dtype(ref=dtype) + if ref is not None: + return "reference" + vlen = h5py.check_dtype(vlen=dtype) + if vlen is not None: + text = self.humanReadableDType(vlen, full=full) + if full: + text = "variable-length " + text + return text + return "object" + elif dtype.type == numpy.bool_: + return "bool" + elif dtype.type == numpy.void: + if dtype.fields is None: + return "opaque" + else: + if not full: + return "compound" + else: + compound = [d[0] for d in dtype.fields.values()] + compound = [self.humanReadableDType(d) for d in compound] + return "compound(%s)" % ", ".join(compound) + elif numpy.issubdtype(dtype, numpy.integer): + if h5py is not None: + enumType = h5py.check_dtype(enum=dtype) + if enumType is not None: + return "enum" + + text = str(dtype.newbyteorder('N')) + if full: + if dtype.byteorder == "<": + text = "Little-endian " + text + elif dtype.byteorder == ">": + text = "Big-endian " + text + elif dtype.byteorder == "=": + text = "Native " + text + + dtype = dtype.newbyteorder('N') + return text + + def humanReadableHdf5Type(self, dataset): + """Format the internal HDF5 type as a string""" + t = dataset.id.get_type() + class_ = t.get_class() + if class_ == h5py.h5t.NO_CLASS: + return "NO_CLASS" + elif class_ == h5py.h5t.INTEGER: + return "INTEGER" + elif class_ == h5py.h5t.FLOAT: + return "FLOAT" + elif class_ == h5py.h5t.TIME: + return "TIME" + elif class_ == h5py.h5t.STRING: + charset = t.get_cset() + strpad = t.get_strpad() + text = "" + + if strpad == h5py.h5t.STR_NULLTERM: + text += "NULLTERM" + elif strpad == h5py.h5t.STR_NULLPAD: + text += "NULLPAD" + elif strpad == h5py.h5t.STR_SPACEPAD: + text += "SPACEPAD" + else: + text += "UNKNOWN_STRPAD" + + if t.is_variable_str(): + text += " VARIABLE" + + if charset == h5py.h5t.CSET_ASCII: + text += " ASCII" + elif charset == h5py.h5t.CSET_UTF8: + text += " UTF8" + else: + text += " UNKNOWN_CSET" + + return text + " STRING" + elif class_ == h5py.h5t.BITFIELD: + return "BITFIELD" + elif class_ == h5py.h5t.OPAQUE: + return "OPAQUE" + elif class_ == h5py.h5t.COMPOUND: + return "COMPOUND" + elif class_ == h5py.h5t.REFERENCE: + return "REFERENCE" + elif class_ == h5py.h5t.ENUM: + return "ENUM" + elif class_ == h5py.h5t.VLEN: + return "VLEN" + elif class_ == h5py.h5t.ARRAY: + return "ARRAY" + else: + return "UNKNOWN_CLASS" diff --git a/silx/gui/hdf5/Hdf5HeaderView.py b/silx/gui/hdf5/Hdf5HeaderView.py index 5912230..7baa6e0 100644 --- a/silx/gui/hdf5/Hdf5HeaderView.py +++ b/silx/gui/hdf5/Hdf5HeaderView.py @@ -25,10 +25,11 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "08/11/2016" +__date__ = "16/06/2017" from .. import qt +from .Hdf5TreeModel import Hdf5TreeModel QTVERSION = qt.qVersion() @@ -83,19 +84,21 @@ class Hdf5HeaderView(qt.QHeaderView): setResizeMode = self.setSectionResizeMode if self.__auto_resize: - setResizeMode(0, qt.QHeaderView.ResizeToContents) - setResizeMode(1, qt.QHeaderView.ResizeToContents) - setResizeMode(2, qt.QHeaderView.ResizeToContents) - setResizeMode(3, qt.QHeaderView.Interactive) - setResizeMode(4, qt.QHeaderView.Interactive) - setResizeMode(5, qt.QHeaderView.ResizeToContents) + setResizeMode(Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.ResizeToContents) + setResizeMode(Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.ResizeToContents) + setResizeMode(Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.ResizeToContents) + setResizeMode(Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.ResizeToContents) + setResizeMode(Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.ResizeToContents) else: - setResizeMode(0, qt.QHeaderView.Interactive) - setResizeMode(1, qt.QHeaderView.Interactive) - setResizeMode(2, qt.QHeaderView.Interactive) - setResizeMode(3, qt.QHeaderView.Interactive) - setResizeMode(4, qt.QHeaderView.Interactive) - setResizeMode(5, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.Interactive) + setResizeMode(Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.Interactive) def setAutoResizeColumns(self, autoResize): """Enable/disable auto-resize. When auto-resized, the header take care diff --git a/silx/gui/hdf5/Hdf5Item.py b/silx/gui/hdf5/Hdf5Item.py index 40793a4..f131f61 100644 --- a/silx/gui/hdf5/Hdf5Item.py +++ b/silx/gui/hdf5/Hdf5Item.py @@ -25,10 +25,9 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "20/01/2017" +__date__ = "26/09/2017" -import numpy import logging import collections from .. import qt @@ -37,6 +36,7 @@ from . import _utils from .Hdf5Node import Hdf5Node import silx.io.utils from silx.gui.data.TextFormatter import TextFormatter +from ..hdf5.Hdf5Formatter import Hdf5Formatter _logger = logging.getLogger(__name__) @@ -47,6 +47,8 @@ except ImportError as e: raise e _formatter = TextFormatter() +_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter) +# FIXME: The formatter should be an attribute of the Hdf5Model class Hdf5Item(Hdf5Node): @@ -55,7 +57,7 @@ class Hdf5Item(Hdf5Node): tree structure. """ - def __init__(self, text, obj, parent, key=None, h5pyClass=None, isBroken=False, populateAll=False): + def __init__(self, text, obj, parent, key=None, h5pyClass=None, linkClass=None, populateAll=False): """ :param str text: text displayed :param object obj: Pointer to h5py data. See the `obj` attribute. @@ -63,9 +65,10 @@ class Hdf5Item(Hdf5Node): self.__obj = obj self.__key = key self.__h5pyClass = h5pyClass - self.__isBroken = isBroken + self.__isBroken = obj is None and h5pyClass is None self.__error = None self.__text = text + self.__linkClass = linkClass Hdf5Node.__init__(self, parent, populateAll=populateAll) @property @@ -88,16 +91,26 @@ class Hdf5Item(Hdf5Node): :rtype: h5py.File or h5py.Dataset or h5py.Group """ - if self.__h5pyClass is None: + if self.__h5pyClass is None and self.obj is not None: self.__h5pyClass = silx.io.utils.get_h5py_class(self.obj) return self.__h5pyClass + @property + def linkClass(self): + """Returns the link class object of this node + + :type: h5py.SoftLink or h5py.HardLink or h5py.ExternalLink or None + """ + return self.__linkClass + def isGroupObj(self): """Returns true if the stored HDF5 object is a group (contains sub groups or datasets). :rtype: bool """ + if self.h5pyClass is None: + return False return issubclass(self.h5pyClass, h5py.Group) def isBrokenObj(self): @@ -111,6 +124,14 @@ class Hdf5Item(Hdf5Node): """ return self.__isBroken + def _getFormatter(self): + """ + Returns an Hdf5Formatter + + :rtype: Hdf5Formatter + """ + return _hdf5Formatter + def _expectedChildCount(self): if self.isGroupObj(): return len(self.obj) @@ -158,6 +179,22 @@ class Hdf5Item(Hdf5Node): self.__isBroken = True else: self.__obj = obj + if not self.isGroupObj(): + try: + # pre-fetch of the data + if obj.shape is None: + pass + elif obj.shape == tuple(): + obj[()] + else: + if obj.compression is None and obj.size > 0: + key = tuple([0] * len(obj.shape)) + obj[key] + except Exception as e: + _logger.debug(e, exc_info=True) + message = "%s broken. %s" % (self.__obj.name, e.args[0]) + self.__error = message + self.__isBroken = True self.__key = None @@ -166,15 +203,15 @@ class Hdf5Item(Hdf5Node): for name in self.obj: try: class_ = self.obj.get(name, getclass=True) - has_error = False + link = self.obj.get(name, getclass=True, getlink=True) except Exception as e: - _logger.error("Internal h5py error", exc_info=True) + _logger.warn("Internal h5py error", exc_info=True) + class_ = None try: - class_ = self.obj.get(name, getclass=True, getlink=True) + link = self.obj.get(name, getclass=True, getlink=True) except Exception as e: - class_ = h5py.HardLink - has_error = True - item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5pyClass=class_, isBroken=has_error) + link = h5py.HardLink + item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5pyClass=class_, linkClass=link) self.appendChild(item) def hasChildren(self): @@ -191,6 +228,8 @@ class Hdf5Item(Hdf5Node): :rtype: qt.QIcon """ + # Pre-fetch the object, in case it is broken + obj = self.obj style = qt.QApplication.style() if self.__isBroken: icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical) @@ -205,99 +244,53 @@ class Hdf5Item(Hdf5Node): elif issubclass(class_, h5py.ExternalLink): return style.standardIcon(qt.QStyle.SP_FileLinkIcon) elif issubclass(class_, h5py.Dataset): - if len(self.obj.shape) < 4: - name = "item-%ddim" % len(self.obj.shape) + if obj.shape is None: + name = "item-none" + elif len(obj.shape) < 4: + name = "item-%ddim" % len(obj.shape) else: name = "item-ndim" - if str(self.obj.dtype) == "object": - name = "item-object" icon = icons.getQIcon(name) return icon return None - def _humanReadableShape(self, dataset): - if dataset.shape == tuple(): - return "scalar" - shape = [str(i) for i in dataset.shape] - text = u" \u00D7 ".join(shape) - return text - - def _humanReadableValue(self, dataset): - if dataset.shape == tuple(): - numpy_object = dataset[()] - text = _formatter.toString(numpy_object) - else: - if dataset.size < 5 and dataset.compression is None: - numpy_object = dataset[0:5] - text = _formatter.toString(numpy_object) - else: - dimension = len(dataset.shape) - if dataset.compression is not None: - text = "Compressed %dD data" % dimension - else: - text = "%dD data" % dimension - return text - - def _humanReadableDType(self, dtype, full=False): - if dtype.type == numpy.string_: - text = "string" - elif dtype.type == numpy.unicode_: - text = "string" - elif dtype.type == numpy.object_: - text = "object" - elif dtype.type == numpy.bool_: - text = "bool" - elif dtype.type == numpy.void: - if dtype.fields is None: - text = "raw" - else: - if not full: - text = "compound" - else: - compound = [d[0] for d in dtype.fields.values()] - compound = [self._humanReadableDType(d) for d in compound] - text = "compound(%s)" % ", ".join(compound) - else: - text = str(dtype) - return text - - def _humanReadableType(self, dataset, full=False): - return self._humanReadableDType(dataset.dtype, full) - - def _setTooltipAttributes(self, attributeDict): + def _createTooltipAttributes(self): """ Add key/value attributes that will be displayed in the item tooltip :param Dict[str,str] attributeDict: Key/value attributes """ + attributeDict = collections.OrderedDict() + if issubclass(self.h5pyClass, h5py.Dataset): - attributeDict["Title"] = "HDF5 Dataset" + attributeDict["#Title"] = "HDF5 Dataset" attributeDict["Name"] = self.basename attributeDict["Path"] = self.obj.name - attributeDict["Shape"] = self._humanReadableShape(self.obj) - attributeDict["Value"] = self._humanReadableValue(self.obj) - attributeDict["Data type"] = self._humanReadableType(self.obj, full=True) + 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): - attributeDict["Title"] = "HDF5 Group" + attributeDict["#Title"] = "HDF5 Group" attributeDict["Name"] = self.basename attributeDict["Path"] = self.obj.name elif issubclass(self.h5pyClass, h5py.File): - attributeDict["Title"] = "HDF5 File" + attributeDict["#Title"] = "HDF5 File" attributeDict["Name"] = self.basename attributeDict["Path"] = "/" elif isinstance(self.obj, h5py.ExternalLink): - attributeDict["Title"] = "HDF5 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): - attributeDict["Title"] = "HDF5 Soft Link" + attributeDict["#Title"] = "HDF5 Soft Link" attributeDict["Name"] = self.basename attributeDict["Path"] = self.obj.name attributeDict["Linked path"] = self.obj.path else: pass + return attributeDict def _getDefaultTooltip(self): """Returns the default tooltip @@ -308,10 +301,8 @@ class Hdf5Item(Hdf5Node): self.obj # lazy loading of the object return self.__error - attrs = collections.OrderedDict() - self._setTooltipAttributes(attrs) - - title = attrs.pop("Title", None) + attrs = self._createTooltipAttributes() + title = attrs.pop("#Title", None) if len(attrs) > 0: tooltip = _utils.htmlFromDict(attrs, title=title) else: @@ -342,7 +333,7 @@ class Hdf5Item(Hdf5Node): return "" class_ = self.h5pyClass if issubclass(class_, h5py.Dataset): - text = self._humanReadableType(self.obj) + text = self._getFormatter().humanReadableType(self.obj) else: text = "" return text @@ -361,7 +352,7 @@ class Hdf5Item(Hdf5Node): class_ = self.h5pyClass if not issubclass(class_, h5py.Dataset): return "" - return self._humanReadableShape(self.obj) + return self._getFormatter().humanReadableShape(self.obj) return None def dataValue(self, role): @@ -375,7 +366,7 @@ class Hdf5Item(Hdf5Node): return "" if not issubclass(self.h5pyClass, h5py.Dataset): return "" - return self._humanReadableValue(self.obj) + return self._getFormatter().humanReadableValue(self.obj) return None def dataDescription(self, role): @@ -412,10 +403,41 @@ class Hdf5Item(Hdf5Node): if role == qt.Qt.TextAlignmentRole: return qt.Qt.AlignTop | qt.Qt.AlignLeft if role == qt.Qt.DisplayRole: + if self.isBrokenObj(): + return "" class_ = self.h5pyClass text = class_.__name__.split(".")[-1] return text if role == qt.Qt.ToolTipRole: class_ = self.h5pyClass + if class_ is None: + return "" return "Class name: %s" % self.__class__ return None + + def dataLink(self, role): + """Data for the link column + + Overwrite it to implement the content of the 'link' column. + + :rtype: qt.QVariant + """ + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + link = self.linkClass + if link is None: + return "" + elif link is h5py.ExternalLink: + return "External" + elif link is h5py.SoftLink: + return "Soft" + elif link is h5py.HardLink: + return "" + else: + return link.__name__ + if role == qt.Qt.ToolTipRole: + return None + return None diff --git a/silx/gui/hdf5/Hdf5Node.py b/silx/gui/hdf5/Hdf5Node.py index 31bb097..0fcb407 100644 --- a/silx/gui/hdf5/Hdf5Node.py +++ b/silx/gui/hdf5/Hdf5Node.py @@ -25,7 +25,9 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "23/09/2016" +__date__ = "16/06/2017" + +import weakref class Hdf5Node(object): @@ -43,7 +45,9 @@ class Hdf5Node(object): everything is lazy loaded. """ self.__child = None - self.__parent = parent + self.__parent = None + if parent is not None: + self.__parent = weakref.ref(parent) if populateAll: self.__child = [] self._populateChild(populateAll=True) @@ -54,7 +58,12 @@ class Hdf5Node(object): :rtype: Hdf5Node """ - return self.__parent + if self.__parent is None: + return None + parent = self.__parent() + if parent is None: + self.__parent = parent + return parent def setParent(self, parent): """Redefine the parent of the node. @@ -63,7 +72,10 @@ class Hdf5Node(object): :param Hdf5Node parent: The new parent """ - self.__parent = parent + if parent is None: + self.__parent = None + else: + self.__parent = weakref.ref(parent) def appendChild(self, child): """Append a child to the node. @@ -208,3 +220,12 @@ class Hdf5Node(object): :rtype: qt.QVariant """ return None + + def dataLink(self, role): + """Data for the link column + + Overwrite it to implement the content of the 'link' column. + + :rtype: qt.QVariant + """ + return None diff --git a/silx/gui/hdf5/Hdf5TreeModel.py b/silx/gui/hdf5/Hdf5TreeModel.py index fb5de06..41fa91c 100644 --- a/silx/gui/hdf5/Hdf5TreeModel.py +++ b/silx/gui/hdf5/Hdf5TreeModel.py @@ -25,7 +25,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "19/12/2016" +__date__ = "22/09/2017" import os @@ -71,6 +71,25 @@ else: return x +def _createRootLabel(h5obj): + """ + Create label for the very first npde of the tree. + + :param h5obj: The h5py object to display in the GUI + :type h5obj: h5py-like object + :rtpye: str + """ + if silx_io.is_file(h5obj): + label = os.path.basename(h5obj.filename) + else: + filename = os.path.basename(h5obj.file.filename) + path = h5obj.name + if path.startswith("/"): + path = path[1:] + label = "%s::%s" % (filename, path) + return label + + class LoadingItemRunnable(qt.QRunnable): """Runner to process item loading from a file""" @@ -107,12 +126,7 @@ class LoadingItemRunnable(qt.QRunnable): :param h5py.File h5obj: The h5py object to display in the GUI :rtpye: Hdf5Node """ - if silx_io.is_file(h5obj): - text = os.path.basename(h5obj.filename) - else: - filename = os.path.basename(h5obj.file.filename) - path = h5obj.name - text = "%s::%s" % (filename, path) + text = _createRootLabel(h5obj) item = Hdf5Item(text=text, obj=h5obj, parent=oldItem.parent, populateAll=True) return item @@ -121,6 +135,7 @@ class LoadingItemRunnable(qt.QRunnable): """Process the file loading. The worker is used as holder of the data and the signal. The result is sent as a signal. """ + h5file = None try: h5file = silx_io.open(self.filename) newItem = self.__loadItemTree(self.oldItem, h5file) @@ -129,6 +144,8 @@ class LoadingItemRunnable(qt.QRunnable): # Should be logged error = e newItem = None + if h5file is not None: + h5file.close() # Take care of None value in case of PySide newItem = _wrapNone(newItem) @@ -174,6 +191,9 @@ class Hdf5TreeModel(qt.QAbstractItemModel): NODE_COLUMN = 5 """Column id containing HDF5 node type""" + LINK_COLUMN = 6 + """Column id containing HDF5 link type""" + COLUMN_IDS = [ NAME_COLUMN, TYPE_COLUMN, @@ -181,20 +201,21 @@ class Hdf5TreeModel(qt.QAbstractItemModel): VALUE_COLUMN, DESCRIPTION_COLUMN, NODE_COLUMN, + LINK_COLUMN, ] """List of logical columns available""" def __init__(self, parent=None): super(Hdf5TreeModel, self).__init__(parent) - self.treeView = parent - self.header_labels = [None] * 6 + self.header_labels = [None] * len(self.COLUMN_IDS) self.header_labels[self.NAME_COLUMN] = 'Name' self.header_labels[self.TYPE_COLUMN] = 'Type' self.header_labels[self.SHAPE_COLUMN] = 'Shape' self.header_labels[self.VALUE_COLUMN] = 'Value' self.header_labels[self.DESCRIPTION_COLUMN] = 'Description' self.header_labels[self.NODE_COLUMN] = 'Node' + self.header_labels[self.LINK_COLUMN] = 'Link' # Create items self.__root = Hdf5Node() @@ -205,14 +226,36 @@ class Hdf5TreeModel(qt.QAbstractItemModel): self.__animatedIcon.iconChanged.connect(self.__updateLoadingItems) self.__runnerSet = set([]) - # store used icons to avoid to avoid the cache to release it + # store used icons to avoid the cache to release it self.__icons = [] + self.__icons.append(icons.getQIcon("item-none")) self.__icons.append(icons.getQIcon("item-0dim")) self.__icons.append(icons.getQIcon("item-1dim")) self.__icons.append(icons.getQIcon("item-2dim")) self.__icons.append(icons.getQIcon("item-3dim")) self.__icons.append(icons.getQIcon("item-ndim")) - self.__icons.append(icons.getQIcon("item-object")) + + 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__() + + 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 = [] def __updateLoadingItems(self, icon): for i in range(self.__root.childCount()): @@ -240,6 +283,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): self.__root.removeChildAtIndex(row) self.endRemoveRows() if newItem is not None: + self.__openedFiles.append(newItem.obj) self.beginInsertRows(rootIndex, row, row) self.__root.insertChild(row, newItem) self.endInsertRows() @@ -423,11 +467,13 @@ class Hdf5TreeModel(qt.QAbstractItemModel): return node.dataDescription(role) elif index.column() == self.NODE_COLUMN: return node.dataNode(role) + elif index.column() == self.LINK_COLUMN: + return node.dataLink(role) else: return None def columnCount(self, parent=qt.QModelIndex()): - return len(self.header_labels) + return len(self.COLUMN_IDS) def hasChildren(self, parent=qt.QModelIndex()): node = self.nodeFromIndex(parent) @@ -536,12 +582,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): or any other class of h5py file structure. """ if text is None: - if silx_io.is_file(h5pyObject): - text = os.path.basename(h5pyObject.filename) - else: - filename = os.path.basename(h5pyObject.file.filename) - path = h5pyObject.name - text = "%s::%s" % (filename, path) + text = _createRootLabel(h5pyObject) if row == -1: row = self.__root.childCount() self.insertNode(row, Hdf5Item(text=text, obj=h5pyObject, parent=self.__root)) @@ -572,6 +613,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): """ try: h5file = silx_io.open(filename) + self.__openedFiles.append(h5file) self.insertH5pyObject(h5file, row=row) except IOError: _logger.debug("File '%s' can't be read.", filename, exc_info=True) diff --git a/silx/gui/hdf5/Hdf5TreeView.py b/silx/gui/hdf5/Hdf5TreeView.py index 09f6fcf..0a4198e 100644 --- a/silx/gui/hdf5/Hdf5TreeView.py +++ b/silx/gui/hdf5/Hdf5TreeView.py @@ -25,7 +25,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "27/09/2016" +__date__ = "20/09/2017" import logging @@ -43,6 +43,8 @@ _logger = logging.getLogger(__name__) class Hdf5TreeView(qt.QTreeView): """TreeView which allow to browse HDF5 file structure. + .. image:: img/Hdf5TreeView.png + It provides columns width auto-resizing and additional signals. @@ -192,6 +194,87 @@ class Hdf5TreeView(qt.QTreeView): continue yield _utils.H5Node(item) + def setSelectedH5Node(self, h5Object): + """ + Select the specified node of the tree using an h5py node. + + - If the item is found, parent items are expended, and then the item + is selected. + - 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 + """ + 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: + # Update the GUI + for index in foundIndices[:-1]: + self.expand(index) + self.setCurrentIndex(foundIndices[-1]) + def mousePressEvent(self, event): """Override mousePressEvent to provide a consistante compatible API between Qt4 and Qt5 diff --git a/silx/gui/hdf5/NexusSortFilterProxyModel.py b/silx/gui/hdf5/NexusSortFilterProxyModel.py index 9a4268c..49a22d3 100644 --- a/silx/gui/hdf5/NexusSortFilterProxyModel.py +++ b/silx/gui/hdf5/NexusSortFilterProxyModel.py @@ -25,7 +25,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "12/04/2017" +__date__ = "16/06/2017" import logging @@ -86,7 +86,8 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel): def __isNXentry(self, node): """Returns true if the node is an NXentry""" - if not issubclass(node.h5pyClass, h5py.Group): + class_ = node.h5pyClass + if class_ is None or not issubclass(node.h5pyClass, h5py.Group): return False nxClass = node.obj.attrs.get("NX_class", None) return nxClass == "NXentry" diff --git a/silx/gui/hdf5/_utils.py b/silx/gui/hdf5/_utils.py index af9c79f..048aa20 100644 --- a/silx/gui/hdf5/_utils.py +++ b/silx/gui/hdf5/_utils.py @@ -28,11 +28,10 @@ package `silx.gui.hdf5` package. __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "26/04/2017" +__date__ = "29/09/2017" import logging -import numpy from .. import qt import silx.io.utils from silx.utils.html import escape @@ -138,10 +137,61 @@ class H5Node(object): :param Hdf5Item h5py_item: An Hdf5Item """ self.__h5py_object = h5py_item.obj + self.__h5py_target = None self.__h5py_item = h5py_item def __getattr__(self, name): - return object.__getattribute__(self.__h5py_object, name) + if hasattr(self.__h5py_object, name): + attr = getattr(self.__h5py_object, name) + return attr + raise AttributeError("H5Node has no attribute %s" % name) + + def __get_target(self, obj): + """ + Return the actual physical target of the provided object. + + Objects can contains links in the middle of the path, this function + check each groups and remove this prefix in case of the link by the + link of the path. + + :param obj: A valid h5py object (File, group or dataset) + :type obj: h5py.Dataset or h5py.Group or h5py.File + :rtype: h5py.Dataset or h5py.Group or h5py.File + """ + elements = obj.name.split("/") + if obj.name == "/": + return obj + elif obj.name.startswith("/"): + elements.pop(0) + path = "" + while len(elements) > 0: + e = elements.pop(0) + path = path + "/" + e + link = obj.parent.get(path, getlink=True) + + if isinstance(link, h5py.ExternalLink): + subpath = "/".join(elements) + external_obj = obj.parent.get(self.basename + "/" + subpath) + return self.__get_target(external_obj) + elif silx.io.utils.is_softlink(link): + # Restart from this stat + path = "" + root_elements = link.path.split("/") + if link.path == "/": + root_elements = [] + elif link.path.startswith("/"): + root_elements.pop(0) + for name in reversed(root_elements): + elements.insert(0, name) + + return obj.file[path] + + @property + def h5py_target(self): + if self.__h5py_target is not None: + return self.__h5py_target + self.__h5py_target = self.__get_target(self.__h5py_object) + return self.__h5py_target @property def h5py_object(self): @@ -170,8 +220,18 @@ class H5Node(object): return self.__h5py_object.name.split("/")[-1] @property + def is_broken(self): + """Returns true if the node is a broken link. + + :rtype: bool + """ + if self.__h5py_item is None: + raise RuntimeError("h5py_item is not defined") + return self.__h5py_item.isBrokenObj() + + @property def local_name(self): - """Returns the local path of this h5py node. + """Returns the path from the master file root to this node. For links, this path is not equal to the h5py one. @@ -183,34 +243,46 @@ class H5Node(object): result = [] item = self.__h5py_item while item is not None: - if issubclass(item.h5pyClass, h5py.File): + # stop before the root item (item without parent) + if item.parent.parent is None: + name = item.obj.name + if name != "/": + result.append(item.obj.name) break - result.append(item.basename) + else: + result.append(item.basename) item = item.parent if item is None: raise RuntimeError("The item does not have parent holding h5py.File") if result == []: return "/" - result.append("") + if not result[-1].startswith("/"): + result.append("") result.reverse() - return "/".join(result) + name = "/".join(result) + return name - def __file_item(self): - """Returns the parent item holding the :class:`h5py.File` object + def __get_local_file(self): + """Returns the file of the root of this tree :rtype: h5py.File - :raises RuntimeException: If no file are found """ item = self.__h5py_item - while item is not None: - if issubclass(item.h5pyClass, h5py.File): - return item + while item.parent.parent is not None: + class_ = item.h5pyClass + if class_ is not None and issubclass(class_, h5py.File): + break item = item.parent - raise RuntimeError("The item does not have parent holding h5py.File") + + class_ = item.h5pyClass + if class_ is not None and issubclass(class_, h5py.File): + return item.obj + else: + return item.obj.file @property def local_file(self): - """Returns the local :class:`h5py.File` object. + """Returns the master file in which is this node. For path containing external links, this file is not equal to the h5py one. @@ -218,12 +290,11 @@ class H5Node(object): :rtype: h5py.File :raises RuntimeException: If no file are found """ - item = self.__file_item() - return item.obj + return self.__get_local_file() @property def local_filename(self): - """Returns the local filename of the h5py node. + """Returns the filename from the master file of this node. For path containing external links, this path is not equal to the filename provided by h5py. @@ -235,13 +306,84 @@ class H5Node(object): @property def local_basename(self): - """Returns the local filename of the h5py node. + """Returns the basename from the master file root to this node. For path containing links, this basename can be different than the basename provided by h5py. :rtype: str """ - if issubclass(self.__h5py_item.h5pyClass, h5py.File): + class_ = self.__h5py_item.h5pyClass + if class_ is not None and issubclass(class_, h5py.File): return "" return self.__h5py_item.basename + + @property + def physical_file(self): + """Returns the physical file in which is this node. + + .. versionadded:: 0.6 + + :rtype: h5py.File + :raises RuntimeError: If no file are found + """ + if isinstance(self.__h5py_object, h5py.ExternalLink): + # It means the link is broken + raise RuntimeError("No file node found") + if isinstance(self.__h5py_object, h5py.SoftLink): + # It means the link is broken + return self.local_file + + physical_obj = self.h5py_target + return physical_obj.file + + @property + def physical_name(self): + """Returns the path from the location this h5py node is physically + stored. + + For broken links, this filename can be different from the + filename provided by h5py. + + :rtype: str + """ + if isinstance(self.__h5py_object, h5py.ExternalLink): + # It means the link is broken + return self.__h5py_object.path + if isinstance(self.__h5py_object, h5py.SoftLink): + # It means the link is broken + return self.__h5py_object.path + + physical_obj = self.h5py_target + return physical_obj.name + + @property + def physical_filename(self): + """Returns the filename from the location this h5py node is physically + stored. + + For broken links, this filename can be different from the + filename provided by h5py. + + :rtype: str + """ + if isinstance(self.__h5py_object, h5py.ExternalLink): + # It means the link is broken + return self.__h5py_object.filename + if isinstance(self.__h5py_object, h5py.SoftLink): + # It means the link is broken + return self.local_file.filename + + return self.physical_file.filename + + @property + def physical_basename(self): + """Returns the basename from the location this h5py node is physically + stored. + + For broken links, this basename can be different from the + basename provided by h5py. + + :rtype: str + """ + return self.physical_name.split("/")[-1] diff --git a/silx/gui/hdf5/test/_mock.py b/silx/gui/hdf5/test/_mock.py deleted file mode 100644 index eada590..0000000 --- a/silx/gui/hdf5/test/_mock.py +++ /dev/null @@ -1,130 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 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 -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""Mock for silx.gui.hdf5 module""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/04/2017" - - -import numpy -try: - import h5py -except ImportError: - h5py = None - - -class Node(object): - - def __init__(self, basename, parent, h5py_class): - self.basename = basename - self.h5py_class = h5py_class - self.attrs = {} - self.parent = parent - if parent is not None: - self.parent._add(self) - - @property - def name(self): - if self.parent is None: - return self.basename - if self.parent.name == "": - return self.basename - return self.parent.name + "/" + self.basename - - @property - def file(self): - if self.parent is None: - return self - return self.parent.file - - -class Group(Node): - """Mock an h5py Group""" - - def __init__(self, name, parent, h5py_class=h5py.Group): - super(Group, self).__init__(name, parent, h5py_class) - self.__items = {} - - def _add(self, node): - self.__items[node.basename] = node - - def __getitem__(self, key): - return self.__items[key] - - def __iter__(self): - for k in self.__items: - yield k - - def __len__(self): - return len(self.__items) - - def get(self, name, getclass=False, getlink=False): - result = self.__items[name] - if getclass: - return result.h5py_class - return result - - def create_dataset(self, name, data): - return Dataset(name, self, data) - - def create_group(self, name): - return Group(name, self) - - def create_NXentry(self, name): - group = Group(name, self) - group.attrs["NX_class"] = "NXentry" - return group - - -class File(Group): - """Mock an h5py File""" - - def __init__(self, filename): - super(File, self).__init__("", None, h5py.File) - self.filename = filename - - -class Dataset(Node): - """Mock an h5py Dataset""" - - def __init__(self, name, parent, value): - super(Dataset, self).__init__(name, parent, h5py.Dataset) - self.__value = value - self.shape = self.__value.shape - self.dtype = self.__value.dtype - self.size = self.__value.size - self.compression = None - self.compression_opts = None - - def __getitem__(self, key): - if not isinstance(self.__value, numpy.ndarray): - if key == tuple(): - return self.__value - elif key == Ellipsis: - return numpy.array(self.__value) - else: - raise ValueError("Bad key") - return self.__value[key] diff --git a/silx/gui/hdf5/test/test_hdf5.py b/silx/gui/hdf5/test/test_hdf5.py index 3bf4897..8e375f2 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__ = "12/04/2017" +__date__ = "22/09/2017" import time @@ -34,11 +34,12 @@ import os import unittest import tempfile import numpy +import shutil from contextlib import contextmanager from silx.gui import qt from silx.gui.test.utils import TestCaseQt from silx.gui import hdf5 -from . import _mock +from silx.io import commonh5 try: import h5py @@ -54,6 +55,13 @@ class _Holder(object): _called += 1 +def create_NXentry(group, name): + attrs = {"NX_class": "NXentry"} + node = commonh5.Group(name, parent=group, attrs=attrs) + group.add_node(node) + return node + + class TestHdf5TreeModel(TestCaseQt): def setUp(self): @@ -124,14 +132,14 @@ class TestHdf5TreeModel(TestCaseQt): h5File.close() def testInsertObject(self): - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") model = hdf5.Hdf5TreeModel() self.assertEquals(model.rowCount(qt.QModelIndex()), 0) model.insertH5pyObject(h5) self.assertEquals(model.rowCount(qt.QModelIndex()), 1) def testRemoveObject(self): - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") model = hdf5.Hdf5TreeModel() self.assertEquals(model.rowCount(qt.QModelIndex()), 0) model.insertH5pyObject(h5) @@ -223,7 +231,7 @@ class TestHdf5TreeModel(TestCaseQt): return model.data(index, qt.Qt.DisplayRole) def testFileData(self): - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") model = hdf5.Hdf5TreeModel() model.insertH5pyObject(h5) displayed = self.getRowDataAsDict(model, row=0) @@ -236,7 +244,7 @@ class TestHdf5TreeModel(TestCaseQt): self.assertEquals(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "File") def testGroupData(self): - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") d = h5.create_group("foo") d.attrs["desc"] = "fooo" @@ -252,9 +260,9 @@ class TestHdf5TreeModel(TestCaseQt): self.assertEquals(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Group") def testDatasetData(self): - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") value = numpy.array([1, 2, 3]) - d = h5.create_dataset("foo", value) + d = h5.create_dataset("foo", data=value) model = hdf5.Hdf5TreeModel() model.insertH5pyObject(d) @@ -269,8 +277,8 @@ class TestHdf5TreeModel(TestCaseQt): def testDropLastAsFirst(self): model = hdf5.Hdf5TreeModel() - h5_1 = _mock.File("/foo/bar/1.mock") - h5_2 = _mock.File("/foo/bar/2.mock") + h5_1 = commonh5.File("/foo/bar/1.mock", "w") + h5_2 = commonh5.File("/foo/bar/2.mock", "w") model.insertH5pyObject(h5_1) model.insertH5pyObject(h5_2) self.assertEquals(self.getItemName(model, 0), "1.mock") @@ -283,8 +291,8 @@ class TestHdf5TreeModel(TestCaseQt): def testDropFirstAsLast(self): model = hdf5.Hdf5TreeModel() - h5_1 = _mock.File("/foo/bar/1.mock") - h5_2 = _mock.File("/foo/bar/2.mock") + h5_1 = commonh5.File("/foo/bar/1.mock", "w") + h5_2 = commonh5.File("/foo/bar/2.mock", "w") model.insertH5pyObject(h5_1) model.insertH5pyObject(h5_2) self.assertEquals(self.getItemName(model, 0), "1.mock") @@ -297,7 +305,7 @@ class TestHdf5TreeModel(TestCaseQt): def testRootParent(self): model = hdf5.Hdf5TreeModel() - h5_1 = _mock.File("/foo/bar/1.mock") + h5_1 = commonh5.File("/foo/bar/1.mock", "w") model.insertH5pyObject(h5_1) index = model.index(0, 0, qt.QModelIndex()) index = model.parent(index) @@ -318,10 +326,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testNXentryStartTime(self): """Test NXentry with start_time""" model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") - h5.create_NXentry("a").create_dataset("start_time", numpy.string_("2015")) - h5.create_NXentry("b").create_dataset("start_time", numpy.string_("2013")) - h5.create_NXentry("c").create_dataset("start_time", numpy.string_("2014")) + h5 = commonh5.File("/foo/bar/1.mock", "w") + create_NXentry(h5, "a").create_dataset("start_time", data=numpy.string_("2015")) + create_NXentry(h5, "b").create_dataset("start_time", data=numpy.string_("2013")) + create_NXentry(h5, "c").create_dataset("start_time", data=numpy.string_("2014")) model.insertH5pyObject(h5) proxy = hdf5.NexusSortFilterProxyModel() @@ -333,10 +341,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testNXentryStartTimeInArray(self): """Test NXentry with start_time""" model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") - h5.create_NXentry("a").create_dataset("start_time", numpy.array([numpy.string_("2015")])) - h5.create_NXentry("b").create_dataset("start_time", numpy.array([numpy.string_("2013")])) - h5.create_NXentry("c").create_dataset("start_time", numpy.array([numpy.string_("2014")])) + h5 = commonh5.File("/foo/bar/1.mock", "w") + create_NXentry(h5, "a").create_dataset("start_time", data=numpy.array([numpy.string_("2015")])) + create_NXentry(h5, "b").create_dataset("start_time", data=numpy.array([numpy.string_("2013")])) + create_NXentry(h5, "c").create_dataset("start_time", data=numpy.array([numpy.string_("2014")])) model.insertH5pyObject(h5) proxy = hdf5.NexusSortFilterProxyModel() @@ -348,10 +356,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testNXentryEndTimeInArray(self): """Test NXentry with end_time""" model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") - h5.create_NXentry("a").create_dataset("end_time", numpy.array([numpy.string_("2015")])) - h5.create_NXentry("b").create_dataset("end_time", numpy.array([numpy.string_("2013")])) - h5.create_NXentry("c").create_dataset("end_time", numpy.array([numpy.string_("2014")])) + h5 = commonh5.File("/foo/bar/1.mock", "w") + create_NXentry(h5, "a").create_dataset("end_time", data=numpy.array([numpy.string_("2015")])) + create_NXentry(h5, "b").create_dataset("end_time", data=numpy.array([numpy.string_("2013")])) + create_NXentry(h5, "c").create_dataset("end_time", data=numpy.array([numpy.string_("2014")])) model.insertH5pyObject(h5) proxy = hdf5.NexusSortFilterProxyModel() @@ -363,10 +371,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testNXentryName(self): """Test NXentry without start_time or end_time""" model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") - h5.create_NXentry("a") - h5.create_NXentry("c") - h5.create_NXentry("b") + h5 = commonh5.File("/foo/bar/1.mock", "w") + create_NXentry(h5, "a") + create_NXentry(h5, "c") + create_NXentry(h5, "b") model.insertH5pyObject(h5) proxy = hdf5.NexusSortFilterProxyModel() @@ -378,10 +386,10 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testStartTime(self): """If it is not NXentry, start_time is not used""" model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") - h5.create_group("a").create_dataset("start_time", numpy.string_("2015")) - h5.create_group("b").create_dataset("start_time", numpy.string_("2013")) - h5.create_group("c").create_dataset("start_time", numpy.string_("2014")) + h5 = commonh5.File("/foo/bar/1.mock", "w") + h5.create_group("a").create_dataset("start_time", data=numpy.string_("2015")) + h5.create_group("b").create_dataset("start_time", data=numpy.string_("2013")) + h5.create_group("c").create_dataset("start_time", data=numpy.string_("2014")) model.insertH5pyObject(h5) proxy = hdf5.NexusSortFilterProxyModel() @@ -392,7 +400,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testName(self): model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") h5.create_group("a") h5.create_group("c") h5.create_group("b") @@ -406,7 +414,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testNumber(self): model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") h5.create_group("a1") h5.create_group("a20") h5.create_group("a3") @@ -420,7 +428,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testMultiNumber(self): model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") h5.create_group("a1-1") h5.create_group("a20-1") h5.create_group("a3-1") @@ -436,7 +444,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt): def testUnconsistantTypes(self): model = hdf5.Hdf5TreeModel() - h5 = _mock.File("/foo/bar/1.mock") + h5 = commonh5.File("/foo/bar/1.mock", "w") h5.create_group("aaa100") h5.create_group("100aaa") model.insertH5pyObject(h5) @@ -448,11 +456,235 @@ class TestNexusSortFilterProxyModel(TestCaseQt): self.assertListEqual(names, ["100aaa", "aaa100"]) -class TestHdf5(TestCaseQt): +class TestH5Node(TestCaseQt): + + @classmethod + def setUpClass(cls): + super(TestH5Node, cls).setUpClass() + if h5py is None: + raise unittest.SkipTest("h5py is not available") + + cls.tmpDirectory = tempfile.mkdtemp() + cls.h5Filename = cls.createResource(cls.tmpDirectory) + cls.h5File = h5py.File(cls.h5Filename, mode="r") + cls.model = cls.createModel(cls.h5File) + + @classmethod + def createResource(cls, directory): + filename = os.path.join(directory, "base.h5") + externalFilename = os.path.join(directory, "base__external.h5") + + externalh5 = h5py.File(externalFilename, mode="w") + externalh5["target/dataset"] = 50 + externalh5["target/link"] = h5py.SoftLink("/target/dataset") + externalh5.close() + + h5 = h5py.File(filename, mode="w") + h5["group/dataset"] = 50 + h5["link/soft_link"] = h5py.SoftLink("/group/dataset") + h5["link/soft_link_to_group"] = h5py.SoftLink("/group") + h5["link/soft_link_to_link"] = h5py.SoftLink("/link/soft_link") + h5["link/soft_link_to_file"] = h5py.SoftLink("/") + h5["link/external_link"] = h5py.ExternalLink(externalFilename, "/target/dataset") + h5["link/external_link_to_link"] = h5py.ExternalLink(externalFilename, "/target/link") + h5["broken_link/external_broken_file"] = h5py.ExternalLink(externalFilename + "_not_exists", "/target/link") + h5["broken_link/external_broken_link"] = h5py.ExternalLink(externalFilename, "/target/not_exists") + h5["broken_link/soft_broken_link"] = h5py.SoftLink("/group/not_exists") + h5["broken_link/soft_link_to_broken_link"] = h5py.SoftLink("/group/not_exists") + h5.close() + + return filename + + @classmethod + def createModel(cls, h5pyFile): + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(h5pyFile) + return model + + @classmethod + def tearDownClass(cls): + cls.model = None + cls.h5File.close() + shutil.rmtree(cls.tmpDirectory) + super(TestH5Node, cls).tearDownClass() + + def getIndexFromPath(self, model, path): + """ + :param qt.QAbstractItemModel: model + """ + index = qt.QModelIndex() + for name in path: + for row in range(model.rowCount(index)): + i = model.index(row, 0, index) + label = model.data(i) + if label == name: + index = i + break + else: + raise RuntimeError("Path not found") + return index + + def getH5NodeFromPath(self, model, path): + index = self.getIndexFromPath(model, path) + item = model.data(index, hdf5.Hdf5TreeModel.H5PY_ITEM_ROLE) + h5node = hdf5.H5Node(item) + return h5node + + def testFile(self): + path = ["base.h5"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "") + self.assertEqual(h5node.physical_name, "/") + self.assertEqual(h5node.local_basename, "") + self.assertEqual(h5node.local_name, "/") + + def testGroup(self): + path = ["base.h5", "group"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "group") + self.assertEqual(h5node.physical_name, "/group") + self.assertEqual(h5node.local_basename, "group") + self.assertEqual(h5node.local_name, "/group") + + def testDataset(self): + path = ["base.h5", "group", "dataset"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "dataset") + self.assertEqual(h5node.physical_name, "/group/dataset") + self.assertEqual(h5node.local_basename, "dataset") + self.assertEqual(h5node.local_name, "/group/dataset") + + def testSoftLink(self): + path = ["base.h5", "link", "soft_link"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "dataset") + self.assertEqual(h5node.physical_name, "/group/dataset") + self.assertEqual(h5node.local_basename, "soft_link") + self.assertEqual(h5node.local_name, "/link/soft_link") + + def testSoftLinkToLink(self): + path = ["base.h5", "link", "soft_link_to_link"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "dataset") + self.assertEqual(h5node.physical_name, "/group/dataset") + self.assertEqual(h5node.local_basename, "soft_link_to_link") + self.assertEqual(h5node.local_name, "/link/soft_link_to_link") + + def testExternalLink(self): + path = ["base.h5", "link", "external_link"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertNotEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.local_filename) + self.assertIn("base__external.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "dataset") + self.assertEqual(h5node.physical_name, "/target/dataset") + self.assertEqual(h5node.local_basename, "external_link") + self.assertEqual(h5node.local_name, "/link/external_link") + + def testExternalLinkToLink(self): + path = ["base.h5", "link", "external_link_to_link"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertNotEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.local_filename) + self.assertIn("base__external.h5", h5node.physical_filename) + + self.assertNotEqual(h5node.physical_filename, h5node.local_filename) + self.assertEqual(h5node.physical_basename, "dataset") + self.assertEqual(h5node.physical_name, "/target/dataset") + self.assertEqual(h5node.local_basename, "external_link_to_link") + self.assertEqual(h5node.local_name, "/link/external_link_to_link") + + def testExternalBrokenFile(self): + path = ["base.h5", "broken_link", "external_broken_file"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertNotEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.local_filename) + self.assertIn("not_exists", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "link") + self.assertEqual(h5node.physical_name, "/target/link") + self.assertEqual(h5node.local_basename, "external_broken_file") + self.assertEqual(h5node.local_name, "/broken_link/external_broken_file") + + def testExternalBrokenLink(self): + path = ["base.h5", "broken_link", "external_broken_link"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertNotEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.local_filename) + self.assertIn("__external", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "not_exists") + self.assertEqual(h5node.physical_name, "/target/not_exists") + self.assertEqual(h5node.local_basename, "external_broken_link") + self.assertEqual(h5node.local_name, "/broken_link/external_broken_link") + + def testSoftBrokenLink(self): + path = ["base.h5", "broken_link", "soft_broken_link"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "not_exists") + self.assertEqual(h5node.physical_name, "/group/not_exists") + self.assertEqual(h5node.local_basename, "soft_broken_link") + self.assertEqual(h5node.local_name, "/broken_link/soft_broken_link") + + def testSoftLinkToBrokenLink(self): + path = ["base.h5", "broken_link", "soft_link_to_broken_link"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "not_exists") + self.assertEqual(h5node.physical_name, "/group/not_exists") + self.assertEqual(h5node.local_basename, "soft_link_to_broken_link") + self.assertEqual(h5node.local_name, "/broken_link/soft_link_to_broken_link") + + def testDatasetFromSoftLinkToGroup(self): + path = ["base.h5", "link", "soft_link_to_group", "dataset"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "dataset") + self.assertEqual(h5node.physical_name, "/group/dataset") + self.assertEqual(h5node.local_basename, "dataset") + self.assertEqual(h5node.local_name, "/link/soft_link_to_group/dataset") + + def testDatasetFromSoftLinkToFile(self): + path = ["base.h5", "link", "soft_link_to_file", "link", "soft_link_to_group", "dataset"] + h5node = self.getH5NodeFromPath(self.model, path) + + self.assertEqual(h5node.physical_filename, h5node.local_filename) + self.assertIn("base.h5", h5node.physical_filename) + self.assertEqual(h5node.physical_basename, "dataset") + self.assertEqual(h5node.physical_name, "/group/dataset") + self.assertEqual(h5node.local_basename, "dataset") + self.assertEqual(h5node.local_name, "/link/soft_link_to_file/link/soft_link_to_group/dataset") + + +class TestHdf5TreeView(TestCaseQt): """Test to check that icons module.""" def setUp(self): - super(TestHdf5, self).setUp() + super(TestHdf5TreeView, self).setUp() if h5py is None: self.skipTest("h5py is not available") @@ -464,15 +696,147 @@ class TestHdf5(TestCaseQt): view = hdf5.Hdf5TreeView() view._createContextMenu(qt.QPoint(0, 0)) + def testSelection_Simple(self): + tree = commonh5.File("/foo/bar/1.mock", "w") + item = tree.create_group("a/b/c/d") + item.create_group("e").create_group("f") + + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(tree) + view = hdf5.Hdf5TreeView() + view.setModel(model) + view.setSelectedH5Node(item) + + selected = list(view.selectedH5Nodes())[0] + self.assertIs(item, selected.h5py_object) + + def testSelection_NotFound(self): + tree2 = commonh5.File("/foo/bar/2.mock", "w") + tree = commonh5.File("/foo/bar/1.mock", "w") + item = tree.create_group("a/b/c/d") + item.create_group("e").create_group("f") + + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(tree) + view = hdf5.Hdf5TreeView() + view.setModel(model) + view.setSelectedH5Node(tree2) + + selection = list(view.selectedH5Nodes()) + self.assertEqual(len(selection), 0) + + def testSelection_ManyGroupFromSameFile(self): + tree = commonh5.File("/foo/bar/1.mock", "w") + group1 = tree.create_group("a1") + group2 = tree.create_group("a2") + group3 = tree.create_group("a3") + group1.create_group("b/c/d") + item = group2.create_group("b/c/d") + group3.create_group("b/c/d") + + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(group1) + model.insertH5pyObject(group2) + model.insertH5pyObject(group3) + view = hdf5.Hdf5TreeView() + view.setModel(model) + view.setSelectedH5Node(item) + + selected = list(view.selectedH5Nodes())[0] + self.assertIs(item, selected.h5py_object) + + def testSelection_RootFromSubTree(self): + tree = commonh5.File("/foo/bar/1.mock", "w") + group = tree.create_group("a1") + group.create_group("b/c/d") + + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(group) + view = hdf5.Hdf5TreeView() + view.setModel(model) + view.setSelectedH5Node(group) + + selected = list(view.selectedH5Nodes())[0] + self.assertIs(group, selected.h5py_object) + + def testSelection_FileFromSubTree(self): + tree = commonh5.File("/foo/bar/1.mock", "w") + group = tree.create_group("a1") + group.create_group("b").create_group("b").create_group("d") + + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(group) + view = hdf5.Hdf5TreeView() + view.setModel(model) + view.setSelectedH5Node(tree) + + selection = list(view.selectedH5Nodes()) + self.assertEquals(len(selection), 0) + + def testSelection_Tree(self): + tree1 = commonh5.File("/foo/bar/1.mock", "w") + tree2 = commonh5.File("/foo/bar/2.mock", "w") + tree3 = commonh5.File("/foo/bar/3.mock", "w") + tree1.create_group("a/b/c") + tree2.create_group("a/b/c") + tree3.create_group("a/b/c") + item = tree2 + + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(tree1) + model.insertH5pyObject(tree2) + model.insertH5pyObject(tree3) + view = hdf5.Hdf5TreeView() + view.setModel(model) + view.setSelectedH5Node(item) + + selected = list(view.selectedH5Nodes())[0] + self.assertIs(item, selected.h5py_object) + + def testSelection_RecurssiveLink(self): + """ + Recurssive link selection + + This example is not really working as expected cause commonh5 do not + support recurssive links. + But item.name == "/a/b" and the result is found. + """ + tree = commonh5.File("/foo/bar/1.mock", "w") + group = tree.create_group("a") + group.add_node(commonh5.SoftLink("b", "/")) + + item = tree["/a/b/a/b/a/b/a/b/a/b/a/b/a/b/a/b"] + + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(tree) + view = hdf5.Hdf5TreeView() + view.setModel(model) + view.setSelectedH5Node(item) + + selected = list(view.selectedH5Nodes())[0] + self.assertEqual(item.name, selected.h5py_object.name) + + def testSelection_SelectNone(self): + tree = commonh5.File("/foo/bar/1.mock", "w") + + model = hdf5.Hdf5TreeModel() + model.insertH5pyObject(tree) + view = hdf5.Hdf5TreeView() + view.setModel(model) + view.setSelectedH5Node(tree) + view.setSelectedH5Node(None) + + selection = list(view.selectedH5Nodes()) + self.assertEqual(len(selection), 0) + def suite(): test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestHdf5TreeModel)) - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestNexusSortFilterProxyModel)) - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestHdf5)) + loadTests = unittest.defaultTestLoader.loadTestsFromTestCase + test_suite.addTest(loadTests(TestHdf5TreeModel)) + test_suite.addTest(loadTests(TestNexusSortFilterProxyModel)) + test_suite.addTest(loadTests(TestHdf5TreeView)) + test_suite.addTest(loadTests(TestH5Node)) return test_suite |