summaryrefslogtreecommitdiff
path: root/silx/gui/hdf5
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/hdf5')
-rw-r--r--silx/gui/hdf5/Hdf5Formatter.py229
-rw-r--r--silx/gui/hdf5/Hdf5HeaderView.py29
-rw-r--r--silx/gui/hdf5/Hdf5Item.py182
-rw-r--r--silx/gui/hdf5/Hdf5Node.py29
-rw-r--r--silx/gui/hdf5/Hdf5TreeModel.py78
-rw-r--r--silx/gui/hdf5/Hdf5TreeView.py85
-rw-r--r--silx/gui/hdf5/NexusSortFilterProxyModel.py5
-rw-r--r--silx/gui/hdf5/_utils.py184
-rw-r--r--silx/gui/hdf5/test/_mock.py130
-rw-r--r--silx/gui/hdf5/test/test_hdf5.py454
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