diff options
Diffstat (limited to 'src/silx/gui/hdf5')
-rw-r--r-- | src/silx/gui/hdf5/Hdf5Formatter.py | 9 | ||||
-rw-r--r-- | src/silx/gui/hdf5/Hdf5HeaderView.py | 60 | ||||
-rwxr-xr-x | src/silx/gui/hdf5/Hdf5Item.py | 157 | ||||
-rw-r--r-- | src/silx/gui/hdf5/Hdf5Node.py | 3 | ||||
-rw-r--r-- | src/silx/gui/hdf5/Hdf5TreeModel.py | 100 | ||||
-rw-r--r-- | src/silx/gui/hdf5/Hdf5TreeView.py | 17 | ||||
-rw-r--r-- | src/silx/gui/hdf5/NexusSortFilterProxyModel.py | 7 | ||||
-rw-r--r-- | src/silx/gui/hdf5/__init__.py | 8 | ||||
-rw-r--r-- | src/silx/gui/hdf5/_utils.py | 15 | ||||
-rwxr-xr-x | src/silx/gui/hdf5/test/test_hdf5.py | 282 |
10 files changed, 440 insertions, 218 deletions
diff --git a/src/silx/gui/hdf5/Hdf5Formatter.py b/src/silx/gui/hdf5/Hdf5Formatter.py index 4dbb0fc..99e0bb6 100644 --- a/src/silx/gui/hdf5/Hdf5Formatter.py +++ b/src/silx/gui/hdf5/Hdf5Formatter.py @@ -37,8 +37,7 @@ import h5py class Hdf5Formatter(qt.QObject): - """Formatter to convert HDF5 data to string. - """ + """Formatter to convert HDF5 data to string.""" formatChanged = qt.Signal() """Emitted when properties of the formatter change.""" @@ -87,7 +86,7 @@ class Hdf5Formatter(qt.QObject): if dataset.shape == tuple(): return "scalar" shape = [str(i) for i in dataset.shape] - text = u" \u00D7 ".join(shape) + text = " \u00D7 ".join(shape) return text def humanReadableValue(self, dataset): @@ -162,7 +161,7 @@ class Hdf5Formatter(qt.QObject): if enumType is not None: return "enum" - text = str(dtype.newbyteorder('N')) + text = str(dtype.newbyteorder("N")) if numpy.issubdtype(dtype, numpy.floating): if hasattr(numpy, "float128") and dtype == numpy.float128: text = "float80" @@ -181,7 +180,7 @@ class Hdf5Formatter(qt.QObject): elif dtype.byteorder == "=": text = "Native " + text - dtype = dtype.newbyteorder('N') + dtype = dtype.newbyteorder("N") return text def humanReadableHdf5Type(self, dataset): diff --git a/src/silx/gui/hdf5/Hdf5HeaderView.py b/src/silx/gui/hdf5/Hdf5HeaderView.py index 6d306e5..16323dd 100644 --- a/src/silx/gui/hdf5/Hdf5HeaderView.py +++ b/src/silx/gui/hdf5/Hdf5HeaderView.py @@ -72,21 +72,49 @@ class Hdf5HeaderView(qt.QHeaderView): def __updateAutoResize(self): """Update the view according to the state of the auto-resize""" if self.__auto_resize: - self.setSectionResizeMode(Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.ResizeToContents) - self.setSectionResizeMode(Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.ResizeToContents) - self.setSectionResizeMode(Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.ResizeToContents) - self.setSectionResizeMode(Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive) - self.setSectionResizeMode(Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive) - self.setSectionResizeMode(Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.ResizeToContents) - self.setSectionResizeMode(Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.ResizeToContents) + self.setSectionResizeMode( + Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.ResizeToContents + ) + self.setSectionResizeMode( + Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.ResizeToContents + ) + self.setSectionResizeMode( + Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.ResizeToContents + ) + self.setSectionResizeMode( + Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive + ) + self.setSectionResizeMode( + Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive + ) + self.setSectionResizeMode( + Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.ResizeToContents + ) + self.setSectionResizeMode( + Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.ResizeToContents + ) else: - self.setSectionResizeMode(Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.Interactive) - self.setSectionResizeMode(Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.Interactive) - self.setSectionResizeMode(Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.Interactive) - self.setSectionResizeMode(Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive) - self.setSectionResizeMode(Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive) - self.setSectionResizeMode(Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.Interactive) - self.setSectionResizeMode(Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.Interactive) + self.setSectionResizeMode( + Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.Interactive + ) + self.setSectionResizeMode( + Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.Interactive + ) + self.setSectionResizeMode( + Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.Interactive + ) + self.setSectionResizeMode( + Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive + ) + self.setSectionResizeMode( + Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive + ) + self.setSectionResizeMode( + Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.Interactive + ) + self.setSectionResizeMode( + Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.Interactive + ) def setAutoResizeColumns(self, autoResize): """Enable/disable auto-resize. When auto-resized, the header take care @@ -125,7 +153,9 @@ class Hdf5HeaderView(qt.QHeaderView): """ return self.__hide_columns_popup - enableHideColumnsPopup = qt.Property(bool, hasHideColumnsPopup, setAutoResizeColumns) + enableHideColumnsPopup = qt.Property( + bool, hasHideColumnsPopup, setAutoResizeColumns + ) """Property to enable/disable popup allowing to hide/show columns.""" def __genHideSectionEvent(self, column): diff --git a/src/silx/gui/hdf5/Hdf5Item.py b/src/silx/gui/hdf5/Hdf5Item.py index 8f20649..2777a94 100755 --- a/src/silx/gui/hdf5/Hdf5Item.py +++ b/src/silx/gui/hdf5/Hdf5Item.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility +# Copyright (c) 2016-2023 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 @@ -28,7 +28,6 @@ __date__ = "17/01/2019" import logging -import collections import enum from typing import Optional @@ -39,6 +38,7 @@ 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) @@ -46,8 +46,8 @@ _hdf5Formatter = Hdf5Formatter(textFormatter=_formatter) class DescriptionType(enum.Enum): - """List of available kind of description. - """ + """List of available kind of description.""" + ERROR = "error" DESCRIPTION = "description" TITLE = "title" @@ -210,9 +210,14 @@ class Hdf5Item(Hdf5Node): 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) + 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) + 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) @@ -220,7 +225,10 @@ class Hdf5Item(Hdf5Node): self.__isBroken = True else: self.__obj = obj - if silx.io.utils.get_h5_class(obj) not in [silx.io.utils.H5Type.GROUP, silx.io.utils.H5Type.FILE]: + if silx.io.utils.get_h5_class(obj) not in [ + silx.io.utils.H5Type.GROUP, + silx.io.utils.H5Type.FILE, + ]: try: # pre-fetch of the data if obj.shape is None: @@ -257,7 +265,10 @@ class Hdf5Item(Hdf5Node): 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.error( + "Internal %s error (second time). The file is corrupted.", + lib_name, + ) _logger.debug("Backtrace", exc_info=True) for name in keys: try: @@ -281,7 +292,14 @@ class Hdf5Item(Hdf5Node): 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) + item = Hdf5Item( + text=name, + obj=None, + parent=self, + key=name, + h5Class=h5class, + linkClass=link, + ) self.appendChild(item) def hasChildren(self): @@ -330,7 +348,7 @@ class Hdf5Item(Hdf5Node): :param Dict[str,str] attributeDict: Key/value attributes """ - attributeDict = collections.OrderedDict() + attributeDict = {} if self.h5Class == silx.io.utils.H5Type.DATASET: attributeDict["#Title"] = "HDF5 Dataset" @@ -338,7 +356,9 @@ class Hdf5Item(Hdf5Node): 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) + 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: @@ -395,14 +415,18 @@ class Hdf5Item(Hdf5Node): # 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 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) + _logger.error( + "NX_class: '%s' is malformed (should be '%s')", + text, + formatedNX_class, + ) text = formatedNX_class self.__nx_class = text @@ -469,59 +493,44 @@ class Hdf5Item(Hdf5Node): 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'), + "NXaperture": ((DescriptionType.DESCRIPTION, "description"),), + "NXbeam_stop": ((DescriptionType.DESCRIPTION, "description"),), + "NXdetector": ( + (DescriptionType.NAME, "local_name"), + (DescriptionType.DESCRIPTION, "description"), ), - 'NXprocess': ( - (DescriptionType.PROGRAM, 'program'), + "NXentry": ((DescriptionType.TITLE, "title"),), + "NXenvironment": ( + (DescriptionType.NAME, "short_name"), + (DescriptionType.NAME, "name"), + (DescriptionType.DESCRIPTION, "description"), ), - 'NXsample': ( - (DescriptionType.TITLE, 'short_title'), - (DescriptionType.NAME, 'name'), - (DescriptionType.DESCRIPTION, 'description') + "NXinstrument": ((DescriptionType.NAME, "name"),), + "NXlog": ((DescriptionType.DESCRIPTION, "description"),), + "NXmirror": ((DescriptionType.DESCRIPTION, "description"),), + "NXnote": ((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') + "NXsample_component": ( + (DescriptionType.NAME, "name"), + (DescriptionType.DESCRIPTION, "description"), ), - 'NXsensor': ( - (DescriptionType.NAME, 'short_name'), - (DescriptionType.NAME, 'name') + "NXsensor": ( + (DescriptionType.NAME, "short_name"), + (DescriptionType.NAME, "name"), ), - 'NXsource': ( - (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'), + "NXsubentry": ( + (DescriptionType.DESCRIPTION, "definition"), + (DescriptionType.PROGRAM, "program_name"), + (DescriptionType.TITLE, "title"), ), } """Mapping from NeXus class to child names containing data to use as value""" @@ -536,19 +545,25 @@ class Hdf5Item(Hdf5Node): return DescriptionType.ERROR, self.__error if self.h5Class == silx.io.utils.H5Type.DATASET: - return DescriptionType.VALUE, self._getFormatter().humanReadableValue(self.obj) + 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) + 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): + 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) diff --git a/src/silx/gui/hdf5/Hdf5Node.py b/src/silx/gui/hdf5/Hdf5Node.py index 0d58748..db49594 100644 --- a/src/silx/gui/hdf5/Hdf5Node.py +++ b/src/silx/gui/hdf5/Hdf5Node.py @@ -36,11 +36,12 @@ class Hdf5Node(object): It provides link to the childs and to the parents, and a link to an external object. """ + def __init__( self, parent=None, populateAll=False, - openedPath: Optional[str]=None, + openedPath: Optional[str] = None, ): """ Constructor diff --git a/src/silx/gui/hdf5/Hdf5TreeModel.py b/src/silx/gui/hdf5/Hdf5TreeModel.py index 8ac800a..3353ab3 100644 --- a/src/silx/gui/hdf5/Hdf5TreeModel.py +++ b/src/silx/gui/hdf5/Hdf5TreeModel.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2023 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 @@ -38,6 +38,7 @@ from .Hdf5Item import Hdf5Item from .Hdf5LoadingItem import Hdf5LoadingItem from . import _utils from ... import io as silx_io +from ...io._sliceh5 import DatasetSlice import h5py @@ -61,6 +62,8 @@ def _createRootLabel(h5obj): if path.startswith("/"): path = path[1:] label = "%s::%s" % (filename, path) + if isinstance(h5obj, DatasetSlice): + label += str(list(h5obj.indices)) return label @@ -69,7 +72,8 @@ class LoadingItemRunnable(qt.QRunnable): class __Signals(qt.QObject): """Signal holder""" - itemReady = qt.Signal(object, object, object) + + itemReady = qt.Signal(object, object, object, str) runnerFinished = qt.Signal(object) def __init__(self, filename, item): @@ -126,7 +130,7 @@ class LoadingItemRunnable(qt.QRunnable): if h5file is not None: h5file.close() - self.itemReady.emit(self.oldItem, newItem, error) + self.itemReady.emit(self.oldItem, newItem, error, self.filename) self.runnerFinished.emit(self) def autoDelete(self): @@ -181,7 +185,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): ] """List of logical columns available""" - sigH5pyObjectLoaded = qt.Signal(object) + sigH5pyObjectLoaded = qt.Signal(object, str) """Emitted when a new root item was loaded and inserted to the model.""" sigH5pyObjectRemoved = qt.Signal(object) @@ -201,13 +205,13 @@ class Hdf5TreeModel(qt.QAbstractItemModel): super(Hdf5TreeModel, self).__init__(parent) 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' + 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() @@ -247,7 +251,6 @@ class Hdf5TreeModel(qt.QAbstractItemModel): """Static method to close explicit references to internal objects.""" _logger.debug("Clear Hdf5TreeModel") for obj in fileList: - _logger.debug("Close file %s", obj.filename) obj.close() fileList[:] = [] @@ -266,14 +269,21 @@ class Hdf5TreeModel(qt.QAbstractItemModel): index2 = self.index(i, self.columnCount() - 1, qt.QModelIndex()) self.dataChanged.emit(index1, index2) - def __itemReady(self, oldItem, newItem, error): + def __itemReady( + self, + oldItem: Hdf5Node, + newItem: Optional[Hdf5Node], + error: Optional[Exception], + filename: str, + ): """Called at the end of a concurent file loading, when the loading item is ready. AN error is defined if an exception occured when loading the newItem . - :param Hdf5Node oldItem: current displayed item - :param Hdf5Node newItem: item loaded, or None if error is defined - :param Exception error: An exception, or None if newItem is defined + :param oldItem: current displayed item + :param newItem: item loaded, or None if error is defined + :param error: An exception, or None if newItem is defined + :param filename: The filename used to load the new item """ row = self.__root.indexOfChild(oldItem) @@ -291,7 +301,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): self.endInsertRows() if isinstance(oldItem, Hdf5LoadingItem): - self.sigH5pyObjectLoaded.emit(newItem.obj) + self.sigH5pyObjectLoaded.emit(newItem.obj, filename) else: self.sigH5pyObjectSynchronized.emit(oldItem.obj, newItem.obj) @@ -384,7 +394,9 @@ class Hdf5TreeModel(qt.QAbstractItemModel): if action == qt.Qt.IgnoreAction: return True - if self.__fileMoveEnabled and mimedata.hasFormat(_utils.Hdf5DatasetMimeData.MIME_TYPE): + if self.__fileMoveEnabled and mimedata.hasFormat( + _utils.Hdf5DatasetMimeData.MIME_TYPE + ): if mimedata.isRoot(): dragNode = mimedata.node() parentNode = self.nodeFromIndex(parentIndex) @@ -404,10 +416,9 @@ class Hdf5TreeModel(qt.QAbstractItemModel): return True if self.__fileDropEnabled and mimedata.hasFormat("text/uri-list"): - parentNode = self.nodeFromIndex(parentIndex) if parentNode is not self.__root: - while(parentNode is not self.__root): + while parentNode is not self.__root: node = parentNode parentNode = node.parent row = parentNode.indexOfChild(node) @@ -424,7 +435,10 @@ class Hdf5TreeModel(qt.QAbstractItemModel): messages.append(e.args[0]) if len(messages) > 0: title = "Error occurred when loading files" - message = "<html>%s:<ul><li>%s</li><ul></html>" % (title, "</li><li>".join(messages)) + message = "<html>%s:<ul><li>%s</li><ul></html>" % ( + title, + "</li><li>".join(messages), + ) qt.QMessageBox.critical(None, title, message) return True @@ -443,14 +457,31 @@ class Hdf5TreeModel(qt.QAbstractItemModel): self.__root.insertChild(row, node) self.endInsertRows() - def moveRow(self, sourceParentIndex, sourceRow, destinationParentIndex, destinationRow): + def moveRow( + self, sourceParentIndex, sourceRow, destinationParentIndex, destinationRow + ): if sourceRow == destinationRow or sourceRow == destinationRow - 1: # abort move, same place return - return self.moveRows(sourceParentIndex, sourceRow, 1, destinationParentIndex, destinationRow) + return self.moveRows( + sourceParentIndex, sourceRow, 1, destinationParentIndex, destinationRow + ) - def moveRows(self, sourceParentIndex, sourceRow, count, destinationParentIndex, destinationRow): - self.beginMoveRows(sourceParentIndex, sourceRow, sourceRow, destinationParentIndex, destinationRow) + def moveRows( + self, + sourceParentIndex, + sourceRow, + count, + destinationParentIndex, + destinationRow, + ): + self.beginMoveRows( + sourceParentIndex, + sourceRow, + sourceRow, + destinationParentIndex, + destinationRow, + ) sourceNode = self.nodeFromIndex(sourceParentIndex) destinationNode = self.nodeFromIndex(destinationParentIndex) @@ -532,14 +563,14 @@ class Hdf5TreeModel(qt.QAbstractItemModel): return qt.QModelIndex() row = grandparent.indexOfChild(parent) - assert row != - 1 + assert row != -1 return self.createIndex(row, 0, parent) def nodeFromIndex(self, index): return index.internalPointer() if index.isValid() else self.__root def _closeFileIfOwned(self, node): - """"Close the file if it was loaded from a filename or a + """Close the file if it was loaded from a filename or a drag-and-drop""" obj = node.obj for f in self.__openedFiles: @@ -572,10 +603,15 @@ class Hdf5TreeModel(qt.QAbstractItemModel): # else compare commonh5 objects if not isinstance(obj2, type(obj1)): return False + def key(item): - if item.file is None: - return item.name - return item.file.filename, item.file.mode, item.name + info = [item.name] + if item.file is not None: + info += [item.file.filename, item.file.mode] + if isinstance(item, DatasetSlice): + info.append(item.indices) + return tuple(info) + return key(obj1) == key(obj2) def h5pyObjectRow(self, h5pyObject): @@ -655,7 +691,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): obj=h5pyObject, parent=self.__root, openedPath=filename, - ) + ), ) def hasPendingOperations(self): @@ -697,7 +733,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): h5file = silx_io.open(filename) if self.__ownFiles: self.__openedFiles.append(h5file) - self.sigH5pyObjectLoaded.emit(h5file) + self.sigH5pyObjectLoaded.emit(h5file, filename) self.insertH5pyObject(h5file, row=row, filename=filename) except IOError: _logger.debug("File '%s' can't be read.", filename, exc_info=True) diff --git a/src/silx/gui/hdf5/Hdf5TreeView.py b/src/silx/gui/hdf5/Hdf5TreeView.py index da35d15..a477fc3 100644 --- a/src/silx/gui/hdf5/Hdf5TreeView.py +++ b/src/silx/gui/hdf5/Hdf5TreeView.py @@ -57,6 +57,7 @@ class Hdf5TreeView(qt.QTreeView): :meth:`removeContextMenuCallback` to add your custum actions according to the selected objects. """ + def __init__(self, parent=None): """ Constructor @@ -167,7 +168,11 @@ class Hdf5TreeView(qt.QTreeView): def dragEnterEvent(self, event): model = self.findHdf5TreeModel() - if model is not None and model.isFileDropEnabled() and event.mimeData().hasFormat("text/uri-list"): + if ( + model is not None + and model.isFileDropEnabled() + and event.mimeData().hasFormat("text/uri-list") + ): self.setState(qt.QAbstractItemView.DraggingState) event.accept() else: @@ -175,7 +180,11 @@ class Hdf5TreeView(qt.QTreeView): def dragMoveEvent(self, event): model = self.findHdf5TreeModel() - if model is not None and model.isFileDropEnabled() and event.mimeData().hasFormat("text/uri-list"): + if ( + model is not None + and model.isFileDropEnabled() + and event.mimeData().hasFormat("text/uri-list") + ): event.setDropAction(qt.Qt.CopyAction) event.accept() else: @@ -215,7 +224,9 @@ class Hdf5TreeView(qt.QTreeView): model = model.sourceModel() else: break - raise RuntimeError("Model from the requested index is not reachable from this view") + raise RuntimeError( + "Model from the requested index is not reachable from this view" + ) def mapToModel(self, index): """Map an index from any model reachable by the view to an index from diff --git a/src/silx/gui/hdf5/NexusSortFilterProxyModel.py b/src/silx/gui/hdf5/NexusSortFilterProxyModel.py index 1b80c3e..0bc7352 100644 --- a/src/silx/gui/hdf5/NexusSortFilterProxyModel.py +++ b/src/silx/gui/hdf5/NexusSortFilterProxyModel.py @@ -76,7 +76,8 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel): """ if sourceLeft.column() != Hdf5TreeModel.NAME_COLUMN: return super(NexusSortFilterProxyModel, self).lessThan( - sourceLeft, sourceRight) + sourceLeft, sourceRight + ) # Do not sort child of root (files) if sourceLeft.parent() == qt.QModelIndex(): @@ -217,7 +218,9 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel): if index.column() == Hdf5TreeModel.NAME_COLUMN: if role == qt.Qt.DecorationRole: sourceIndex = self.mapToSource(index) - item = self.sourceModel().data(sourceIndex, Hdf5TreeModel.H5PY_ITEM_ROLE) + item = self.sourceModel().data( + sourceIndex, Hdf5TreeModel.H5PY_ITEM_ROLE + ) if self.__isNXnode(item): result = self.__getNxIcon(result) return result diff --git a/src/silx/gui/hdf5/__init__.py b/src/silx/gui/hdf5/__init__.py index 2243484..8e07407 100644 --- a/src/silx/gui/hdf5/__init__.py +++ b/src/silx/gui/hdf5/__init__.py @@ -40,4 +40,10 @@ from ._utils import Hdf5ContextMenuEvent # noqa from .NexusSortFilterProxyModel import NexusSortFilterProxyModel # noqa from .Hdf5TreeModel import Hdf5TreeModel # noqa -__all__ = ['Hdf5TreeView', 'H5Node', 'Hdf5ContextMenuEvent', 'NexusSortFilterProxyModel', 'Hdf5TreeModel'] +__all__ = [ + "Hdf5TreeView", + "H5Node", + "Hdf5ContextMenuEvent", + "NexusSortFilterProxyModel", + "Hdf5TreeModel", +] diff --git a/src/silx/gui/hdf5/_utils.py b/src/silx/gui/hdf5/_utils.py index 1d1b4cb..7232bfe 100644 --- a/src/silx/gui/hdf5/_utils.py +++ b/src/silx/gui/hdf5/_utils.py @@ -33,7 +33,7 @@ __date__ = "17/01/2019" from html import escape import logging import os.path - +from silx.gui import constants import silx.io.utils import silx.io.url from .. import qt @@ -109,19 +109,20 @@ class Hdf5DatasetMimeData(qt.QMimeData): MIME_TYPE = "application/x-internal-h5py-dataset" - SILX_URI_TYPE = "application/x-silx-uri" + SILX_URI_TYPE = constants.SILX_URI_MIMETYPE + """For compatibility with silx <= 1.1""" def __init__(self, node=None, dataset=None, isRoot=False): qt.QMimeData.__init__(self) self.__dataset = dataset self.__node = node self.__isRoot = isRoot - self.setData(self.MIME_TYPE, "".encode(encoding='utf-8')) + self.setData(self.MIME_TYPE, "".encode(encoding="utf-8")) if node is not None: h5Node = H5Node(node) silxUrl = h5Node.url self.setText(silxUrl) - self.setData(self.SILX_URI_TYPE, silxUrl.encode(encoding='utf-8')) + self.setData(constants.SILX_URI_MIMETYPE, silxUrl.encode(encoding="utf-8")) def isRoot(self): return self.__isRoot @@ -427,9 +428,9 @@ class H5Node(object): :rtype: ~silx.io.url.DataUrl """ absolute_filename = os.path.abspath(self.local_filename) - return silx.io.url.DataUrl(scheme="silx", - file_path=absolute_filename, - data_path=self.local_name) + return silx.io.url.DataUrl( + scheme="silx", file_path=absolute_filename, data_path=self.local_name + ) @property def url(self): diff --git a/src/silx/gui/hdf5/test/test_hdf5.py b/src/silx/gui/hdf5/test/test_hdf5.py index 6e77e1d..cb08436 100755 --- a/src/silx/gui/hdf5/test/test_hdf5.py +++ b/src/silx/gui/hdf5/test/test_hdf5.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2023 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 @@ -30,23 +30,24 @@ __date__ = "12/03/2019" import time import os -import unittest import tempfile import numpy -from pkg_resources import parse_version +from packaging.version import Version from contextlib import contextmanager from silx.gui import qt from silx.gui.utils.testutils import TestCaseQt from silx.gui import hdf5 from silx.gui.utils.testutils import SignalListener from silx.io import commonh5 +from silx.io import h5py_utils +from silx.io.url import DataUrl import weakref import h5py import pytest -h5py2_9 = parse_version(h5py.version.version) >= parse_version('2.9.0') +h5py2_9 = Version(h5py.version.version) >= Version("2.9.0") @pytest.fixture(scope="class") @@ -54,7 +55,7 @@ def useH5File(request, tmpdir_factory): tmp = tmpdir_factory.mktemp("test_hdf5") request.cls.filename = os.path.join(tmp, "data.h5") # create h5 data - with h5py.File(request.cls.filename, "w") as f: + with h5py_utils.File(request.cls.filename, "w") as f: g = f.create_group("arrays") g.create_dataset("scalar", data=10) yield @@ -69,7 +70,6 @@ def create_NXentry(group, name): @pytest.mark.usefixtures("useH5File") class TestHdf5TreeModel(TestCaseQt): - def setUp(self): super(TestHdf5TreeModel, self).setUp() @@ -87,7 +87,7 @@ class TestHdf5TreeModel(TestCaseQt): fd, tmp_name = tempfile.mkstemp(suffix=".h5") os.close(fd) # create h5 data - h5file = h5py.File(tmp_name, "w") + h5file = h5py_utils.File(tmp_name, "w") g = h5file.create_group("arrays") g.create_dataset("scalar", data=10) h5file.close() @@ -134,7 +134,9 @@ class TestHdf5TreeModel(TestCaseQt): self.assertEqual(model.rowCount(qt.QModelIndex()), 0) model.insertFileAsync(self.filename) index = model.index(0, 0, qt.QModelIndex()) - self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5LoadingItem.Hdf5LoadingItem) + self.assertIsInstance( + model.nodeFromIndex(index), hdf5.Hdf5LoadingItem.Hdf5LoadingItem + ) self.waitForPendingOperations(model) index = model.index(0, 0, qt.QModelIndex()) self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5Item.Hdf5Item) @@ -160,7 +162,7 @@ class TestHdf5TreeModel(TestCaseQt): self.assertEqual(model.rowCount(qt.QModelIndex()), 0) def testSynchronizeObject(self): - h5 = h5py.File(self.filename, mode="r") + h5 = h5py_utils.File(self.filename, mode="r") model = hdf5.Hdf5TreeModel() model.insertH5pyObject(h5) self.assertEqual(model.rowCount(qt.QModelIndex()), 1) @@ -235,7 +237,7 @@ class TestHdf5TreeModel(TestCaseQt): """A file inserted as an h5py object is not open (then not closed) internally.""" try: - h5File = h5py.File(self.filename, mode="r") + h5File = h5py_utils.File(self.filename, mode="r") model = hdf5.Hdf5TreeModel() self.assertEqual(model.rowCount(qt.QModelIndex()), 0) model.insertH5pyObject(h5File) @@ -244,7 +246,9 @@ class TestHdf5TreeModel(TestCaseQt): h5File = model.data(index, role=hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) model.removeIndex(index) self.assertEqual(model.rowCount(qt.QModelIndex()), 0) - self.assertTrue(bool(h5File.id.valid), "The HDF5 file was unexpetedly closed") + self.assertTrue( + bool(h5File.id.valid), "The HDF5 file was unexpetedly closed" + ) finally: h5File.close() @@ -269,7 +273,12 @@ class TestHdf5TreeModel(TestCaseQt): def getRowDataAsDict(self, model, row): displayed = {} - roles = [qt.Qt.DisplayRole, qt.Qt.DecorationRole, qt.Qt.ToolTipRole, qt.Qt.TextAlignmentRole] + roles = [ + qt.Qt.DisplayRole, + qt.Qt.DecorationRole, + qt.Qt.ToolTipRole, + qt.Qt.TextAlignmentRole, + ] for column in range(0, model.columnCount(qt.QModelIndex())): index = model.index(0, column, qt.QModelIndex()) for role in roles: @@ -286,13 +295,27 @@ class TestHdf5TreeModel(TestCaseQt): model = hdf5.Hdf5TreeModel() model.insertH5pyObject(h5) displayed = self.getRowDataAsDict(model, row=0) - self.assertEqual(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock") - self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon) - self.assertEqual(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], None) - self.assertEqual(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "File") + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock" + ) + self.assertIsInstance( + displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], None + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "File" + ) def testGroupData(self): h5 = commonh5.File("/foo/bar/1.mock", "w") @@ -302,13 +325,27 @@ class TestHdf5TreeModel(TestCaseQt): model = hdf5.Hdf5TreeModel() model.insertH5pyObject(d) displayed = self.getRowDataAsDict(model, row=0) - self.assertEqual(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo") - self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon) - self.assertEqual(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "fooo") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Group") + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo" + ) + self.assertIsInstance( + displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "fooo" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Group" + ) def testDatasetData(self): h5 = commonh5.File("/foo/bar/1.mock", "w") @@ -318,13 +355,29 @@ class TestHdf5TreeModel(TestCaseQt): model = hdf5.Hdf5TreeModel() model.insertH5pyObject(d) displayed = self.getRowDataAsDict(model, row=0) - self.assertEqual(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo") - self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon) - self.assertEqual(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], value.dtype.name) - self.assertEqual(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "3") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "[1 2 3]") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "[1 2 3]") - self.assertEqual(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Dataset") + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo" + ) + self.assertIsInstance( + displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], + value.dtype.name, + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "3" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "[1 2 3]" + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], + "[1 2 3]", + ) + self.assertEqual( + displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Dataset" + ) def testDropLastAsFirst(self): model = hdf5.Hdf5TreeModel() @@ -365,17 +418,18 @@ class TestHdf5TreeModel(TestCaseQt): @pytest.mark.usefixtures("useH5File") class TestHdf5TreeModelSignals(TestCaseQt): - def setUp(self): TestCaseQt.setUp(self) self.model = hdf5.Hdf5TreeModel() - self.h5 = h5py.File(self.filename, mode='r') + self.h5 = h5py_utils.File(self.filename, mode="r") self.model.insertH5pyObject(self.h5) self.listener = SignalListener() self.model.sigH5pyObjectLoaded.connect(self.listener.partial(signal="loaded")) self.model.sigH5pyObjectRemoved.connect(self.listener.partial(signal="removed")) - self.model.sigH5pyObjectSynchronized.connect(self.listener.partial(signal="synchronized")) + self.model.sigH5pyObjectSynchronized.connect( + self.listener.partial(signal="synchronized") + ) def tearDown(self): self.signals = None @@ -395,16 +449,28 @@ class TestHdf5TreeModelSignals(TestCaseQt): raise RuntimeError("Still waiting for a pending operation") def testInsert(self): - h5 = h5py.File(self.filename, mode='r') + h5 = h5py_utils.File(self.filename, mode="r") self.model.insertH5pyObject(h5) self.assertEqual(self.listener.callCount(), 0) def testLoaded(self): - self.model.insertFile(self.filename) - self.assertEqual(self.listener.callCount(), 1) - self.assertEqual(self.listener.karguments(argumentName="signal")[0], "loaded") - self.assertIsNot(self.listener.arguments(callIndex=0)[0], self.h5) - self.assertEqual(self.listener.arguments(callIndex=0)[0].filename, self.filename) + for data_path in [None, "/arrays/scalar"]: + with self.subTest(data_path=data_path): + url = DataUrl(file_path=self.filename, data_path=data_path) + insertedFilename = url.path() + self.model.insertFile(insertedFilename) + self.assertEqual(self.listener.callCount(), 1) + self.assertEqual( + self.listener.karguments(argumentName="signal")[0], "loaded" + ) + self.assertIsNot(self.listener.arguments(callIndex=0)[0], self.h5) + self.assertEqual( + self.listener.arguments(callIndex=0)[0].file.filename, self.filename + ) + self.assertEqual( + self.listener.arguments(callIndex=0)[1], insertedFilename + ) + self.listener.clear() def testRemoved(self): self.model.removeH5pyObject(self.h5) @@ -416,13 +482,14 @@ class TestHdf5TreeModelSignals(TestCaseQt): self.model.synchronizeH5pyObject(self.h5) self.waitForPendingOperations(self.model) self.assertEqual(self.listener.callCount(), 1) - self.assertEqual(self.listener.karguments(argumentName="signal")[0], "synchronized") + self.assertEqual( + self.listener.karguments(argumentName="signal")[0], "synchronized" + ) self.assertIs(self.listener.arguments(callIndex=0)[0], self.h5) self.assertIsNot(self.listener.arguments(callIndex=0)[1], self.h5) class TestNexusSortFilterProxyModel(TestCaseQt): - def getChildNames(self, model, index): count = model.rowCount(index) result = [] @@ -451,9 +518,15 @@ class TestNexusSortFilterProxyModel(TestCaseQt): """Test NXentry with start_time""" model = hdf5.Hdf5TreeModel() 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")])) + 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() @@ -466,9 +539,15 @@ class TestNexusSortFilterProxyModel(TestCaseQt): """Test NXentry with end_time""" model = hdf5.Hdf5TreeModel() 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")])) + 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() @@ -565,7 +644,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt): self.assertListEqual(names, ["100aaa", "aaa100"]) -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def useH5Model(request, tmpdir_factory): # Create HDF5 files tmp = tmpdir_factory.mktemp("test_hdf5") @@ -573,39 +652,53 @@ def useH5Model(request, tmpdir_factory): extH5FileName = os.path.join(tmp, "base__external.h5") extDatFileName = os.path.join(tmp, "base__external.dat") - externalh5 = h5py.File(extH5FileName, mode="w") + externalh5 = h5py_utils.File(extH5FileName, mode="w") externalh5["target/dataset"] = 50 externalh5["target/link"] = h5py.SoftLink("/target/dataset") externalh5["/ext/vds0"] = [0, 1] externalh5["/ext/vds1"] = [2, 3] externalh5.close() - numpy.array([0,1,10,10,2,3]).tofile(extDatFileName) + numpy.array([0, 1, 10, 10, 2, 3]).tofile(extDatFileName) - h5 = h5py.File(filename, mode="w") + h5 = h5py_utils.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_soft_link"] = h5py.SoftLink("/link/soft_link") + h5["link/soft_link_to_external_link"] = h5py.SoftLink("/link/external_link") h5["link/soft_link_to_file"] = h5py.SoftLink("/") h5["group/soft_link_relative"] = h5py.SoftLink("dataset") h5["link/external_link"] = h5py.ExternalLink(extH5FileName, "/target/dataset") - h5["link/external_link_to_link"] = h5py.ExternalLink(extH5FileName, "/target/link") - h5["broken_link/external_broken_file"] = h5py.ExternalLink(extH5FileName + "_not_exists", "/target/link") - h5["broken_link/external_broken_link"] = h5py.ExternalLink(extH5FileName, "/target/not_exists") + h5["link/external_link_to_soft_link"] = h5py.ExternalLink( + extH5FileName, "/target/link" + ) + h5["broken_link/external_broken_file"] = h5py.ExternalLink( + extH5FileName + "_not_exists", "/target/link" + ) + h5["broken_link/external_broken_link"] = h5py.ExternalLink( + extH5FileName, "/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") if h5py2_9: - layout = h5py.VirtualLayout((2,2), dtype=int) - layout[0] = h5py.VirtualSource("base__external.h5", name="/ext/vds0", shape=(2,), dtype=int) - layout[1] = h5py.VirtualSource("base__external.h5", name="/ext/vds1", shape=(2,), dtype=int) + layout = h5py.VirtualLayout((2, 2), dtype=int) + layout[0] = h5py.VirtualSource( + "base__external.h5", name="/ext/vds0", shape=(2,), dtype=int + ) + layout[1] = h5py.VirtualSource( + "base__external.h5", name="/ext/vds1", shape=(2,), dtype=int + ) h5.create_group("/ext") h5["/ext"].create_virtual_dataset("virtual", layout) - external = [("base__external.dat", 0, 2*8), ("base__external.dat", 4*8, 2*8)] - h5["/ext"].create_dataset("raw", shape=(2,2), dtype=int, external=external) + external = [ + ("base__external.dat", 0, 2 * 8), + ("base__external.dat", 4 * 8, 2 * 8), + ] + h5["/ext"].create_dataset("raw", shape=(2, 2), dtype=int, external=external) h5.close() - with h5py.File(filename, mode="r") as h5File: + with h5py_utils.File(filename, mode="r") as h5File: # Create model request.cls.model = hdf5.Hdf5TreeModel() request.cls.model.insertH5pyObject(h5File) @@ -615,7 +708,7 @@ def useH5Model(request, tmpdir_factory): TestCaseQt.qWaitForDestroy(ref) -@pytest.mark.usefixtures('useH5Model') +@pytest.mark.usefixtures("useH5Model") class _TestModelBase(TestCaseQt): def getIndexFromPath(self, model, path): """ @@ -639,7 +732,6 @@ class _TestModelBase(TestCaseQt): class TestH5Item(_TestModelBase): - def testFile(self): path = ["base.h5"] h5item = self.getH5ItemFromPath(self.model, path) @@ -665,7 +757,7 @@ class TestH5Item(_TestModelBase): self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "Soft") def testSoftLinkToLink(self): - path = ["base.h5", "link", "soft_link_to_link"] + path = ["base.h5", "link", "soft_link_to_soft_link"] h5item = self.getH5ItemFromPath(self.model, path) self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "Soft") @@ -683,7 +775,7 @@ class TestH5Item(_TestModelBase): self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "External") def testExternalLinkToLink(self): - path = ["base.h5", "link", "external_link_to_link"] + path = ["base.h5", "link", "external_link_to_soft_link"] h5item = self.getH5ItemFromPath(self.model, path) self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "External") @@ -719,7 +811,14 @@ class TestH5Item(_TestModelBase): self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "") def testDatasetFromSoftLinkToFile(self): - path = ["base.h5", "link", "soft_link_to_file", "link", "soft_link_to_group", "dataset"] + path = [ + "base.h5", + "link", + "soft_link_to_file", + "link", + "soft_link_to_group", + "dataset", + ] h5item = self.getH5ItemFromPath(self.model, path) self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "") @@ -740,7 +839,6 @@ class TestH5Item(_TestModelBase): class TestH5Node(_TestModelBase): - def getH5NodeFromPath(self, model, path): item = self.getH5ItemFromPath(model, path) h5node = hdf5.H5Node(item) @@ -790,16 +888,30 @@ class TestH5Node(_TestModelBase): 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"] + def testSoftLinkToSoftLink(self): + path = ["base.h5", "link", "soft_link_to_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_to_link") - self.assertEqual(h5node.local_name, "/link/soft_link_to_link") + self.assertEqual(h5node.local_basename, "soft_link_to_soft_link") + self.assertEqual(h5node.local_name, "/link/soft_link_to_soft_link") + + def testSoftLinkToExternalLink(self): + path = ["base.h5", "link", "soft_link_to_external_link"] + h5node = self.getH5NodeFromPath(self.model, path) + + with self.assertRaises(KeyError): + # h5py bug: #1706 + 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, "soft_link_to_external_link") + self.assertEqual(h5node.local_name, "/link/soft_link_to_external_link") def testSoftLinkRelative(self): path = ["base.h5", "group", "soft_link_relative"] @@ -824,19 +936,18 @@ class TestH5Node(_TestModelBase): 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"] + def testExternalLinkToSoftLink(self): + path = ["base.h5", "link", "external_link_to_soft_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") + self.assertEqual(h5node.local_basename, "external_link_to_soft_link") + self.assertEqual(h5node.local_name, "/link/external_link_to_soft_link") def testExternalBrokenFile(self): path = ["base.h5", "broken_link", "external_broken_file"] @@ -896,7 +1007,14 @@ class TestH5Node(_TestModelBase): 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"] + 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) @@ -904,7 +1022,9 @@ class TestH5Node(_TestModelBase): 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") + self.assertEqual( + h5node.local_name, "/link/soft_link_to_file/link/soft_link_to_group/dataset" + ) @pytest.mark.skipif(not h5py2_9, reason="requires h5py>=2.9") def testExternalVirtual(self): |