diff options
Diffstat (limited to 'src/silx/gui/hdf5/Hdf5Item.py')
-rwxr-xr-x | src/silx/gui/hdf5/Hdf5Item.py | 642 |
1 files changed, 642 insertions, 0 deletions
diff --git a/src/silx/gui/hdf5/Hdf5Item.py b/src/silx/gui/hdf5/Hdf5Item.py new file mode 100755 index 0000000..e07f835 --- /dev/null +++ b/src/silx/gui/hdf5/Hdf5Item.py @@ -0,0 +1,642 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2019 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. +# +# ###########################################################################*/ + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "17/01/2019" + + +import logging +import collections +import enum + +from .. import qt +from .. import icons +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__) +_formatter = TextFormatter() +_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter) +# FIXME: The formatter should be an attribute of the Hdf5Model + + +class DescriptionType(enum.Enum): + """List of available kind of description. + """ + ERROR = "error" + DESCRIPTION = "description" + TITLE = "title" + PROGRAM = "program" + NAME = "name" + VALUE = "value" + + +class Hdf5Item(Hdf5Node): + """Subclass of :class:`qt.QStandardItem` to represent an HDF5-like + item (dataset, file, group or link) as an element of a HDF5-like + tree structure. + """ + + def __init__(self, text, obj, parent, key=None, h5Class=None, linkClass=None, populateAll=False): + """ + :param str text: text displayed + :param object obj: Pointer to a h5py-link object. See the `obj` attribute. + """ + self.__obj = obj + self.__key = key + self.__h5Class = h5Class + self.__isBroken = obj is None and h5Class is None + self.__error = None + self.__text = text + self.__linkClass = linkClass + self.__description = None + self.__nx_class = None + Hdf5Node.__init__(self, parent, populateAll=populateAll) + + def _getCanonicalName(self): + parent = self.parent + if parent is None: + return self.__text + else: + return "%s/%s" % (parent._getCanonicalName(), self.__text) + + @property + def obj(self): + if self.__key: + self.__initH5Object() + return self.__obj + + @property + def basename(self): + return self.__text + + @property + def h5Class(self): + """Returns the class of the stored object. + + When the object is in lazy loading, this method should be able to + return the type of the future loaded object. It allows to delay the + real load of the object. + + :rtype: silx.io.utils.H5Type + """ + if self.__h5Class is None and self.obj is not None: + self.__h5Class = silx.io.utils.get_h5_class(self.obj) + return self.__h5Class + + @property + def h5pyClass(self): + """Returns the class of the stored object. + + When the object is in lazy loading, this method should be able to + return the type of the future loaded object. It allows to delay the + real load of the object. + + :rtype: h5py.File or h5py.Dataset or h5py.Group + """ + type_ = self.h5Class + return silx.io.utils.h5type_to_h5py_class(type_) + + @property + def linkClass(self): + """Returns the link class object of this node + + :rtype: H5Type + """ + 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.h5Class is None: + return False + return self.h5Class in [silx.io.utils.H5Type.GROUP, silx.io.utils.H5Type.FILE] + + def isBrokenObj(self): + """Returns true if the stored HDF5 object is broken. + + The stored object is then an h5py-like link (external or not) which + point to nowhere (tbhe external file is not here, the expected + dataset is still not on the file...) + + :rtype: bool + """ + return self.__isBroken + + def _getFormatter(self): + """ + Returns an Hdf5Formatter + + :rtype: Hdf5Formatter + """ + return _hdf5Formatter + + def _expectedChildCount(self): + if self.isGroupObj(): + return len(self.obj) + return 0 + + def __initH5Object(self): + """Lazy load of the HDF5 node. It is reached from the parent node + with the key of the node.""" + parent_obj = self.parent.obj + + try: + obj = parent_obj.get(self.__key) + except Exception as e: + _logger.error("Internal error while reaching HDF5 object: %s", str(e)) + _logger.debug("Backtrace", exc_info=True) + try: + self.__obj = parent_obj.get(self.__key, getlink=True) + except Exception: + self.__obj = None + self.__error = e.args[0] + self.__isBroken = True + else: + if obj is None: + # that's a broken link + self.__obj = parent_obj.get(self.__key, getlink=True) + + # TODO monkey-patch file (ask that in h5py for consistency) + if not hasattr(self.__obj, "name"): + parent_name = parent_obj.name + if parent_name == "/": + self.__obj.name = "/" + self.__key + else: + self.__obj.name = parent_name + "/" + self.__key + # TODO monkey-patch file (ask that in h5py for consistency) + if not hasattr(self.__obj, "file"): + self.__obj.file = parent_obj.file + + class_ = silx.io.utils.get_h5_class(self.__obj) + + if class_ == silx.io.utils.H5Type.EXTERNAL_LINK: + message = "External link broken. Path %s::%s does not exist" % (self.__obj.filename, self.__obj.path) + elif class_ == silx.io.utils.H5Type.SOFT_LINK: + message = "Soft link broken. Path %s does not exist" % (self.__obj.path) + else: + name = self.__obj.__class__.__name__.split(".")[-1].capitalize() + message = "%s broken" % (name) + self.__error = message + 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 + + def _populateChild(self, populateAll=False): + if self.isGroupObj(): + keys = [] + try: + for name in self.obj: + keys.append(name) + except Exception: + lib_name = self.obj.__class__.__module__.split(".")[0] + _logger.error("Internal %s error. The file is corrupted.", lib_name) + _logger.debug("Backtrace", exc_info=True) + if keys == []: + # If the file was open in READ_ONLY we still can reach something + # https://github.com/silx-kit/silx/issues/2262 + try: + for name in self.obj: + keys.append(name) + except Exception: + lib_name = self.obj.__class__.__module__.split(".")[0] + _logger.error("Internal %s error (second time). The file is corrupted.", lib_name) + _logger.debug("Backtrace", exc_info=True) + for name in keys: + try: + class_ = self.obj.get(name, getclass=True) + link = self.obj.get(name, getclass=True, getlink=True) + link = silx.io.utils.get_h5_class(class_=link) + except Exception: + lib_name = self.obj.__class__.__module__.split(".")[0] + _logger.error("Internal %s error", lib_name) + _logger.debug("Backtrace", exc_info=True) + class_ = None + try: + link = self.obj.get(name, getclass=True, getlink=True) + link = silx.io.utils.get_h5_class(class_=link) + except Exception: + _logger.debug("Backtrace", exc_info=True) + link = silx.io.utils.H5Type.HARD_LINK + + h5class = None + if class_ is not None: + h5class = silx.io.utils.get_h5_class(class_=class_) + if h5class is None: + _logger.error("Class %s unsupported", class_) + item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5Class=h5class, linkClass=link) + self.appendChild(item) + + def hasChildren(self): + """Retuens true of this node have chrild. + + :rtype: bool + """ + if not self.isGroupObj(): + return False + return Hdf5Node.hasChildren(self) + + def _getDefaultIcon(self): + """Returns the icon displayed by the main column. + + :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) + return icon + class_ = self.h5Class + if class_ == silx.io.utils.H5Type.FILE: + return style.standardIcon(qt.QStyle.SP_FileIcon) + elif class_ == silx.io.utils.H5Type.GROUP: + return style.standardIcon(qt.QStyle.SP_DirIcon) + elif class_ == silx.io.utils.H5Type.SOFT_LINK: + return style.standardIcon(qt.QStyle.SP_DirLinkIcon) + elif class_ == silx.io.utils.H5Type.EXTERNAL_LINK: + return style.standardIcon(qt.QStyle.SP_FileLinkIcon) + elif class_ == silx.io.utils.H5Type.DATASET: + if obj.shape is None: + name = "item-none" + elif len(obj.shape) < 4: + name = "item-%ddim" % len(obj.shape) + else: + name = "item-ndim" + icon = icons.getQIcon(name) + return icon + return None + + 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 self.h5Class == silx.io.utils.H5Type.DATASET: + attributeDict["#Title"] = "HDF5 Dataset" + attributeDict["Name"] = self.basename + attributeDict["Path"] = self.obj.name + attributeDict["Shape"] = self._getFormatter().humanReadableShape(self.obj) + attributeDict["Value"] = self._getFormatter().humanReadableValue(self.obj) + attributeDict["Data type"] = self._getFormatter().humanReadableType(self.obj, full=True) + elif self.h5Class == silx.io.utils.H5Type.GROUP: + attributeDict["#Title"] = "HDF5 Group" + if self.nexusClassName: + attributeDict["NX_class"] = self.nexusClassName + attributeDict["Name"] = self.basename + attributeDict["Path"] = self.obj.name + elif self.h5Class == silx.io.utils.H5Type.FILE: + attributeDict["#Title"] = "HDF5 File" + attributeDict["Name"] = self.basename + attributeDict["Path"] = "/" + elif self.h5Class == silx.io.utils.H5Type.EXTERNAL_LINK: + attributeDict["#Title"] = "HDF5 External Link" + attributeDict["Name"] = self.basename + attributeDict["Path"] = self.obj.name + attributeDict["Linked path"] = self.obj.path + attributeDict["Linked file"] = self.obj.filename + elif self.h5Class == silx.io.utils.H5Type.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 + + :rtype: str + """ + if self.__error is not None: + self.obj # lazy loading of the object + return self.__error + + attrs = self._createTooltipAttributes() + title = attrs.pop("#Title", None) + if len(attrs) > 0: + tooltip = _utils.htmlFromDict(attrs, title=title) + else: + tooltip = "" + + return tooltip + + @property + def nexusClassName(self): + """Returns the Nexus class name""" + if self.__nx_class is None: + obj = self.obj.attrs.get("NX_class", None) + if obj is None: + text = "" + else: + text = self._getFormatter().textFormatter().toString(obj) + text = text.strip('"') + # Check NX_class formatting + lower = text.lower() + formatedNX_class = "" + if lower.startswith('nx'): + formatedNX_class = 'NX' + lower[2:] + if lower == 'nxcansas': + formatedNX_class = 'NXcanSAS' # That's the only class with capital letters... + if text != formatedNX_class: + _logger.error("NX_class: '%s' is malformed (should be '%s')", + text, + formatedNX_class) + text = formatedNX_class + + self.__nx_class = text + return self.__nx_class + + def dataName(self, role): + """Data for the name column""" + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + return self.__text + if role == qt.Qt.DecorationRole: + return self._getDefaultIcon() + if role == qt.Qt.ToolTipRole: + return self._getDefaultTooltip() + return None + + def dataType(self, role): + """Data for the type column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + if self.__error is not None: + return "" + class_ = self.h5Class + if self.isGroupObj(): + text = self.nexusClassName + elif class_ == silx.io.utils.H5Type.DATASET: + text = self._getFormatter().humanReadableType(self.obj) + else: + text = "" + return text + return None + + def dataShape(self, role): + """Data for the shape column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + if self.__error is not None: + return "" + class_ = self.h5Class + if class_ != silx.io.utils.H5Type.DATASET: + return "" + return self._getFormatter().humanReadableShape(self.obj) + return None + + def dataValue(self, role): + """Data for the value column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + if self.__error is not None: + return "" + if self.h5Class != silx.io.utils.H5Type.DATASET: + return "" + return self._getFormatter().humanReadableValue(self.obj) + return None + + _NEXUS_CLASS_TO_VALUE_CHILDREN = { + 'NXaperture': ( + (DescriptionType.DESCRIPTION, 'description'), + ), + 'NXbeam_stop': ( + (DescriptionType.DESCRIPTION, 'description'), + ), + 'NXdetector': ( + (DescriptionType.NAME, 'local_name'), + (DescriptionType.DESCRIPTION, 'description') + ), + 'NXentry': ( + (DescriptionType.TITLE, 'title'), + ), + 'NXenvironment': ( + (DescriptionType.NAME, 'short_name'), + (DescriptionType.NAME, 'name'), + (DescriptionType.DESCRIPTION, 'description') + ), + 'NXinstrument': ( + (DescriptionType.NAME, 'name'), + ), + 'NXlog': ( + (DescriptionType.DESCRIPTION, 'description'), + ), + 'NXmirror': ( + (DescriptionType.DESCRIPTION, 'description'), + ), + 'NXpositioner': ( + (DescriptionType.NAME, 'name'), + ), + 'NXprocess': ( + (DescriptionType.PROGRAM, 'program'), + ), + 'NXsample': ( + (DescriptionType.TITLE, 'short_title'), + (DescriptionType.NAME, 'name'), + (DescriptionType.DESCRIPTION, 'description') + ), + 'NXsample_component': ( + (DescriptionType.NAME, 'name'), + (DescriptionType.DESCRIPTION, 'description') + ), + 'NXsensor': ( + (DescriptionType.NAME, 'short_name'), + (DescriptionType.NAME, 'name') + ), + 'NXsource': ( + (DescriptionType.NAME, 'name'), + ), # or its 'short_name' attribute... This is not supported + 'NXsubentry': ( + (DescriptionType.DESCRIPTION, 'definition'), + (DescriptionType.PROGRAM, 'program_name'), + (DescriptionType.TITLE, 'title'), + ), + } + """Mapping from NeXus class to child names containing data to use as value""" + + def __computeDataDescription(self): + """Compute the data description of this item + + :rtype: Tuple[kind, str] + """ + if self.__isBroken or self.__error is not None: + self.obj # lazy loading of the object + return DescriptionType.ERROR, self.__error + + if self.h5Class == silx.io.utils.H5Type.DATASET: + return DescriptionType.VALUE, self._getFormatter().humanReadableValue(self.obj) + + elif self.isGroupObj() and self.nexusClassName: + # For NeXus groups, try to find a title or name + # By default, look for a title (most application definitions should have one) + defaultSequence = ((DescriptionType.TITLE, 'title'),) + sequence = self._NEXUS_CLASS_TO_VALUE_CHILDREN.get(self.nexusClassName, defaultSequence) + for kind, child_name in sequence: + for index in range(self.childCount()): + child = self.child(index) + if (isinstance(child, Hdf5Item) and + child.h5Class == silx.io.utils.H5Type.DATASET and + child.basename == child_name): + return kind, self._getFormatter().humanReadableValue(child.obj) + + description = self.obj.attrs.get("desc", None) + if description is not None: + return DescriptionType.DESCRIPTION, description + else: + return None, None + + def __getDataDescription(self): + """Returns a cached version of the data description + + As the data description have to reach inside the HDF5 tree, the result + is cached. A better implementation could be to use a MRU cache, to avoid + to allocate too much data. + + :rtype: Tuple[kind, str] + """ + if self.__description is None: + self.__description = self.__computeDataDescription() + return self.__description + + def dataDescription(self, role): + """Data for the description column""" + if role == qt.Qt.DecorationRole: + kind, _label = self.__getDataDescription() + if kind is not None: + icon = icons.getQIcon("description-%s" % kind.value) + return icon + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + _kind, label = self.__getDataDescription() + return label + if role == qt.Qt.ToolTipRole: + if self.__error is not None: + self.obj # lazy loading of the object + self.__initH5Object() + return self.__error + kind, label = self.__getDataDescription() + if label is not None: + return "<b>%s</b><br/>%s" % (kind.value.capitalize(), label) + else: + return "" + return None + + def dataNode(self, role): + """Data for the node column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + if self.isBrokenObj(): + return "" + class_ = self.obj.__class__ + text = class_.__name__.split(".")[-1] + return text + if role == qt.Qt.ToolTipRole: + class_ = self.obj.__class__ + 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: + # Mark as link + link = self.linkClass + if link is None: + pass + elif link == silx.io.utils.H5Type.HARD_LINK: + pass + elif link == silx.io.utils.H5Type.EXTERNAL_LINK: + return "External" + elif link == silx.io.utils.H5Type.SOFT_LINK: + return "Soft" + else: + return link.__name__ + # Mark as external data + if self.h5Class == silx.io.utils.H5Type.DATASET: + obj = self.obj + if hasattr(obj, "is_virtual"): + if obj.is_virtual: + return "Virtual" + if hasattr(obj, "external"): + if obj.external: + return "ExtRaw" + return "" + if role == qt.Qt.ToolTipRole: + return None + return None |