From a763e5d1b3921b3194f3d4e94ab9de3fbe08bbdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Picca=20Fr=C3=A9d=C3=A9ric-Emmanuel?= Date: Tue, 28 May 2019 08:16:16 +0200 Subject: New upstream version 0.10.1+dfsg --- silx/gui/data/DataViewer.py | 92 ++--- silx/gui/data/DataViewerFrame.py | 5 +- silx/gui/data/DataViewerSelector.py | 6 +- silx/gui/data/DataViews.py | 499 +++++++++++++++++++++++++-- silx/gui/data/Hdf5TableView.py | 13 +- silx/gui/data/HexaTableView.py | 8 +- silx/gui/data/NXdataWidgets.py | 439 +++++++++++++++++++++-- silx/gui/data/TextFormatter.py | 50 ++- silx/gui/data/test/test_arraywidget.py | 6 +- silx/gui/data/test/test_dataviewer.py | 14 +- silx/gui/data/test/test_numpyaxesselector.py | 7 +- silx/gui/data/test/test_textformatter.py | 12 +- 12 files changed, 959 insertions(+), 192 deletions(-) (limited to 'silx/gui/data') diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py index 4db2863..b33a931 100644 --- a/silx/gui/data/DataViewer.py +++ b/silx/gui/data/DataViewer.py @@ -32,12 +32,10 @@ from silx.gui.data.DataViews import _normalizeData import logging from silx.gui import qt from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector -from silx.utils import deprecation -from silx.utils.property import classproperty __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "24/04/2018" +__date__ = "12/02/2019" _logger = logging.getLogger(__name__) @@ -70,66 +68,6 @@ class DataViewer(qt.QFrame): viewer.setVisible(True) """ - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.EMPTY_MODE", since_version="0.7", skip_backtrace_count=2) - def EMPTY_MODE(self): - return DataViews.EMPTY_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.PLOT1D_MODE", since_version="0.7", skip_backtrace_count=2) - def PLOT1D_MODE(self): - return DataViews.PLOT1D_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.PLOT2D_MODE", since_version="0.7", skip_backtrace_count=2) - def PLOT2D_MODE(self): - return DataViews.PLOT2D_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.PLOT3D_MODE", since_version="0.7", skip_backtrace_count=2) - def PLOT3D_MODE(self): - return DataViews.PLOT3D_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.RAW_MODE", since_version="0.7", skip_backtrace_count=2) - def RAW_MODE(self): - return DataViews.RAW_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.RAW_ARRAY_MODE", since_version="0.7", skip_backtrace_count=2) - def RAW_ARRAY_MODE(self): - return DataViews.RAW_ARRAY_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.RAW_RECORD_MODE", since_version="0.7", skip_backtrace_count=2) - def RAW_RECORD_MODE(self): - return DataViews.RAW_RECORD_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.RAW_SCALAR_MODE", since_version="0.7", skip_backtrace_count=2) - def RAW_SCALAR_MODE(self): - return DataViews.RAW_SCALAR_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.STACK_MODE", since_version="0.7", skip_backtrace_count=2) - def STACK_MODE(self): - return DataViews.STACK_MODE - - # TODO: Can be removed for silx 0.8 - @classproperty - @deprecation.deprecated(replacement="DataViews.HDF5_MODE", since_version="0.7", skip_backtrace_count=2) - def HDF5_MODE(self): - return DataViews.HDF5_MODE - displayedViewChanged = qt.Signal(object) """Emitted when the displayed view changes""" @@ -288,6 +226,7 @@ class DataViewer(qt.QFrame): else: self.__displayedData = self.__data + # TODO: would be good to avoid that, it should be synchonous qt.QTimer.singleShot(10, self.__setDataInView) def __setDataInView(self): @@ -405,18 +344,16 @@ class DataViewer(qt.QFrame): data = self.__data info = self._getInfo() # sort available views according to priority - priorities = [v.getDataPriority(data, info) for v in self.__views] - views = zip(priorities, self.__views) + views = [] + for v in self.__views: + views.extend(v.getMatchingViews(data, info)) + views = [(v.getCachedDataPriority(data, info), v) for v in views] views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views) views = sorted(views, reverse=True) + views = [v[1] for v in views] # store available views - if len(views) == 0: - self.__setCurrentAvailableViews([]) - available = [] - else: - available = [v[1] for v in views] - self.__setCurrentAvailableViews(available) + self.__setCurrentAvailableViews(views) def __updateView(self): """Display the data using the widget which fit the best""" @@ -447,7 +384,7 @@ class DataViewer(qt.QFrame): priority to lowest. :rtype: DataView """ - hdf5View = self.getViewFromModeId(DataViewer.HDF5_MODE) + hdf5View = self.getViewFromModeId(DataViews.HDF5_MODE) if hdf5View in available: return hdf5View return self.getViewFromModeId(DataViews.EMPTY_MODE) @@ -487,6 +424,17 @@ class DataViewer(qt.QFrame): """ return self.__currentAvailableViews + def getReachableViews(self): + """Returns the list of reachable views from the registred available + views. + + :rtype: List[DataView] + """ + views = [] + for v in self.availableViews(): + views.extend(v.getReachableViews()) + return views + def availableViews(self): """Returns the list of registered views diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py index 4e6d2e8..9bfb95b 100644 --- a/silx/gui/data/DataViewerFrame.py +++ b/silx/gui/data/DataViewerFrame.py @@ -27,7 +27,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "24/04/2018" +__date__ = "12/02/2019" from silx.gui import qt from .DataViewer import DataViewer @@ -120,6 +120,9 @@ class DataViewerFrame(qt.QWidget): """ self.__dataViewer.setGlobalHooks(hooks) + def getReachableViews(self): + return self.__dataViewer.getReachableViews() + def availableViews(self): """Returns the list of registered views diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py index 35bbe99..a1e9947 100644 --- a/silx/gui/data/DataViewerSelector.py +++ b/silx/gui/data/DataViewerSelector.py @@ -29,7 +29,7 @@ from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "23/01/2018" +__date__ = "12/02/2019" import weakref import functools @@ -85,7 +85,7 @@ class DataViewerSelector(qt.QWidget): iconSize = qt.QSize(16, 16) - for view in self.__dataViewer.availableViews(): + for view in self.__dataViewer.getReachableViews(): label = view.label() icon = view.icon() button = qt.QPushButton(label) @@ -155,7 +155,7 @@ class DataViewerSelector(qt.QWidget): self.__dataViewer.setDisplayedView(view) def __checkAvailableButtons(self): - views = set(self.__dataViewer.availableViews()) + views = set(self.__dataViewer.getReachableViews()) if views == set(self.__buttons.keys()): return # Recreate all the buttons diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py index 2291e87..6575d0d 100644 --- a/silx/gui/data/DataViews.py +++ b/silx/gui/data/DataViews.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility +# Copyright (c) 2016-2019 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -31,6 +31,7 @@ import numbers import numpy import silx.io +from silx.utils import deprecation from silx.gui import qt, icons from silx.gui.data.TextFormatter import TextFormatter from silx.io import nxdata @@ -41,7 +42,7 @@ from silx.gui.dialog.ColormapDialog import ColormapDialog __authors__ = ["V. Valls", "P. Knobel"] __license__ = "MIT" -__date__ = "23/05/2018" +__date__ = "19/02/2019" _logger = logging.getLogger(__name__) @@ -67,6 +68,8 @@ NXDATA_CURVE_MODE = 73 NXDATA_XYVSCATTER_MODE = 74 NXDATA_IMAGE_MODE = 75 NXDATA_STACK_MODE = 76 +NXDATA_VOLUME_MODE = 77 +NXDATA_VOLUME_AS_STACK_MODE = 78 def _normalizeData(data): @@ -100,6 +103,7 @@ class DataInfo(object): """Store extracted information from a data""" def __init__(self, data): + self.__priorities = {} data = self.normalizeData(data) self.isArray = False self.interpretation = None @@ -131,9 +135,6 @@ class DataInfo(object): elif nx_class == "NXdata": # group claiming to be NXdata could not be parsed self.isInvalidNXdata = True - elif nx_class == "NXentry" and "default" in data.attrs: - # entry claiming to have a default NXdata could not be parsed - self.isInvalidNXdata = True elif nx_class == "NXroot" or silx.io.is_file(data): # root claiming to have a default entry if "default" in data.attrs: @@ -141,6 +142,9 @@ class DataInfo(object): if def_entry in data and "default" in data[def_entry].attrs: # and entry claims to have default NXdata self.isInvalidNXdata = True + elif "default" in data.attrs: + # group claiming to have a default NXdata could not be parsed + self.isInvalidNXdata = True if isinstance(data, numpy.ndarray): self.isArray = True @@ -201,6 +205,12 @@ class DataInfo(object): Else returns the data.""" return _normalizeData(data) + def cachePriority(self, view, priority): + self.__priorities[view] = priority + + def getPriority(self, view): + return self.__priorities[view] + class DataViewHooks(object): """A set of hooks defined to custom the behaviour of the data views.""" @@ -357,6 +367,35 @@ class DataView(object): """ return [] + def getReachableViews(self): + """Returns the views that can be returned by `getMatchingViews`. + + :param object data: Any object to be displayed + :param DataInfo info: Information cached about this data + :rtype: List[DataView] + """ + return [self] + + def getMatchingViews(self, data, info): + """Returns the views according to data and info from the data. + + :param object data: Any object to be displayed + :param DataInfo info: Information cached about this data + :rtype: List[DataView] + """ + priority = self.getCachedDataPriority(data, info) + if priority == DataView.UNSUPPORTED: + return [] + return [self] + + def getCachedDataPriority(self, data, info): + try: + priority = info.getPriority(self) + except KeyError: + priority = self.getDataPriority(data, info) + info.cachePriority(self, priority) + return priority + def getDataPriority(self, data, info): """ Returns the priority of using this view according to a data. @@ -377,7 +416,53 @@ class DataView(object): return str(self) < str(other) -class CompositeDataView(DataView): +class _CompositeDataView(DataView): + """Contains sub views""" + + def getViews(self): + """Returns the direct sub views registered in this view. + + :rtype: List[DataView] + """ + raise NotImplementedError() + + def getReachableViews(self): + """Returns all views that can be reachable at on point. + + This method return any sub view provided (recursivly). + + :rtype: List[DataView] + """ + raise NotImplementedError() + + def getMatchingViews(self, data, info): + """Returns sub views matching this data and info. + + This method return any sub view provided (recursivly). + + :param object data: Any object to be displayed + :param DataInfo info: Information cached about this data + :rtype: List[DataView] + """ + raise NotImplementedError() + + @deprecation.deprecated(replacement="getReachableViews", since_version="0.10") + def availableViews(self): + return self.getViews() + + def isSupportedData(self, data, info): + """If true, the composite view allow sub views to access to this data. + Else this this data is considered as not supported by any of sub views + (incliding this composite view). + + :param object data: Any object to be displayed + :param DataInfo info: Information cached about this data + :rtype: bool + """ + return True + + +class SelectOneDataView(_CompositeDataView): """Data view which can display a data using different view according to the kind of the data.""" @@ -386,7 +471,7 @@ class CompositeDataView(DataView): :param qt.QWidget parent: Parent of the hold widget """ - super(CompositeDataView, self).__init__(parent, modeId, icon, label) + super(SelectOneDataView, self).__init__(parent, modeId, icon, label) self.__views = OrderedDict() self.__currentView = None @@ -395,7 +480,7 @@ class CompositeDataView(DataView): :param DataViewHooks hooks: The data view hooks to use """ - super(CompositeDataView, self).setHooks(hooks) + super(SelectOneDataView, self).setHooks(hooks) if hooks is not None: for v in self.__views: v.setHooks(hooks) @@ -407,16 +492,40 @@ class CompositeDataView(DataView): dataView.setHooks(hooks) self.__views[dataView] = None - def availableViews(self): + def getReachableViews(self): + views = [] + addSelf = False + for v in self.__views: + if isinstance(v, SelectManyDataView): + views.extend(v.getReachableViews()) + else: + addSelf = True + if addSelf: + # Single views are hidden by this view + views.insert(0, self) + return views + + def getMatchingViews(self, data, info): + if not self.isSupportedData(data, info): + return [] + view = self.__getBestView(data, info) + if isinstance(view, SelectManyDataView): + return view.getMatchingViews(data, info) + else: + return [self] + + def getViews(self): """Returns the list of registered views :rtype: List[DataView] """ return list(self.__views.keys()) - def getBestView(self, data, info): + def __getBestView(self, data, info): """Returns the best view according to priorities.""" - views = [(v.getDataPriority(data, info), v) for v in self.__views.keys()] + if not self.isSupportedData(data, info): + return None + views = [(v.getCachedDataPriority(data, info), v) for v in self.__views.keys()] views = filter(lambda t: t[0] > DataView.UNSUPPORTED, views) views = sorted(views, key=lambda t: t[0], reverse=True) @@ -471,17 +580,17 @@ class CompositeDataView(DataView): self.__currentView.setData(data) def axesNames(self, data, info): - view = self.getBestView(data, info) + view = self.__getBestView(data, info) self.__currentView = view return view.axesNames(data, info) def getDataPriority(self, data, info): - view = self.getBestView(data, info) + view = self.__getBestView(data, info) self.__currentView = view if view is None: return DataView.UNSUPPORTED else: - return view.getDataPriority(data, info) + return view.getCachedDataPriority(data, info) def replaceView(self, modeId, newView): """Replace a data view with a custom view. @@ -502,7 +611,7 @@ class CompositeDataView(DataView): if view.modeId() == modeId: oldView = view break - elif isinstance(view, CompositeDataView): + elif isinstance(view, _CompositeDataView): # recurse hooks = self.getHooks() if hooks is not None: @@ -519,6 +628,135 @@ class CompositeDataView(DataView): return True +# NOTE: SelectOneDataView was introduced with silx 0.10 +CompositeDataView = SelectOneDataView + + +class SelectManyDataView(_CompositeDataView): + """Data view which can select a set of sub views according to + the kind of the data. + + This view itself is abstract and is not exposed. + """ + + def __init__(self, parent, views=None): + """Constructor + + :param qt.QWidget parent: Parent of the hold widget + """ + super(SelectManyDataView, self).__init__(parent, modeId=None, icon=None, label=None) + if views is None: + views = [] + self.__views = views + + def setHooks(self, hooks): + """Set the data context to use with this view. + + :param DataViewHooks hooks: The data view hooks to use + """ + super(SelectManyDataView, self).setHooks(hooks) + if hooks is not None: + for v in self.__views: + v.setHooks(hooks) + + def addView(self, dataView): + """Add a new dataview to the available list.""" + hooks = self.getHooks() + if hooks is not None: + dataView.setHooks(hooks) + self.__views.append(dataView) + + def getViews(self): + """Returns the list of registered views + + :rtype: List[DataView] + """ + return list(self.__views) + + def getReachableViews(self): + views = [] + for v in self.__views: + views.extend(v.getReachableViews()) + return views + + def getMatchingViews(self, data, info): + """Returns the views according to data and info from the data. + + :param object data: Any object to be displayed + :param DataInfo info: Information cached about this data + """ + if not self.isSupportedData(data, info): + return [] + views = [v for v in self.__views if v.getCachedDataPriority(data, info) != DataView.UNSUPPORTED] + return views + + def customAxisNames(self): + raise RuntimeError("Abstract view") + + def setCustomAxisValue(self, name, value): + raise RuntimeError("Abstract view") + + def select(self): + raise RuntimeError("Abstract view") + + def createWidget(self, parent): + raise RuntimeError("Abstract view") + + def clear(self): + for v in self.__views: + v.clear() + + def setData(self, data): + raise RuntimeError("Abstract view") + + def axesNames(self, data, info): + raise RuntimeError("Abstract view") + + def getDataPriority(self, data, info): + if not self.isSupportedData(data, info): + return DataView.UNSUPPORTED + priorities = [v.getCachedDataPriority(data, info) for v in self.__views] + priorities = [v for v in priorities if v != DataView.UNSUPPORTED] + priorities = sorted(priorities) + if len(priorities) == 0: + return DataView.UNSUPPORTED + return priorities[-1] + + def replaceView(self, modeId, newView): + """Replace a data view with a custom view. + Return True in case of success, False in case of failure. + + .. note:: + + This method must be called just after instantiation, before + the viewer is used. + + :param int modeId: Unique mode ID identifying the DataView to + be replaced. + :param DataViews.DataView newView: New data view + :return: True if replacement was successful, else False + """ + oldView = None + for iview, view in enumerate(self.__views): + if view.modeId() == modeId: + oldView = view + break + elif isinstance(view, CompositeDataView): + # recurse + hooks = self.getHooks() + if hooks is not None: + newView.setHooks(hooks) + if view.replaceView(modeId, newView): + return True + + if oldView is None: + return False + + # replace oldView with new view in dict + self.__views[iview] = newView + return True + + class _EmptyView(DataView): """Dummy view to display nothing""" @@ -1096,17 +1334,6 @@ class _InvalidNXdataView(DataView): # invalid: could not even be parsed by NXdata self._msg = "Group has @NX_class = NXdata, but could not be interpreted" self._msg += " as valid NXdata." - elif nx_class == "NXentry": - self._msg = "NXentry group provides a @default attribute," - default_nxdata_name = data.attrs["default"] - if default_nxdata_name not in data: - self._msg += " but no corresponding NXdata group exists." - elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata": - self._msg += " but the corresponding item is not a " - self._msg += "NXdata group." - else: - self._msg += " but the corresponding NXdata seems to be" - self._msg += " malformed." elif nx_class == "NXroot" or silx.io.is_file(data): default_entry = data[data.attrs["default"]] default_nxdata_name = default_entry.attrs["default"] @@ -1122,6 +1349,17 @@ class _InvalidNXdataView(DataView): else: self._msg += " but the corresponding NXdata seems to be" self._msg += " malformed." + else: + self._msg = "Group provides a @default attribute," + default_nxdata_name = data.attrs["default"] + if default_nxdata_name not in data: + self._msg += " but no corresponding NXdata group exists." + elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata": + self._msg += " but the corresponding item is not a " + self._msg += "NXdata group." + else: + self._msg += " but the corresponding NXdata seems to be" + self._msg += " malformed." return 100 @@ -1277,7 +1515,7 @@ class _NXdataXYVScatterView(DataView): class _NXdataImageView(DataView): """DataView using a Plot2D for displaying NXdata images: - 2-D signal or n-D signals with *@interpretation=spectrum*.""" + 2-D signal or n-D signals with *@interpretation=image*.""" def __init__(self, parent): DataView.__init__(self, parent, modeId=NXDATA_IMAGE_MODE) @@ -1323,6 +1561,53 @@ class _NXdataImageView(DataView): return DataView.UNSUPPORTED +class _NXdataComplexImageView(DataView): + """DataView using a ComplexImageView for displaying NXdata complex images: + 2-D signal or n-D signals with *@interpretation=image*.""" + def __init__(self, parent): + DataView.__init__(self, parent, + modeId=NXDATA_IMAGE_MODE) + + def createWidget(self, parent): + from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot + widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap()) + widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) + return widget + + def clear(self): + self.getWidget().clear() + + def setData(self, data): + data = self.normalizeData(data) + nxd = nxdata.get_default(data, validate=False) + + # last two axes are Y & X + img_slicing = slice(-2, None) + y_axis, x_axis = nxd.axes[img_slicing] + y_label, x_label = nxd.axes_names[img_slicing] + + self.getWidget().setImageData( + [nxd.signal] + nxd.auxiliary_signals, + x_axis=x_axis, y_axis=y_axis, + signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names, + xlabel=x_label, ylabel=y_label, + title=nxd.title) + + def axesNames(self, data, info): + # disabled (used by default axis selector widget in Hdf5Viewer) + return None + + def getDataPriority(self, data, info): + data = self.normalizeData(data) + + if info.hasNXdata and not info.isInvalidNXdata: + nxd = nxdata.get_default(data, validate=False) + if nxd.is_image and numpy.iscomplexobj(nxd.signal): + return 100 + + return DataView.UNSUPPORTED + + class _NXdataStackView(DataView): def __init__(self, parent): DataView.__init__(self, parent, @@ -1368,6 +1653,154 @@ class _NXdataStackView(DataView): return DataView.UNSUPPORTED +class _NXdataVolumeView(DataView): + def __init__(self, parent): + DataView.__init__(self, parent, + label="NXdata (3D)", + icon=icons.getQIcon("view-nexus"), + modeId=NXDATA_VOLUME_MODE) + try: + import silx.gui.plot3d # noqa + except ImportError: + _logger.warning("Plot3dView is not available") + _logger.debug("Backtrace", exc_info=True) + raise + + def normalizeData(self, data): + data = DataView.normalizeData(self, data) + data = _normalizeComplex(data) + return data + + def createWidget(self, parent): + from silx.gui.data.NXdataWidgets import ArrayVolumePlot + widget = ArrayVolumePlot(parent) + return widget + + def axesNames(self, data, info): + # disabled (used by default axis selector widget in Hdf5Viewer) + return None + + def clear(self): + self.getWidget().clear() + + def setData(self, data): + data = self.normalizeData(data) + nxd = nxdata.get_default(data, validate=False) + signal_name = nxd.signal_name + z_axis, y_axis, x_axis = nxd.axes[-3:] + z_label, y_label, x_label = nxd.axes_names[-3:] + title = nxd.title or signal_name + + widget = self.getWidget() + widget.setData( + nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis, + signal_name=signal_name, + xlabel=x_label, ylabel=y_label, zlabel=z_label, + title=title) + + def getDataPriority(self, data, info): + data = self.normalizeData(data) + if info.hasNXdata and not info.isInvalidNXdata: + if nxdata.get_default(data, validate=False).is_volume: + return 150 + + return DataView.UNSUPPORTED + + +class _NXdataVolumeAsStackView(DataView): + def __init__(self, parent): + DataView.__init__(self, parent, + label="NXdata (2D)", + icon=icons.getQIcon("view-nexus"), + modeId=NXDATA_VOLUME_AS_STACK_MODE) + + def createWidget(self, parent): + from silx.gui.data.NXdataWidgets import ArrayStackPlot + widget = ArrayStackPlot(parent) + widget.getStackView().setColormap(self.defaultColormap()) + widget.getStackView().getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) + return widget + + def axesNames(self, data, info): + # disabled (used by default axis selector widget in Hdf5Viewer) + return None + + def clear(self): + self.getWidget().clear() + + def setData(self, data): + data = self.normalizeData(data) + nxd = nxdata.get_default(data, validate=False) + signal_name = nxd.signal_name + z_axis, y_axis, x_axis = nxd.axes[-3:] + z_label, y_label, x_label = nxd.axes_names[-3:] + title = nxd.title or signal_name + + widget = self.getWidget() + widget.setStackData( + nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis, + signal_name=signal_name, + xlabel=x_label, ylabel=y_label, zlabel=z_label, + title=title) + # Override the colormap, while setStack overwrite it + widget.getStackView().setColormap(self.defaultColormap()) + + def getDataPriority(self, data, info): + data = self.normalizeData(data) + if info.isComplex: + return DataView.UNSUPPORTED + if info.hasNXdata and not info.isInvalidNXdata: + if nxdata.get_default(data, validate=False).is_volume: + return 200 + + return DataView.UNSUPPORTED + +class _NXdataComplexVolumeAsStackView(DataView): + def __init__(self, parent): + DataView.__init__(self, parent, + label="NXdata (2D)", + icon=icons.getQIcon("view-nexus"), + modeId=NXDATA_VOLUME_AS_STACK_MODE) + self._is_complex_data = False + + def createWidget(self, parent): + from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot + widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap()) + widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) + return widget + + def axesNames(self, data, info): + # disabled (used by default axis selector widget in Hdf5Viewer) + return None + + def clear(self): + self.getWidget().clear() + + def setData(self, data): + data = self.normalizeData(data) + nxd = nxdata.get_default(data, validate=False) + signal_name = nxd.signal_name + z_axis, y_axis, x_axis = nxd.axes[-3:] + z_label, y_label, x_label = nxd.axes_names[-3:] + title = nxd.title or signal_name + + self.getWidget().setImageData( + [nxd.signal] + nxd.auxiliary_signals, + x_axis=x_axis, y_axis=y_axis, + signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names, + xlabel=x_label, ylabel=y_label, title=nxd.title) + + def getDataPriority(self, data, info): + data = self.normalizeData(data) + if not info.isComplex: + return DataView.UNSUPPORTED + if info.hasNXdata and not info.isInvalidNXdata: + if nxdata.get_default(data, validate=False).is_volume: + return 200 + + return DataView.UNSUPPORTED + + class _NXdataView(CompositeDataView): """Composite view displaying NXdata groups using the most adequate widget depending on the dimensionality.""" @@ -1382,5 +1815,17 @@ class _NXdataView(CompositeDataView): self.addView(_NXdataScalarView(parent)) self.addView(_NXdataCurveView(parent)) self.addView(_NXdataXYVScatterView(parent)) + self.addView(_NXdataComplexImageView(parent)) self.addView(_NXdataImageView(parent)) self.addView(_NXdataStackView(parent)) + + # The 3D view can be displayed using 2 ways + nx3dViews = SelectManyDataView(parent) + nx3dViews.addView(_NXdataVolumeAsStackView(parent)) + nx3dViews.addView(_NXdataComplexVolumeAsStackView(parent)) + try: + nx3dViews.addView(_NXdataVolumeView(parent)) + except Exception: + _logger.warning("NXdataVolumeView is not available") + _logger.debug("Backtrace", exc_info=True) + self.addView(nx3dViews) diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py index 9e28fbf..d7c33f3 100644 --- a/silx/gui/data/Hdf5TableView.py +++ b/silx/gui/data/Hdf5TableView.py @@ -30,12 +30,14 @@ from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "05/07/2018" +__date__ = "12/02/2019" import collections import functools import os.path import logging +import h5py + from silx.gui import qt import silx.io from .TextFormatter import TextFormatter @@ -44,11 +46,6 @@ from silx.gui.widgets import HierarchicalTableView from ..hdf5.Hdf5Formatter import Hdf5Formatter from ..hdf5._utils import htmlFromDict -try: - import h5py -except ImportError: - h5py = None - _logger = logging.getLogger(__name__) @@ -198,11 +195,9 @@ class _CellFilterAvailableData(_CellData): } def __init__(self, filterId): - import h5py.version if h5py.version.hdf5_version_tuple >= (1, 10, 2): # Previous versions only returns True if the filter was first used # to decode a dataset - import h5py.h5z self.__availability = h5py.h5z.filter_avail(filterId) else: self.__availability = "na" @@ -416,7 +411,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): self.__data.addHeaderRow(headerLabel="Data info") - if h5py is not None and hasattr(obj, "id") and hasattr(obj.id, "get_type"): + if hasattr(obj, "id") and hasattr(obj.id, "get_type"): # display the HDF5 type self.__data.addHeaderValueRow("HDF5 type", self.__formatHdf5Type) self.__data.addHeaderValueRow("dtype", self.__formatDType) diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py index c86c0af..1617f0a 100644 --- a/silx/gui/data/HexaTableView.py +++ b/silx/gui/data/HexaTableView.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017 European Synchrotron Radiation Facility +# Copyright (c) 2017-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -28,11 +28,13 @@ hexadecimal viewer. """ from __future__ import division -import numpy import collections + +import numpy +import six + 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"] diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py index f7c479d..e5a2550 100644 --- a/silx/gui/data/NXdataWidgets.py +++ b/silx/gui/data/NXdataWidgets.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility +# Copyright (c) 2017-2019 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,19 +26,25 @@ """ __authors__ = ["P. Knobel"] __license__ = "MIT" -__date__ = "10/10/2018" +__date__ = "12/11/2018" +import logging +import numbers import numpy from silx.gui import qt from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector from silx.gui.plot import Plot1D, Plot2D, StackView, ScatterView +from silx.gui.plot.ComplexImageView import ComplexImageView from silx.gui.colors import Colormap from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser from silx.math.calibration import ArrayCalibration, NoCalibration, LinearCalibration +_logger = logging.getLogger(__name__) + + class ArrayCurvePlot(qt.QWidget): """ Widget for plotting a curve from a multi-dimensional signal array @@ -72,21 +78,16 @@ class ArrayCurvePlot(qt.QWidget): self._plot = Plot1D(self) - self.selectorDock = qt.QDockWidget("Data selector", self._plot) - # not closable - self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable | - qt.QDockWidget.DockWidgetFloatable) - self._selector = NumpyAxesSelector(self.selectorDock) + self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self.__selector_is_connected = False - self.selectorDock.setWidget(self._selector) - self._plot.addTabbedDockWidget(self.selectorDock) self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend) - layout = qt.QGridLayout() + layout = qt.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._plot, 0, 0) + layout.addWidget(self._plot) + layout.addWidget(self._selector) self.setLayout(layout) @@ -130,9 +131,9 @@ class ArrayCurvePlot(qt.QWidget): self._selector.setAxisNames(["Y"]) if len(ys[0].shape) < 2: - self.selectorDock.hide() + self._selector.hide() else: - self.selectorDock.show() + self._selector.show() self._plot.setGraphTitle(title or "") self._updateCurve() @@ -182,6 +183,9 @@ class ArrayCurvePlot(qt.QWidget): break def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) self._plot.clear() @@ -339,11 +343,8 @@ class ArrayImagePlot(qt.QWidget): normalization=Colormap.LINEAR)) self._plot.getIntensityHistogramAction().setVisible(True) - self.selectorDock = qt.QDockWidget("Data selector", self._plot) # not closable - self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable | - qt.QDockWidget.DockWidgetFloatable) - self._selector = NumpyAxesSelector(self.selectorDock) + self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) self._selector.selectionChanged.connect(self._updateImage) @@ -355,9 +356,8 @@ class ArrayImagePlot(qt.QWidget): layout = qt.QVBoxLayout() layout.addWidget(self._plot) + layout.addWidget(self._selector) layout.addWidget(self._auxSigSlider) - self.selectorDock.setWidget(self._selector) - self._plot.addTabbedDockWidget(self.selectorDock) self.setLayout(layout) @@ -413,9 +413,9 @@ class ArrayImagePlot(qt.QWidget): self._selector.setData(signals[0]) if len(signals[0].shape) <= img_ndim: - self.selectorDock.hide() + self._selector.hide() else: - self.selectorDock.show() + self._selector.show() self._auxSigSlider.setMaximum(len(signals) - 1) if len(signals) > 1: @@ -425,6 +425,7 @@ class ArrayImagePlot(qt.QWidget): self._auxSigSlider.setValue(0) self._updateImage() + self._plot.resetZoom() self._selector.selectionChanged.connect(self._updateImage) self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) @@ -492,12 +493,202 @@ class ArrayImagePlot(qt.QWidget): self._plot.setGraphTitle(title) self._plot.getXAxis().setLabel(self.__x_axis_name) self._plot.getYAxis().setLabel(self.__y_axis_name) - self._plot.resetZoom() def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) self._plot.clear() +class ArrayComplexImagePlot(qt.QWidget): + """ + Widget for plotting an image of complex from a multi-dimensional signal array + and two 1D axes array. + + The signal array can have an arbitrary number of dimensions, the only + limitation being that the last two dimensions must have the same length as + the axes arrays. + + Sliders are provided to select indices on the first (n - 2) dimensions of + the signal array, and the plot is updated to show the image corresponding + to the selection. + + If one or both of the axes does not have regularly spaced values, the + the image is plotted as a coloured scatter plot. + """ + def __init__(self, parent=None, colormap=None): + """ + + :param parent: Parent QWidget + """ + super(ArrayComplexImagePlot, self).__init__(parent) + + self.__signals = None + self.__signals_names = None + self.__x_axis = None + self.__x_axis_name = None + self.__y_axis = None + self.__y_axis_name = None + + self._plot = ComplexImageView(self) + if colormap is not None: + for mode in (ComplexImageView.Mode.ABSOLUTE, + ComplexImageView.Mode.SQUARE_AMPLITUDE, + ComplexImageView.Mode.REAL, + ComplexImageView.Mode.IMAGINARY): + self._plot.setColormap(colormap, mode) + + self._plot.getPlot().getIntensityHistogramAction().setVisible(True) + self._plot.setKeepDataAspectRatio(True) + + # not closable + self._selector = NumpyAxesSelector(self) + self._selector.setNamedAxesSelectorVisibility(False) + self._selector.selectionChanged.connect(self._updateImage) + + self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) + self._auxSigSlider.setMinimum(0) + self._auxSigSlider.setValue(0) + self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) + self._auxSigSlider.setToolTip("Select auxiliary signals") + + layout = qt.QVBoxLayout() + layout.addWidget(self._plot) + layout.addWidget(self._selector) + layout.addWidget(self._auxSigSlider) + + self.setLayout(layout) + + def _sliderIdxChanged(self, value): + self._updateImage() + + def getPlot(self): + """Returns the plot used for the display + + :rtype: PlotWidget + """ + return self._plot.getPlot() + + def setImageData(self, signals, + x_axis=None, y_axis=None, + signals_names=None, + xlabel=None, ylabel=None, + title=None): + """ + + :param signals: list of n-D datasets, whose last 2 dimensions are used as the + image's values, or list of 3D datasets interpreted as RGBA image. + :param x_axis: 1-D dataset used as the image's x coordinates. If + provided, its lengths must be equal to the length of the last + dimension of ``signal``. + :param y_axis: 1-D dataset used as the image's y. If provided, + its lengths must be equal to the length of the 2nd to last + dimension of ``signal``. + :param signals_names: Names for each image, used as subtitle and legend. + :param xlabel: Label for X axis + :param ylabel: Label for Y axis + :param title: Graph title + """ + self._selector.selectionChanged.disconnect(self._updateImage) + self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) + + self.__signals = signals + self.__signals_names = signals_names + self.__x_axis = x_axis + self.__x_axis_name = xlabel + self.__y_axis = y_axis + self.__y_axis_name = ylabel + self.__title = title + + self._selector.clear() + self._selector.setAxisNames(["Y", "X"]) + self._selector.setData(signals[0]) + + if len(signals[0].shape) <= 2: + self._selector.hide() + else: + self._selector.show() + + self._auxSigSlider.setMaximum(len(signals) - 1) + if len(signals) > 1: + self._auxSigSlider.show() + else: + self._auxSigSlider.hide() + self._auxSigSlider.setValue(0) + + self._updateImage() + self._plot.getPlot().resetZoom() + + self._selector.selectionChanged.connect(self._updateImage) + self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) + + def _updateImage(self): + selection = self._selector.selection() + auxSigIdx = self._auxSigSlider.value() + + images = [img[selection] for img in self.__signals] + image = images[auxSigIdx] + + x_axis = self.__x_axis + y_axis = self.__y_axis + + if x_axis is None and y_axis is None: + xcalib = NoCalibration() + ycalib = NoCalibration() + else: + if x_axis is None: + # no calibration + x_axis = numpy.arange(image.shape[1]) + elif numpy.isscalar(x_axis) or len(x_axis) == 1: + # constant axis + x_axis = x_axis * numpy.ones((image.shape[1], )) + elif len(x_axis) == 2: + # linear calibration + x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] + + if y_axis is None: + y_axis = numpy.arange(image.shape[0]) + elif numpy.isscalar(y_axis) or len(y_axis) == 1: + y_axis = y_axis * numpy.ones((image.shape[0], )) + elif len(y_axis) == 2: + y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] + + xcalib = ArrayCalibration(x_axis) + ycalib = ArrayCalibration(y_axis) + + self._plot.setData(image) + if xcalib.is_affine(): + xorigin, xscale = xcalib(0), xcalib.get_slope() + else: + _logger.warning("Unsupported complex image X axis calibration") + xorigin, xscale = 0., 1. + + if ycalib.is_affine(): + yorigin, yscale = ycalib(0), ycalib.get_slope() + else: + _logger.warning("Unsupported complex image Y axis calibration") + yorigin, yscale = 0., 1. + + self._plot.setOrigin((xorigin, yorigin)) + self._plot.setScale((xscale, yscale)) + + title = "" + if self.__title: + title += self.__title + if not title.strip().endswith(self.__signals_names[auxSigIdx]): + title += "\n" + self.__signals_names[auxSigIdx] + self._plot.setGraphTitle(title) + self._plot.getXAxis().setLabel(self.__x_axis_name) + self._plot.getYAxis().setLabel(self.__y_axis_name) + + def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) + self._plot.setData(None) + + class ArrayStackPlot(qt.QWidget): """ Widget for plotting a n-D array (n >= 3) as a stack of images. @@ -665,4 +856,208 @@ class ArrayStackPlot(qt.QWidget): self.__x_axis_name]) def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) self._stack_view.clear() + + +class ArrayVolumePlot(qt.QWidget): + """ + Widget for plotting a n-D array (n >= 3) as a 3D scalar field. + Three axis arrays can be provided to calibrate the axes. + + The signal array can have an arbitrary number of dimensions, the only + limitation being that the last 3 dimensions must have the same length as + the axes arrays. + + Sliders are provided to select indices on the first (n - 3) dimensions of + the signal array, and the plot is updated to load the stack corresponding + to the selection. + """ + def __init__(self, parent=None): + """ + + :param parent: Parent QWidget + """ + super(ArrayVolumePlot, self).__init__(parent) + + self.__signal = None + self.__signal_name = None + # the Z, Y, X axes apply to the last three dimensions of the signal + # (in that order) + self.__z_axis = None + self.__z_axis_name = None + self.__y_axis = None + self.__y_axis_name = None + self.__x_axis = None + self.__x_axis_name = None + + from silx.gui.plot3d.ScalarFieldView import ScalarFieldView + from silx.gui.plot3d import SFViewParamTree + + self._view = ScalarFieldView(self) + + def computeIsolevel(data): + data = data[numpy.isfinite(data)] + if len(data) == 0: + return 0 + else: + return numpy.mean(data) + numpy.std(data) + + self._view.addIsosurface(computeIsolevel, '#FF0000FF') + + # Create a parameter tree for the scalar field view + options = SFViewParamTree.TreeView(self._view) + options.setSfView(self._view) + + # Add the parameter tree to the main window in a dock widget + dock = qt.QDockWidget() + dock.setWidget(options) + self._view.addDockWidget(qt.Qt.RightDockWidgetArea, dock) + + self._hline = qt.QFrame(self) + self._hline.setFrameStyle(qt.QFrame.HLine) + self._hline.setFrameShadow(qt.QFrame.Sunken) + self._legend = qt.QLabel(self) + self._selector = NumpyAxesSelector(self) + self._selector.setNamedAxesSelectorVisibility(False) + self.__selector_is_connected = False + + layout = qt.QVBoxLayout() + layout.addWidget(self._view) + layout.addWidget(self._hline) + layout.addWidget(self._legend) + layout.addWidget(self._selector) + + self.setLayout(layout) + + def getVolumeView(self): + """Returns the plot used for the display + + :rtype: ScalarFieldView + """ + return self._view + + def normalizeComplexData(self, data): + """ + Converts a complex data array to its amplitude, if necessary. + :param data: the data to normalize + :return: + """ + if hasattr(data, "dtype"): + isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) + else: + isComplex = isinstance(data, numbers.Complex) + if isComplex: + data = numpy.absolute(data) + return data + + def setData(self, signal, + x_axis=None, y_axis=None, z_axis=None, + signal_name=None, + xlabel=None, ylabel=None, zlabel=None, + title=None): + """ + + :param signal: n-D dataset, whose last 3 dimensions are used as the + 3D stack values. + :param x_axis: 1-D dataset used as the image's x coordinates. If + provided, its lengths must be equal to the length of the last + dimension of ``signal``. + :param y_axis: 1-D dataset used as the image's y. If provided, + its lengths must be equal to the length of the 2nd to last + dimension of ``signal``. + :param z_axis: 1-D dataset used as the image's z. If provided, + its lengths must be equal to the length of the 3rd to last + dimension of ``signal``. + :param signal_name: Label used in the legend + :param xlabel: Label for X axis + :param ylabel: Label for Y axis + :param zlabel: Label for Z axis + :param title: Graph title + """ + signal = self.normalizeComplexData(signal) + if self.__selector_is_connected: + self._selector.selectionChanged.disconnect(self._updateVolume) + self.__selector_is_connected = False + + self.__signal = signal + self.__signal_name = signal_name or "" + self.__x_axis = x_axis + self.__x_axis_name = xlabel + self.__y_axis = y_axis + self.__y_axis_name = ylabel + self.__z_axis = z_axis + self.__z_axis_name = zlabel + + self._selector.setData(signal) + self._selector.setAxisNames(["Y", "X", "Z"]) + + self._view.setAxesLabels(self.__x_axis_name or 'X', + self.__y_axis_name or 'Y', + self.__z_axis_name or 'Z') + self._updateVolume() + + # the legend label shows the selection slice producing the volume + # (only interesting for ndim > 3) + if signal.ndim > 3: + self._selector.setVisible(True) + self._legend.setVisible(True) + self._hline.setVisible(True) + else: + self._selector.setVisible(False) + self._legend.setVisible(False) + self._hline.setVisible(False) + + if not self.__selector_is_connected: + self._selector.selectionChanged.connect(self._updateVolume) + self.__selector_is_connected = True + + def _updateVolume(self): + """Update displayed stack according to the current axes selector + data.""" + data = self._selector.selectedData() + x_axis = self.__x_axis + y_axis = self.__y_axis + z_axis = self.__z_axis + + offset = [] + scale = [] + for axis in [x_axis, y_axis, z_axis]: + if axis is None: + calibration = NoCalibration() + elif len(axis) == 2: + calibration = LinearCalibration( + y_intercept=axis[0], slope=axis[1]) + else: + calibration = ArrayCalibration(axis) + if not calibration.is_affine(): + _logger.warning("Axis has not linear values, ignored") + offset.append(0.) + scale.append(1.) + else: + offset.append(calibration(0)) + scale.append(calibration.get_slope()) + + legend = self.__signal_name + "[" + for sl in self._selector.selection(): + if sl == slice(None): + legend += ":, " + else: + legend += str(sl) + ", " + legend = legend[:-2] + "]" + self._legend.setText("Displayed data: " + legend) + + self._view.setData(data, copy=False) + self._view.setScale(*scale) + self._view.setTranslation(*offset) + self._view.setAxesLabels(self.__x_axis_name, + self.__y_axis_name, + self.__z_axis_name) + + def clear(self): + old = self._selector.blockSignals(True) + self._selector.clear() + self._selector.blockSignals(old) + self._view.setData(None) diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py index 1401634..98c37d7 100644 --- a/silx/gui/data/TextFormatter.py +++ b/silx/gui/data/TextFormatter.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017 European Synchrotron Radiation Facility +# Copyright (c) 2017-2018 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 @@ -29,16 +29,15 @@ __authors__ = ["V. Valls"] __license__ = "MIT" __date__ = "24/07/2018" -import numpy +import logging import numbers -from silx.third_party import six + +import numpy +import six + from silx.gui import qt -import logging -try: - import h5py -except ImportError: - h5py = None +import h5py _logger = logging.getLogger(__name__) @@ -322,10 +321,9 @@ class TextFormatter(qt.QObject): if dtype.kind == 'S': return self.__formatCharString(data) elif dtype.kind == 'O': - if h5py is not None: - text = self.__formatH5pyObject(data, dtype) - if text is not None: - return text + text = self.__formatH5pyObject(data, dtype) + if text is not None: + return text try: # Try ascii/utf-8 text = "%s" % data.decode("utf-8") @@ -339,15 +337,14 @@ class TextFormatter(qt.QObject): 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 + 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 @@ -373,21 +370,20 @@ class TextFormatter(qt.QObject): template = self.__floatFormat params = (data.real) return template % params - elif h5py is not None and isinstance(data, h5py.h5r.Reference): + elif 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): + elif 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 + 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_arraywidget.py b/silx/gui/data/test/test_arraywidget.py index 50ffc84..6bcbbd3 100644 --- a/silx/gui/data/test/test_arraywidget.py +++ b/silx/gui/data/test/test_arraywidget.py @@ -36,10 +36,7 @@ from silx.gui import qt from silx.gui.data import ArrayTableWidget from silx.gui.utils.testutils import TestCaseQt -try: - import h5py -except ImportError: - h5py = None +import h5py class TestArrayWidget(TestCaseQt): @@ -190,7 +187,6 @@ class TestArrayWidget(TestCaseQt): self.assertIs(b0, b1) -@unittest.skipIf(h5py is None, "Could not import h5py") class TestH5pyArrayWidget(TestCaseQt): """Basic test for ArrayTableWidget with a dataset. diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py index a681f33..dc6fee8 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__ = "23/04/2018" +__date__ = "19/02/2019" import os import tempfile @@ -42,10 +42,7 @@ from silx.gui.data.DataViewerFrame import DataViewerFrame from silx.gui.utils.testutils import SignalListener from silx.gui.utils.testutils import TestCaseQt -try: - import h5py -except ImportError: - h5py = None +import h5py class _DataViewMock(DataView): @@ -170,8 +167,6 @@ class AbstractDataViewerTests(TestCaseQt): self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) def test_3d_h5_dataset(self): - if h5py is None: - self.skipTest("h5py library is not available") with self.h5_temporary_file() as h5file: dataset = h5file["data"] widget = self.create_widget() @@ -242,8 +237,9 @@ class AbstractDataViewerTests(TestCaseQt): # replace a view that is a child of a composite view widget = self.create_widget() view = _DataViewMock(widget) - widget.replaceView(DataViews.NXDATA_INVALID_MODE, - view) + replaced = widget.replaceView(DataViews.NXDATA_INVALID_MODE, + view) + self.assertTrue(replaced) nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE) self.assertNotIn(DataViews.NXDATA_INVALID_MODE, [v.modeId() for v in nxdata_view.availableViews()]) diff --git a/silx/gui/data/test/test_numpyaxesselector.py b/silx/gui/data/test/test_numpyaxesselector.py index 6b7b58c..df11c1a 100644 --- a/silx/gui/data/test/test_numpyaxesselector.py +++ b/silx/gui/data/test/test_numpyaxesselector.py @@ -37,10 +37,7 @@ from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector from silx.gui.utils.testutils import SignalListener from silx.gui.utils.testutils import TestCaseQt -try: - import h5py -except ImportError: - h5py = None +import h5py class TestNumpyAxesSelector(TestCaseQt): @@ -121,8 +118,6 @@ class TestNumpyAxesSelector(TestCaseQt): os.unlink(tmp_name) def test_h5py_dataset(self): - if h5py is None: - self.skipTest("h5py library is not available") with self.h5_temporary_file() as h5file: dataset = h5file["data"] expectedResult = dataset[0] diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py index 850aa00..935344a 100644 --- a/silx/gui/data/test/test_textformatter.py +++ b/silx/gui/data/test/test_textformatter.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 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 @@ -29,17 +29,15 @@ __date__ = "12/12/2017" import unittest import shutil import tempfile + import numpy +import six from silx.gui.utils.testutils import TestCaseQt from silx.gui.utils.testutils import SignalListener from ..TextFormatter import TextFormatter -from silx.third_party import six -try: - import h5py -except ImportError: - h5py = None +import h5py class TestTextFormatter(TestCaseQt): @@ -108,8 +106,6 @@ 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") -- cgit v1.2.3