diff options
Diffstat (limited to 'silx/gui/data')
-rw-r--r-- | silx/gui/data/ArrayTableModel.py | 7 | ||||
-rw-r--r-- | silx/gui/data/ArrayTableWidget.py | 2 | ||||
-rw-r--r-- | silx/gui/data/DataViewer.py | 10 | ||||
-rw-r--r-- | silx/gui/data/DataViewerFrame.py | 12 | ||||
-rw-r--r-- | silx/gui/data/DataViews.py | 209 | ||||
-rw-r--r-- | silx/gui/data/Hdf5TableView.py | 76 | ||||
-rw-r--r-- | silx/gui/data/HexaTableView.py | 278 | ||||
-rw-r--r-- | silx/gui/data/NXdataWidgets.py | 22 | ||||
-rw-r--r-- | silx/gui/data/RecordTableView.py | 10 | ||||
-rw-r--r-- | silx/gui/data/TextFormatter.py | 168 | ||||
-rw-r--r-- | silx/gui/data/test/test_dataviewer.py | 28 | ||||
-rw-r--r-- | silx/gui/data/test/test_textformatter.py | 113 |
12 files changed, 838 insertions, 97 deletions
diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py index 87a2fc1..ad4d33a 100644 --- a/silx/gui/data/ArrayTableModel.py +++ b/silx/gui/data/ArrayTableModel.py @@ -34,7 +34,7 @@ from silx.gui.data.TextFormatter import TextFormatter __authors__ = ["V.A. Sole"] __license__ = "MIT" -__date__ = "24/01/2017" +__date__ = "27/09/2017" _logger = logging.getLogger(__name__) @@ -191,7 +191,7 @@ class ArrayTableModel(qt.QAbstractTableModel): selection = self._getIndexTuple(index.row(), index.column()) if role == qt.Qt.DisplayRole: - return self._formatter.toString(self._array[selection]) + return self._formatter.toString(self._array[selection], self._array.dtype) if role == qt.Qt.BackgroundRole and self._bgcolors is not None: r, g, b = self._bgcolors[selection][0:3] @@ -296,6 +296,9 @@ class ArrayTableModel(qt.QAbstractTableModel): elif copy: # copy requested (default) self._array = numpy.array(data, copy=True) + if hasattr(data, "dtype"): + # Avoid to lose the monkey-patched h5py dtype + self._array.dtype = data.dtype elif not _is_array(data): raise TypeError("data is not a proper array. Try setting" + " copy=True to convert it into a numpy array" + diff --git a/silx/gui/data/ArrayTableWidget.py b/silx/gui/data/ArrayTableWidget.py index ba3fa11..cb8e915 100644 --- a/silx/gui/data/ArrayTableWidget.py +++ b/silx/gui/data/ArrayTableWidget.py @@ -230,6 +230,8 @@ class ArrayTableWidget(qt.QWidget): To select the perspective, use :meth:`setPerspective` or use :meth:`setFrameAxes`. To select the frame, use :meth:`setFrameIndex`. + + .. image:: img/ArrayTableWidget.png """ def __init__(self, parent=None): """ diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py index 3a3ac64..750c654 100644 --- a/silx/gui/data/DataViewer.py +++ b/silx/gui/data/DataViewer.py @@ -22,8 +22,8 @@ # THE SOFTWARE. # # ###########################################################################*/ -"""This module defines a widget designed to display data using to most adapted -view from available ones from silx. +"""This module defines a widget designed to display data using the most adapted +view from the ones provided by silx. """ from __future__ import division @@ -35,7 +35,7 @@ from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "26/04/2017" +__date__ = "03/10/2017" _logger = logging.getLogger(__name__) @@ -144,7 +144,7 @@ class DataViewer(qt.QFrame): DataViews._Hdf5View, DataViews._NXdataView, DataViews._Plot1dView, - DataViews._Plot2dView, + DataViews._ImageView, DataViews._Plot3dView, DataViews._RawView, DataViews._StackView, @@ -201,7 +201,7 @@ class DataViewer(qt.QFrame): self.__numpySelection.clear() info = DataViews.DataInfo(self.__data) axisNames = self.__currentView.axesNames(self.__data, info) - if info.isArray and self.__data is not None and len(axisNames) > 0: + if info.isArray and info.size != 0 and self.__data is not None and axisNames is not None: self.__useAxisSelection = True self.__numpySelection.setAxisNames(axisNames) self.__numpySelection.setCustomAxis(self.__currentView.customAxisNames()) diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py index b48fa7b..e050d4a 100644 --- a/silx/gui/data/DataViewerFrame.py +++ b/silx/gui/data/DataViewerFrame.py @@ -27,7 +27,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "10/04/2017" +__date__ = "21/09/2017" from silx.gui import qt from .DataViewer import DataViewer @@ -79,6 +79,14 @@ class DataViewerFrame(qt.QWidget): """Avoid to create views while the instance is not created.""" super(_DataViewer, self)._initializeViews() + def _createDefaultViews(self, parent): + """Expose the original `createDefaultViews` function""" + return super(_DataViewer, self).createDefaultViews() + + def createDefaultViews(self, parent=None): + """Allow the DataViewerFrame to override this function""" + return self.parent().createDefaultViews(parent) + self.__dataViewer = _DataViewer(self) # initialize views when `self.__dataViewer` is set self.__dataViewer.initializeViews() @@ -127,7 +135,7 @@ class DataViewerFrame(qt.QWidget): :param QWidget parent: QWidget parent of the views :rtype: list[silx.gui.data.DataViews.DataView] """ - return self.__dataViewer.createDefaultViews(parent) + return self.__dataViewer._createDefaultViews(parent) def addView(self, view): """Allow to add a view to the dataview. diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py index d8d605a..1ad997b 100644 --- a/silx/gui/data/DataViews.py +++ b/silx/gui/data/DataViews.py @@ -25,6 +25,7 @@ """This module defines a views used by :class:`silx.gui.data.DataViewer`. """ +from collections import OrderedDict import logging import numbers import numpy @@ -34,11 +35,11 @@ from silx.gui import qt, icons from silx.gui.data.TextFormatter import TextFormatter from silx.io import nxdata from silx.gui.hdf5 import H5Node -from silx.io.nxdata import NXdata +from silx.io.nxdata import NXdata, get_attr_as_string __authors__ = ["V. Valls", "P. Knobel"] __license__ = "MIT" -__date__ = "07/04/2017" +__date__ = "03/10/2017" _logger = logging.getLogger(__name__) @@ -52,6 +53,7 @@ RAW_MODE = 40 RAW_ARRAY_MODE = 41 RAW_RECORD_MODE = 42 RAW_SCALAR_MODE = 43 +RAW_HEXA_MODE = 44 STACK_MODE = 50 HDF5_MODE = 60 @@ -62,6 +64,8 @@ def _normalizeData(data): If the data embed a numpy data or a dataset it is returned. Else returns the input data.""" if isinstance(data, H5Node): + if data.is_broken: + return None return data.h5py_object return data @@ -89,11 +93,14 @@ class DataInfo(object): self.isArray = False self.interpretation = None self.isNumeric = False + self.isVoid = False self.isComplex = False + self.isBoolean = False self.isRecord = False self.isNXdata = False self.shape = tuple() self.dim = 0 + self.size = 0 if data is None: return @@ -110,23 +117,32 @@ class DataInfo(object): self.isArray = False if silx.io.is_dataset(data): - self.interpretation = data.attrs.get("interpretation", None) + if "interpretation" in data.attrs: + self.interpretation = get_attr_as_string(data, "interpretation") + else: + self.interpretation = None elif self.isNXdata: self.interpretation = nxd.interpretation else: self.interpretation = None if hasattr(data, "dtype"): + if numpy.issubdtype(data.dtype, numpy.void): + # That's a real opaque type, else it is a structured type + self.isVoid = data.dtype.fields is None self.isNumeric = numpy.issubdtype(data.dtype, numpy.number) self.isRecord = data.dtype.fields is not None self.isComplex = numpy.issubdtype(data.dtype, numpy.complex) + self.isBoolean = numpy.issubdtype(data.dtype, numpy.bool_) elif self.isNXdata: self.isNumeric = numpy.issubdtype(nxd.signal.dtype, numpy.number) self.isComplex = numpy.issubdtype(nxd.signal.dtype, numpy.complex) + self.isBoolean = numpy.issubdtype(nxd.signal.dtype, numpy.bool_) else: self.isNumeric = isinstance(data, numbers.Number) self.isComplex = isinstance(data, numbers.Complex) + self.isBoolean = isinstance(data, bool) self.isRecord = False if hasattr(data, "shape"): @@ -135,7 +151,13 @@ class DataInfo(object): self.shape = nxd.signal.shape else: self.shape = tuple() - self.dim = len(self.shape) + if self.shape is not None: + self.dim = len(self.shape) + + if hasattr(data, "size"): + self.size = int(data.size) + else: + self.size = 1 def normalizeData(self, data): """Returns a normalized data if the embed a numpy or a dataset. @@ -237,12 +259,12 @@ class DataView(object): def axesNames(self, data, info): """Returns names of the expected axes of the view, according to the - input data. + input data. A none value will disable the default axes selectior. :param data: Data to display :type data: numpy.ndarray or h5py.Dataset :param DataInfo info: Pre-computed information on the data - :rtype: list[str] + :rtype: list[str] or None """ return [] @@ -276,7 +298,7 @@ class CompositeDataView(DataView): :param qt.QWidget parent: Parent of the hold widget """ super(CompositeDataView, self).__init__(parent, modeId, icon, label) - self.__views = {} + self.__views = OrderedDict() self.__currentView = None def addView(self, dataView): @@ -285,10 +307,9 @@ class CompositeDataView(DataView): def getBestView(self, data, info): """Returns the best view according to priorities.""" - info = DataInfo(data) views = [(v.getDataPriority(data, info), v) for v in self.__views.keys()] views = filter(lambda t: t[0] > DataView.UNSUPPORTED, views) - views = sorted(views, reverse=True) + views = sorted(views, key=lambda t: t[0], reverse=True) if len(views) == 0: return None @@ -361,7 +382,7 @@ class _EmptyView(DataView): DataView.__init__(self, parent, modeId=EMPTY_MODE) def axesNames(self, data, info): - return [] + return None def createWidget(self, parent): return qt.QLabel(parent) @@ -406,6 +427,8 @@ class _Plot1dView(DataView): return ["y"] def getDataPriority(self, data, info): + if info.size <= 0: + return DataView.UNSUPPORTED if data is None or not info.isArray or not info.isNumeric: return DataView.UNSUPPORTED if info.dim < 1: @@ -434,9 +457,10 @@ class _Plot2dView(DataView): def createWidget(self, parent): from silx.gui import plot widget = plot.Plot2D(parent=parent) + widget.getIntensityHistogramAction().setVisible(True) widget.setKeepDataAspectRatio(True) - widget.setGraphXLabel('X') - widget.setGraphYLabel('Y') + widget.getXAxis().setLabel('X') + widget.getYAxis().setLabel('Y') return widget def clear(self): @@ -459,7 +483,11 @@ class _Plot2dView(DataView): return ["y", "x"] def getDataPriority(self, data, info): - if data is None or not info.isArray or not info.isNumeric: + if info.size <= 0: + return DataView.UNSUPPORTED + if (data is None or + not info.isArray or + not (info.isNumeric or info.isBoolean)): return DataView.UNSUPPORTED if info.dim < 2: return DataView.UNSUPPORTED @@ -494,8 +522,15 @@ class _Plot3dView(DataView): plot = ScalarFieldView.ScalarFieldView(parent) plot.setAxesLabels(*reversed(self.axesNames(None, None))) - plot.addIsosurface( - lambda data: numpy.mean(data) + numpy.std(data), '#FF0000FF') + + def computeIsolevel(data): + data = data[numpy.isfinite(data)] + if len(data) == 0: + return 0 + else: + return numpy.mean(data) + numpy.std(data) + + plot.addIsosurface(computeIsolevel, '#FF0000FF') # Create a parameter tree for the scalar field view options = SFViewParamTree.TreeView(plot) @@ -527,6 +562,8 @@ class _Plot3dView(DataView): return ["z", "y", "x"] def getDataPriority(self, data, info): + if info.size <= 0: + return DataView.UNSUPPORTED if data is None or not info.isArray or not info.isNumeric: return DataView.UNSUPPORTED if info.dim < 3: @@ -539,6 +576,54 @@ class _Plot3dView(DataView): return 10 +class _ComplexImageView(DataView): + """View displaying data using a ComplexImageView""" + + def __init__(self, parent): + super(_ComplexImageView, self).__init__( + parent=parent, + modeId=PLOT2D_MODE, + label="Complex Image", + icon=icons.getQIcon("view-2d")) + + def createWidget(self, parent): + from silx.gui.plot.ComplexImageView import ComplexImageView + widget = ComplexImageView(parent=parent) + widget.getPlot().getIntensityHistogramAction().setVisible(True) + widget.getPlot().setKeepDataAspectRatio(True) + widget.getXAxis().setLabel('X') + widget.getYAxis().setLabel('Y') + return widget + + def clear(self): + self.getWidget().setData(None) + + def normalizeData(self, data): + data = DataView.normalizeData(self, data) + return data + + def setData(self, data): + data = self.normalizeData(data) + self.getWidget().setData(data) + + def axesNames(self, data, info): + return ["y", "x"] + + def getDataPriority(self, data, info): + if info.size <= 0: + return DataView.UNSUPPORTED + if data is None or not info.isArray or not info.isComplex: + return DataView.UNSUPPORTED + if info.dim < 2: + return DataView.UNSUPPORTED + if info.interpretation == "image": + return 1000 + if info.dim == 2: + return 200 + else: + return 190 + + class _ArrayView(DataView): """View displaying data using a 2d table""" @@ -562,6 +647,8 @@ class _ArrayView(DataView): return ["col", "row"] def getDataPriority(self, data, info): + if info.size <= 0: + return DataView.UNSUPPORTED if data is None or not info.isArray or info.isRecord: return DataView.UNSUPPORTED if info.dim < 2: @@ -618,6 +705,8 @@ class _StackView(DataView): return ["depth", "y", "x"] def getDataPriority(self, data, info): + if info.size <= 0: + return DataView.UNSUPPORTED if data is None or not info.isArray or not info.isNumeric: return DataView.UNSUPPORTED if info.dim < 3: @@ -644,17 +733,21 @@ class _ScalarView(DataView): self.getWidget().setText("") def setData(self, data): - data = self.normalizeData(data) - if silx.io.is_dataset(data): - data = data[()] - text = self.__formatter.toString(data) + d = self.normalizeData(data) + if silx.io.is_dataset(d): + d = d[()] + text = self.__formatter.toString(d, data.dtype) self.getWidget().setText(text) def axesNames(self, data, info): return [] def getDataPriority(self, data, info): + if info.size <= 0: + return DataView.UNSUPPORTED data = self.normalizeData(data) + if info.shape is None: + return DataView.UNSUPPORTED if data is None: return DataView.UNSUPPORTED if silx.io.is_group(data): @@ -681,13 +774,16 @@ class _RecordView(DataView): data = self.normalizeData(data) widget = self.getWidget() widget.setArrayData(data) - widget.resizeRowsToContents() - widget.resizeColumnsToContents() + if len(data) < 100: + widget.resizeRowsToContents() + widget.resizeColumnsToContents() def axesNames(self, data, info): return ["data"] def getDataPriority(self, data, info): + if info.size <= 0: + return DataView.UNSUPPORTED if info.isRecord: return 40 if data is None or not info.isArray: @@ -703,6 +799,36 @@ class _RecordView(DataView): return DataView.UNSUPPORTED +class _HexaView(DataView): + """View displaying data using text""" + + def __init__(self, parent): + DataView.__init__(self, parent, modeId=RAW_HEXA_MODE) + + def createWidget(self, parent): + from .HexaTableView import HexaTableView + widget = HexaTableView(parent) + return widget + + def clear(self): + self.getWidget().setArrayData(None) + + def setData(self, data): + data = self.normalizeData(data) + widget = self.getWidget() + widget.setArrayData(data) + + def axesNames(self, data, info): + return [] + + def getDataPriority(self, data, info): + if info.size <= 0: + return DataView.UNSUPPORTED + if info.isVoid: + return 2000 + return DataView.UNSUPPORTED + + class _Hdf5View(DataView): """View displaying data using text""" @@ -727,7 +853,7 @@ class _Hdf5View(DataView): widget.setData(data) def axesNames(self, data, info): - return [] + return None def getDataPriority(self, data, info): widget = self.getWidget() @@ -750,11 +876,28 @@ class _RawView(CompositeDataView): modeId=RAW_MODE, label="Raw", icon=icons.getQIcon("view-raw")) + self.addView(_HexaView(parent)) self.addView(_ScalarView(parent)) self.addView(_ArrayView(parent)) self.addView(_RecordView(parent)) +class _ImageView(CompositeDataView): + """View displaying data as 2D image + + It choose between Plot2D and ComplexImageView widgets + """ + + def __init__(self, parent): + super(_ImageView, self).__init__( + parent=parent, + modeId=PLOT2D_MODE, + label="Image", + icon=icons.getQIcon("view-2d")) + self.addView(_ComplexImageView(parent)) + self.addView(_Plot2dView(parent)) + + class _NXdataScalarView(DataView): """DataView using a table view for displaying NXdata scalars: 0-D signal or n-D signal with *@interpretation=scalar*""" @@ -806,7 +949,7 @@ class _NXdataCurveView(DataView): def axesNames(self, data, info): # disabled (used by default axis selector widget in Hdf5Viewer) - return [] + return None def clear(self): self.getWidget().clear() @@ -814,10 +957,10 @@ class _NXdataCurveView(DataView): def setData(self, data): data = self.normalizeData(data) nxd = NXdata(data) - signal_name = data.attrs["signal"] + signal_name = get_attr_as_string(data, "signal") group_name = data.name - if nxd.axes_names[-1] is not None: - x_errors = nxd.get_axis_errors(nxd.axes_names[-1]) + if nxd.axes_dataset_names[-1] is not None: + x_errors = nxd.get_axis_errors(nxd.axes_dataset_names[-1]) else: x_errors = None @@ -853,7 +996,7 @@ class _NXdataXYVScatterView(DataView): def axesNames(self, data, info): # disabled (used by default axis selector widget in Hdf5Viewer) - return [] + return None def clear(self): self.getWidget().clear() @@ -861,7 +1004,7 @@ class _NXdataXYVScatterView(DataView): def setData(self, data): data = self.normalizeData(data) nxd = NXdata(data) - signal_name = data.attrs["signal"] + signal_name = get_attr_as_string(data, "signal") # signal_errors = nx.errors # not supported group_name = data.name x_axis, y_axis = nxd.axes[-2:] @@ -902,7 +1045,8 @@ class _NXdataImageView(DataView): return widget def axesNames(self, data, info): - return [] + # disabled (used by default axis selector widget in Hdf5Viewer) + return None def clear(self): self.getWidget().clear() @@ -910,7 +1054,7 @@ class _NXdataImageView(DataView): def setData(self, data): data = self.normalizeData(data) nxd = NXdata(data) - signal_name = data.attrs["signal"] + signal_name = get_attr_as_string(data, "signal") group_name = data.name y_axis, x_axis = nxd.axes[-2:] y_label, x_label = nxd.axes_names[-2:] @@ -942,7 +1086,8 @@ class _NXdataStackView(DataView): return widget def axesNames(self, data, info): - return [] + # disabled (used by default axis selector widget in Hdf5Viewer) + return None def clear(self): self.getWidget().clear() @@ -950,7 +1095,7 @@ class _NXdataStackView(DataView): def setData(self, data): data = self.normalizeData(data) nxd = NXdata(data) - signal_name = data.attrs["signal"] + signal_name = get_attr_as_string(data, "signal") group_name = data.name z_axis, y_axis, x_axis = nxd.axes[-3:] z_label, y_label, x_label = nxd.axes_names[-3:] diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py index 5d79907..ba737e3 100644 --- a/silx/gui/data/Hdf5TableView.py +++ b/silx/gui/data/Hdf5TableView.py @@ -30,7 +30,7 @@ from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "07/04/2017" +__date__ = "29/09/2017" import functools import os.path @@ -40,6 +40,13 @@ import silx.io from .TextFormatter import TextFormatter import silx.gui.hdf5 from silx.gui.widgets import HierarchicalTableView +from ..hdf5.Hdf5Formatter import Hdf5Formatter + +try: + import h5py +except ImportError: + h5py = None + _logger = logging.getLogger(__name__) @@ -177,6 +184,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): self.__obj = None self.__data = _TableData(columnCount=4) self.__formatter = None + self.__hdf5Formatter = Hdf5Formatter(self) formatter = TextFormatter(self) self.setFormatter(formatter) self.setObject(data) @@ -207,7 +215,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): value = cell.value() if callable(value): value = value(self.__obj) - return str(value) + return value return None def flags(self, index): @@ -248,6 +256,22 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): else: self.reset() + def __formatHdf5Type(self, dataset): + """Format the HDF5 type""" + return self.__hdf5Formatter.humanReadableHdf5Type(dataset) + + def __formatDType(self, dataset): + """Format the numpy dtype""" + return self.__hdf5Formatter.humanReadableType(dataset, full=True) + + def __formatShape(self, dataset): + """Format the shape""" + if dataset.shape is None or len(dataset.shape) <= 1: + return self.__hdf5Formatter.humanReadableShape(dataset) + size = dataset.size + shape = self.__hdf5Formatter.humanReadableShape(dataset) + return u"%s = %s" % (shape, size) + def __initProperties(self): """Initialize the list of available properties according to the defined h5py-like object.""" @@ -270,26 +294,48 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): else: objectType = obj.__class__.__name__ self.__data.addHeaderRow(headerLabel="HDF5 %s" % objectType) - self.__data.addHeaderRow(headerLabel="Path info") - self.__data.addHeaderValueRow("basename", lambda x: os.path.basename(x.name)) - self.__data.addHeaderValueRow("name", lambda x: x.name) - if silx.io.is_file(obj): - self.__data.addHeaderValueRow("filename", lambda x: x.filename) + SEPARATOR = "::" + self.__data.addHeaderRow(headerLabel="Path info") if isinstance(obj, silx.gui.hdf5.H5Node): # helpful informations if the object come from an HDF5 tree - self.__data.addHeaderValueRow("local_basename", lambda x: x.local_basename) - self.__data.addHeaderValueRow("local_name", lambda x: x.local_name) - self.__data.addHeaderValueRow("local_filename", lambda x: x.local_file.filename) + self.__data.addHeaderValueRow("Basename", lambda x: x.local_basename) + self.__data.addHeaderValueRow("Name", lambda x: x.local_name) + local = lambda x: x.local_filename + SEPARATOR + x.local_name + self.__data.addHeaderValueRow("Local", local) + physical = lambda x: x.physical_filename + SEPARATOR + x.physical_name + self.__data.addHeaderValueRow("Physical", physical) + else: + # it's a real H5py object + self.__data.addHeaderValueRow("Basename", lambda x: os.path.basename(x.name)) + self.__data.addHeaderValueRow("Name", lambda x: x.name) + self.__data.addHeaderValueRow("File", lambda x: x.file.filename) + + if hasattr(obj, "path"): + # That's a link + if hasattr(obj, "filename"): + link = lambda x: x.filename + SEPARATOR + x.path + else: + link = lambda x: x.path + self.__data.addHeaderValueRow("Link", link) + else: + if silx.io.is_file(obj): + physical = lambda x: x.filename + SEPARATOR + x.name + else: + physical = lambda x: x.file.filename + SEPARATOR + x.name + self.__data.addHeaderValueRow("Physical", physical) if hasattr(obj, "dtype"): + self.__data.addHeaderRow(headerLabel="Data info") - self.__data.addHeaderValueRow("dtype", lambda x: x.dtype) + + if h5py is not None and hasattr(obj, "id"): + # display the HDF5 type + self.__data.addHeaderValueRow("HDF5 type", self.__formatHdf5Type) + self.__data.addHeaderValueRow("dtype", self.__formatDType) if hasattr(obj, "shape"): - self.__data.addHeaderValueRow("shape", lambda x: x.shape) - if hasattr(obj, "size"): - self.__data.addHeaderValueRow("size", lambda x: x.size) + self.__data.addHeaderValueRow("shape", self.__formatShape) if hasattr(obj, "chunks") and obj.chunks is not None: self.__data.addHeaderValueRow("chunks", lambda x: x.chunks) @@ -354,6 +400,8 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): if formatter is self.__formatter: return + self.__hdf5Formatter.setTextFormatter(formatter) + if qt.qVersion() > "4.6": self.beginResetModel() diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py new file mode 100644 index 0000000..1b2a7e9 --- /dev/null +++ b/silx/gui/data/HexaTableView.py @@ -0,0 +1,278 @@ +# 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 module defines model and widget to display raw data using an +hexadecimal viewer. +""" +from __future__ import division + +import numpy +import collections +from silx.gui import qt +import silx.io.utils +from silx.third_party import six +from silx.gui.widgets.TableWidget import CopySelectedCellsAction + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "27/09/2017" + + +class _VoidConnector(object): + """Byte connector to a numpy.void data. + + It uses a cache of 32 x 1KB and a direct read access API from HDF5. + """ + + def __init__(self, data): + self.__cache = collections.OrderedDict() + self.__len = data.itemsize + self.__data = data + + def __getBuffer(self, bufferId): + if bufferId not in self.__cache: + pos = bufferId << 10 + data = self.__data.tobytes()[pos:pos + 1024] + self.__cache[bufferId] = data + if len(self.__cache) > 32: + self.__cache.popitem() + else: + data = self.__cache[bufferId] + return data + + def __getitem__(self, pos): + """Returns the value of the byte at the given position. + + :param uint pos: Position of the byte + :rtype: int + """ + bufferId = pos >> 10 + bufferPos = pos & 0b1111111111 + data = self.__getBuffer(bufferId) + value = data[bufferPos] + if six.PY2: + return ord(value) + else: + return value + + def __len__(self): + """ + Returns the number of available bytes. + + :rtype: uint + """ + return self.__len + + +class HexaTableModel(qt.QAbstractTableModel): + """This data model provides access to a numpy void data. + + Bytes are displayed one by one as a hexadecimal viewer. + + The 16th first columns display bytes as hexadecimal, the last column + displays the same data as ASCII. + + :param qt.QObject parent: Parent object + :param data: A numpy array or a h5py dataset + """ + def __init__(self, parent=None, data=None): + qt.QAbstractTableModel.__init__(self, parent) + + self.__data = None + self.__connector = None + self.setArrayData(data) + + if hasattr(qt.QFontDatabase, "systemFont"): + self.__font = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont) + else: + self.__font = qt.QFont("Monospace") + self.__font.setStyleHint(qt.QFont.TypeWriter) + self.__palette = qt.QPalette() + + def rowCount(self, parent_idx=None): + """Returns number of rows to be displayed in table""" + if self.__connector is None: + return 0 + return ((len(self.__connector) - 1) >> 4) + 1 + + def columnCount(self, parent_idx=None): + """Returns number of columns to be displayed in table""" + return 0x10 + 1 + + def data(self, index, role=qt.Qt.DisplayRole): + """QAbstractTableModel method to access data values + in the format ready to be displayed""" + if not index.isValid(): + return None + + if self.__connector is None: + return None + + row = index.row() + column = index.column() + + if role == qt.Qt.DisplayRole: + if column == 0x10: + start = (row << 4) + text = "" + for i in range(0x10): + pos = start + i + if pos >= len(self.__connector): + break + value = self.__connector[pos] + if value > 0x20 and value < 0x7F: + text += chr(value) + else: + text += "." + return text + else: + pos = (row << 4) + column + if pos < len(self.__connector): + value = self.__connector[pos] + return "%02X" % value + else: + return "" + elif role == qt.Qt.FontRole: + return self.__font + + elif role == qt.Qt.BackgroundColorRole: + pos = (row << 4) + column + if column != 0x10 and pos >= len(self.__connector): + return self.__palette.color(qt.QPalette.Disabled, qt.QPalette.Background) + else: + return None + + return None + + def headerData(self, section, orientation, role=qt.Qt.DisplayRole): + """Returns the 0-based row or column index, for display in the + horizontal and vertical headers""" + if section == -1: + # PyQt4 send -1 when there is columns but no rows + return None + + if role == qt.Qt.DisplayRole: + if orientation == qt.Qt.Vertical: + return "%02X" % (section << 4) + if orientation == qt.Qt.Horizontal: + if section == 0x10: + return "ASCII" + else: + return "%02X" % section + elif role == qt.Qt.FontRole: + return self.__font + elif role == qt.Qt.TextAlignmentRole: + if orientation == qt.Qt.Vertical: + return qt.Qt.AlignRight + if orientation == qt.Qt.Horizontal: + if section == 0x10: + return qt.Qt.AlignLeft + else: + return qt.Qt.AlignCenter + return None + + def flags(self, index): + """QAbstractTableModel method to inform the view whether data + is editable or not. + """ + row = index.row() + column = index.column() + pos = (row << 4) + column + if column != 0x10 and pos >= len(self.__connector): + return qt.Qt.NoItemFlags + return qt.QAbstractTableModel.flags(self, index) + + def setArrayData(self, data): + """Set the data array. + + :param data: A numpy object or a dataset. + """ + if qt.qVersion() > "4.6": + self.beginResetModel() + + self.__connector = None + self.__data = data + if self.__data is not None: + if silx.io.utils.is_dataset(self.__data): + data = data[()] + elif isinstance(self.__data, numpy.ndarray): + data = data[()] + self.__connector = _VoidConnector(data) + + if qt.qVersion() > "4.6": + self.endResetModel() + else: + self.reset() + + def arrayData(self): + """Returns the internal data. + + :rtype: numpy.ndarray of h5py.Dataset + """ + return self.__data + + +class HexaTableView(qt.QTableView): + """TableView using HexaTableModel as default model. + + It customs the column size to provide a better layout. + """ + def __init__(self, parent=None): + """ + Constructor + + :param qt.QWidget parent: parent QWidget + """ + qt.QTableView.__init__(self, parent) + + model = HexaTableModel(self) + self.setModel(model) + self._copyAction = CopySelectedCellsAction(self) + self.addAction(self._copyAction) + + def copy(self): + self._copyAction.trigger() + + def setArrayData(self, data): + """Set the data array. + + :param data: A numpy object or a dataset. + """ + self.model().setArrayData(data) + self.__fixHeader() + + def __fixHeader(self): + """Update the view according to the state of the auto-resize""" + header = self.horizontalHeader() + if qt.qVersion() < "5.0": + setResizeMode = header.setResizeMode + else: + setResizeMode = header.setSectionResizeMode + + header.setDefaultSectionSize(30) + header.setStretchLastSection(True) + for i in range(0x10): + setResizeMode(i, qt.QHeaderView.Fixed) + setResizeMode(0x10, qt.QHeaderView.Stretch) diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py index 343c7f9..b820380 100644 --- a/silx/gui/data/NXdataWidgets.py +++ b/silx/gui/data/NXdataWidgets.py @@ -26,7 +26,7 @@ """ __authors__ = ["P. Knobel"] __license__ = "MIT" -__date__ = "20/03/2017" +__date__ = "27/06/2017" import numpy @@ -135,8 +135,8 @@ class ArrayCurvePlot(qt.QWidget): self.selectorDock.show() self._plot.setGraphTitle(title or "") - self._plot.setGraphXLabel(self.__axis_name or "X") - self._plot.setGraphYLabel(self.__signal_name or "Y") + self._plot.getXAxis().setLabel(self.__axis_name or "X") + self._plot.getYAxis().setLabel(self.__signal_name or "Y") self._updateCurve() if not self.__selector_is_connected: @@ -188,8 +188,8 @@ class ArrayCurvePlot(qt.QWidget): xerror=self.__axis_errors, yerror=y_errors) self._plot.resetZoom() - self._plot.setGraphXLabel(self.__axis_name) - self._plot.setGraphYLabel(self.__signal_name) + self._plot.getXAxis().setLabel(self.__axis_name) + self._plot.getYAxis().setLabel(self.__signal_name) def clear(self): self._plot.clear() @@ -289,8 +289,8 @@ class ArrayImagePlot(qt.QWidget): self.selectorDock.show() self._plot.setGraphTitle(title or "") - self._plot.setGraphXLabel(self.__x_axis_name or "X") - self._plot.setGraphYLabel(self.__y_axis_name or "Y") + self._plot.getXAxis().setLabel(self.__x_axis_name or "X") + self._plot.getYAxis().setLabel(self.__y_axis_name or "Y") self._updateImage() @@ -352,8 +352,8 @@ class ArrayImagePlot(qt.QWidget): numpy.ravel(scattery), numpy.ravel(img), legend=legend) - self._plot.setGraphXLabel(self.__x_axis_name) - self._plot.setGraphYLabel(self.__y_axis_name) + self._plot.getXAxis().setLabel(self.__x_axis_name) + self._plot.getYAxis().setLabel(self.__y_axis_name) self._plot.resetZoom() def clear(self): @@ -450,8 +450,8 @@ class ArrayStackPlot(qt.QWidget): self._stack_view.setGraphTitle(title or "") # by default, the z axis is the image position (dimension not plotted) - self._stack_view.setGraphXLabel(self.__x_axis_name or "X") - self._stack_view.setGraphYLabel(self.__y_axis_name or "Y") + self._stack_view.getPlot().getXAxis().setLabel(self.__x_axis_name or "X") + self._stack_view.getPlot().getYAxis().setLabel(self.__y_axis_name or "Y") self._updateStack() diff --git a/silx/gui/data/RecordTableView.py b/silx/gui/data/RecordTableView.py index ce6a178..54881b7 100644 --- a/silx/gui/data/RecordTableView.py +++ b/silx/gui/data/RecordTableView.py @@ -37,7 +37,7 @@ from silx.gui.widgets.TableWidget import CopySelectedCellsAction __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "27/01/2017" +__date__ = "02/10/2017" class _MultiLineItem(qt.QItemDelegate): @@ -206,9 +206,9 @@ class RecordTableModel(qt.QAbstractTableModel): data = data[key[1]] if role == qt.Qt.DisplayRole: - return self.__formatter.toString(data) + return self.__formatter.toString(data, dtype=self.__data.dtype) elif role == qt.Qt.EditRole: - return self.__editFormatter.toString(data) + return self.__editFormatter.toString(data, dtype=self.__data.dtype) return None def headerData(self, section, orientation, role=qt.Qt.DisplayRole): @@ -270,11 +270,11 @@ class RecordTableModel(qt.QAbstractTableModel): else: self.__is_array = False - self.__fields = [] if data is not None: if data.dtype.fields is not None: - for name, (dtype, _index) in data.dtype.fields.items(): + fields = sorted(data.dtype.fields.items(), key=lambda e: e[1][1]) + for name, (dtype, _index) in fields: if dtype.shape != tuple(): keys = itertools.product(*[range(x) for x in dtype.shape]) for key in keys: diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py index f074de5..37e1f48 100644 --- a/silx/gui/data/TextFormatter.py +++ b/silx/gui/data/TextFormatter.py @@ -27,14 +27,18 @@ data module to format data as text in the same way.""" __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "26/04/2017" +__date__ = "27/09/2017" import numpy import numbers -import binascii from silx.third_party import six from silx.gui import qt +try: + import h5py +except ImportError: + h5py = None + class TextFormatter(qt.QObject): """Formatter to convert data to string. @@ -73,11 +77,13 @@ class TextFormatter(qt.QObject): self.__floatFormat = formatter.floatFormat() self.__useQuoteForText = formatter.useQuoteForText() self.__imaginaryUnit = formatter.imaginaryUnit() + self.__enumFormat = formatter.enumFormat() else: self.__integerFormat = "%d" self.__floatFormat = "%g" self.__useQuoteForText = True self.__imaginaryUnit = u"j" + self.__enumFormat = u"%(name)s(%(value)d)" def integerFormat(self): """Returns the format string controlling how the integer data @@ -162,40 +168,151 @@ class TextFormatter(qt.QObject): self.__imaginaryUnit = imaginaryUnit self.formatChanged.emit() - def toString(self, data): + def setEnumFormat(self, value): + """Set format string controlling how the enum data are + formated by this object. + + :param str value: Format string (e.g. "%(name)s(%(value)d)"). + This is the C-style format string used by python when formatting + strings with the modulus operator. + """ + if self.__enumFormat == value: + return + self.__enumFormat = value + self.formatChanged.emit() + + def enumFormat(self): + """Returns the format string controlling how the enum data + are formated by this object. + + This is the C-style format string used by python when formatting + strings with the modulus operator. + + :rtype: str + """ + return self.__enumFormat + + def __formatText(self, text): + if self.__useQuoteForText: + text = "\"%s\"" % text.replace("\\", "\\\\").replace("\"", "\\\"") + return text + + def __formatBinary(self, data): + if isinstance(data, numpy.void): + if six.PY2: + data = [ord(d) for d in data.item()] + else: + data = data.item().astype(numpy.uint8) + else: + data = [ord(d) for d in data] + data = ["\\x%02X" % d for d in data] + if self.__useQuoteForText: + return "b\"%s\"" % "".join(data) + else: + return "".join(data) + + def __formatSafeAscii(self, data): + if six.PY2: + data = [ord(d) for d in data] + data = [chr(d) if (d > 0x20 and d < 0x7F) else "\\x%02X" % d for d in data] + if self.__useQuoteForText: + data = [c if c != '"' else "\\" + c for c in data] + return "b\"%s\"" % "".join(data) + else: + return "".join(data) + + def __formatH5pyObject(self, data, dtype): + # That's an HDF5 object + ref = h5py.check_dtype(ref=dtype) + if ref is not None: + if bool(data): + return "REF" + else: + return "NULL_REF" + vlen = h5py.check_dtype(vlen=dtype) + if vlen is not None: + if vlen == six.text_type: + # HDF5 UTF8 + return self.__formatText(data) + elif vlen == six.binary_type: + # HDF5 ASCII + try: + text = "%s" % data.decode("ascii") + return self.__formatText(text) + except UnicodeDecodeError: + return self.__formatSafeAscii(data) + return None + + def toString(self, data, dtype=None): """Format a data into a string using formatter options :param object data: Data to render + :param dtype: enforce a dtype (mostly used to remember the h5py dtype, + special h5py dtypes are not propagated from array to items) :rtype: str """ if isinstance(data, tuple): text = [self.toString(d) for d in data] return "(" + " ".join(text) + ")" - elif isinstance(data, (list, numpy.ndarray)): + elif isinstance(data, list): text = [self.toString(d) for d in data] return "[" + " ".join(text) + "]" + elif isinstance(data, (numpy.ndarray)): + if dtype is None: + dtype = data.dtype + if data.shape == (): + # it is a scaler + return self.toString(data[()], dtype) + else: + text = [self.toString(d, dtype) for d in data] + return "[" + " ".join(text) + "]" elif isinstance(data, numpy.void): - dtype = data.dtype + if dtype is None: + dtype = data.dtype if data.dtype.fields is not None: - text = [self.toString(data[f]) for f in dtype.fields] + text = [self.toString(data[f], dtype) for f in dtype.fields] return "(" + " ".join(text) + ")" - return "0x" + binascii.hexlify(data).decode("ascii") - elif isinstance(data, (numpy.string_, numpy.object_, bytes)): - # This have to be done before checking python string inheritance + return self.__formatBinary(data) + elif isinstance(data, (numpy.unicode_, six.text_type)): + return self.__formatText(data) + elif isinstance(data, (numpy.string_, six.binary_type)): + if dtype is not None: + # Maybe a sub item from HDF5 + if dtype.kind == 'S': + try: + text = "%s" % data.decode("ascii") + return self.__formatText(text) + except UnicodeDecodeError: + return self.__formatSafeAscii(data) + elif dtype.kind == 'O': + if h5py is not None: + text = self.__formatH5pyObject(data, dtype) + if text is not None: + return text try: + # Try ascii/utf-8 text = "%s" % data.decode("utf-8") - if self.__useQuoteForText: - text = "\"%s\"" % text.replace("\"", "\\\"") - return text + return self.__formatText(text) except UnicodeDecodeError: pass - return "0x" + binascii.hexlify(data).decode("ascii") + return self.__formatBinary(data) elif isinstance(data, six.string_types): text = "%s" % data - if self.__useQuoteForText: - text = "\"%s\"" % text.replace("\"", "\\\"") - return text - elif isinstance(data, (numpy.integer, numbers.Integral)): + return self.__formatText(text) + elif isinstance(data, (numpy.integer)): + if dtype is None: + dtype = data.dtype + if h5py is not None: + enumType = h5py.check_dtype(enum=dtype) + if enumType is not None: + for key, value in enumType.items(): + if value == data: + result = {} + result["name"] = key + result["value"] = data + return self.__enumFormat % result + return self.__integerFormat % data + elif isinstance(data, (numbers.Integral)): return self.__integerFormat % data elif isinstance(data, (numbers.Real, numpy.floating)): # It have to be done before complex checking @@ -219,4 +336,21 @@ class TextFormatter(qt.QObject): template = self.__floatFormat params = (data.real) return template % params + elif h5py is not None and isinstance(data, h5py.h5r.Reference): + dtype = h5py.special_dtype(ref=h5py.Reference) + text = self.__formatH5pyObject(data, dtype) + return text + elif h5py is not None and isinstance(data, h5py.h5r.RegionReference): + dtype = h5py.special_dtype(ref=h5py.RegionReference) + text = self.__formatH5pyObject(data, dtype) + return text + elif isinstance(data, numpy.object_) or dtype is not None: + if dtype is None: + dtype = data.dtype + if h5py is not None: + text = self.__formatH5pyObject(data, dtype) + if text is not None: + return text + # That's a numpy object + return str(data) return str(data) diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py index 5a0de0b..dd3114a 100644 --- a/silx/gui/data/test/test_dataviewer.py +++ b/silx/gui/data/test/test_dataviewer.py @@ -24,7 +24,7 @@ # ###########################################################################*/ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "10/04/2017" +__date__ = "22/08/2017" import os import tempfile @@ -42,8 +42,6 @@ from silx.gui.data.DataViewerFrame import DataViewerFrame from silx.gui.test.utils import SignalListener from silx.gui.test.utils import TestCaseQt -from silx.gui.hdf5.test import _mock - try: import h5py except ImportError: @@ -111,6 +109,24 @@ class AbstractDataViewerTests(TestCaseQt): self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) self.assertIn(DataViewer.PLOT2D_MODE, availableModes) + def test_plot_2d_bool(self): + data = numpy.zeros((10, 10), dtype=numpy.bool) + data[::2, ::2] = True + widget = self.create_widget() + widget.setData(data) + availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) + self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) + self.assertIn(DataViewer.PLOT2D_MODE, availableModes) + + def test_plot_2d_complex_data(self): + data = numpy.arange(3 ** 2, dtype=numpy.complex) + data.shape = [3] * 2 + widget = self.create_widget() + widget.setData(data) + availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) + self.assertEqual(DataViewer.RAW_MODE, widget.displayMode()) + self.assertIn(DataViewer.PLOT2D_MODE, availableModes) + def test_plot_3d_data(self): data = numpy.arange(3 ** 3) data.shape = [3] * 3 @@ -212,6 +228,7 @@ class AbstractDataViewerTests(TestCaseQt): self.assertTrue(view not in widget.availableViews()) self.assertTrue(view not in widget.currentAvailableViews()) + class TestDataViewer(AbstractDataViewerTests): def create_widget(self): return DataViewer() @@ -225,11 +242,10 @@ class TestDataViewerFrame(AbstractDataViewerTests): class TestDataView(TestCaseQt): def createComplexData(self): - line = [1, 2j, 3+3j, 4] + line = [1, 2j, 3 + 3j, 4] image = [line, line, line, line] cube = [image, image, image, image] - data = numpy.array(cube, - dtype=numpy.complex) + data = numpy.array(cube, dtype=numpy.complex) return data def createDataViewWithData(self, dataViewClass, data): diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py index f21e033..2a7a66b 100644 --- a/silx/gui/data/test/test_textformatter.py +++ b/silx/gui/data/test/test_textformatter.py @@ -24,13 +24,22 @@ # ###########################################################################*/ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "24/01/2017" +__date__ = "27/09/2017" import unittest +import shutil +import tempfile +import numpy from silx.gui.test.utils import TestCaseQt from silx.gui.test.utils import SignalListener from ..TextFormatter import TextFormatter +from silx.third_party import six + +try: + import h5py +except ImportError: + h5py = None class TestTextFormatter(TestCaseQt): @@ -83,10 +92,108 @@ class TestTextFormatter(TestCaseQt): self.assertEquals(result, '"toto"') +class TestTextFormatterWithH5py(TestCaseQt): + + @classmethod + def setUpClass(cls): + super(TestTextFormatterWithH5py, cls).setUpClass() + if h5py is None: + raise unittest.SkipTest("h5py is not available") + + cls.tmpDirectory = tempfile.mkdtemp() + cls.h5File = h5py.File("%s/formatter.h5" % cls.tmpDirectory, mode="w") + cls.formatter = TextFormatter() + + @classmethod + def tearDownClass(cls): + super(TestTextFormatterWithH5py, cls).tearDownClass() + cls.h5File.close() + cls.h5File = None + shutil.rmtree(cls.tmpDirectory) + + def create_dataset(self, data, dtype=None): + testName = "%s" % self.id() + dataset = self.h5File.create_dataset(testName, data=data, dtype=dtype) + return dataset + + def testAscii(self): + d = self.create_dataset(data=b"abc") + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, '"abc"') + + def testUnicode(self): + d = self.create_dataset(data=u"i\u2661cookies") + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(len(result), 11) + self.assertEquals(result, u'"i\u2661cookies"') + + def testBadAscii(self): + d = self.create_dataset(data=b"\xF0\x9F\x92\x94") + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, 'b"\\xF0\\x9F\\x92\\x94"') + + def testVoid(self): + d = self.create_dataset(data=numpy.void(b"abc\xF0")) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, 'b"\\x61\\x62\\x63\\xF0"') + + def testEnum(self): + dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42})) + d = numpy.array(42, dtype=dtype) + d = self.create_dataset(data=d) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, 'BLUE(42)') + + def testRef(self): + dtype = h5py.special_dtype(ref=h5py.Reference) + d = numpy.array(self.h5File.ref, dtype=dtype) + d = self.create_dataset(data=d) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, 'REF') + + def testArrayAscii(self): + d = self.create_dataset(data=[b"abc"]) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, '["abc"]') + + def testArrayUnicode(self): + dtype = h5py.special_dtype(vlen=six.text_type) + d = numpy.array([u"i\u2661cookies"], dtype=dtype) + d = self.create_dataset(data=d) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(len(result), 13) + self.assertEquals(result, u'["i\u2661cookies"]') + + def testArrayBadAscii(self): + d = self.create_dataset(data=[b"\xF0\x9F\x92\x94"]) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, '[b"\\xF0\\x9F\\x92\\x94"]') + + def testArrayVoid(self): + d = self.create_dataset(data=numpy.void([b"abc\xF0"])) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, '[b"\\x61\\x62\\x63\\xF0"]') + + def testArrayEnum(self): + dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42})) + d = numpy.array([42, 1, 100], dtype=dtype) + d = self.create_dataset(data=d) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, '[BLUE(42) GREEN(1) 100]') + + def testArrayRef(self): + dtype = h5py.special_dtype(ref=h5py.Reference) + d = numpy.array([self.h5File.ref, None], dtype=dtype) + d = self.create_dataset(data=d) + result = self.formatter.toString(d[()], dtype=d.dtype) + self.assertEquals(result, '[REF NULL_REF]') + + def suite(): + loadTests = unittest.defaultTestLoader.loadTestsFromTestCase test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestTextFormatter)) + test_suite.addTest(loadTests(TestTextFormatter)) + test_suite.addTest(loadTests(TestTextFormatterWithH5py)) return test_suite |