summaryrefslogtreecommitdiff
path: root/silx/gui/data
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/data')
-rw-r--r--silx/gui/data/ArrayTableModel.py7
-rw-r--r--silx/gui/data/ArrayTableWidget.py2
-rw-r--r--silx/gui/data/DataViewer.py10
-rw-r--r--silx/gui/data/DataViewerFrame.py12
-rw-r--r--silx/gui/data/DataViews.py209
-rw-r--r--silx/gui/data/Hdf5TableView.py76
-rw-r--r--silx/gui/data/HexaTableView.py278
-rw-r--r--silx/gui/data/NXdataWidgets.py22
-rw-r--r--silx/gui/data/RecordTableView.py10
-rw-r--r--silx/gui/data/TextFormatter.py168
-rw-r--r--silx/gui/data/test/test_dataviewer.py28
-rw-r--r--silx/gui/data/test/test_textformatter.py113
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