summaryrefslogtreecommitdiff
path: root/src/silx/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/app')
-rw-r--r--src/silx/app/__init__.py1
-rw-r--r--src/silx/app/compare/CompareImagesWindow.py254
-rw-r--r--src/silx/app/compare/__init__.py (renamed from src/silx/app/view/setup.py)19
-rw-r--r--src/silx/app/compare/main.py105
-rw-r--r--src/silx/app/compare/test/__init__.py23
-rw-r--r--src/silx/app/compare/test/test_compare.py (renamed from src/silx/app/view/utils.py)38
-rw-r--r--src/silx/app/compare/test/test_launcher.py142
-rw-r--r--src/silx/app/convert.py363
-rw-r--r--src/silx/app/test/__init__.py1
-rw-r--r--src/silx/app/test/test_convert.py19
-rw-r--r--src/silx/app/test_.py1
-rw-r--r--src/silx/app/utils/__init__.py (renamed from src/silx/app/setup.py)20
-rw-r--r--src/silx/app/utils/parseutils.py133
-rw-r--r--src/silx/app/utils/test/__init__.py23
-rw-r--r--src/silx/app/utils/test/test_parseutils.py68
-rw-r--r--src/silx/app/view/About.py33
-rw-r--r--src/silx/app/view/ApplicationContext.py40
-rw-r--r--src/silx/app/view/CustomNxdataWidget.py13
-rw-r--r--src/silx/app/view/DataPanel.py3
-rw-r--r--src/silx/app/view/Viewer.py201
-rw-r--r--src/silx/app/view/__init__.py1
-rw-r--r--src/silx/app/view/main.py78
-rw-r--r--src/silx/app/view/test/__init__.py1
-rw-r--r--src/silx/app/view/test/test_launcher.py14
-rw-r--r--src/silx/app/view/test/test_view.py23
25 files changed, 1249 insertions, 368 deletions
diff --git a/src/silx/app/__init__.py b/src/silx/app/__init__.py
index 3af680c..4c0bc00 100644
--- a/src/silx/app/__init__.py
+++ b/src/silx/app/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
diff --git a/src/silx/app/compare/CompareImagesWindow.py b/src/silx/app/compare/CompareImagesWindow.py
new file mode 100644
index 0000000..7a509ae
--- /dev/null
+++ b/src/silx/app/compare/CompareImagesWindow.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python
+# /*##########################################################################
+#
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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.
+#
+# ###########################################################################*/
+"""Main window used to compare images
+"""
+
+import logging
+import numpy
+import typing
+import os.path
+
+import silx.io
+from silx.gui import icons
+from silx.gui import qt
+from silx.gui.plot.CompareImages import CompareImages
+from silx.gui.widgets.UrlSelectionTable import UrlSelectionTable
+from ..utils import parseutils
+from silx.gui.plot.tools.profile.manager import ProfileManager
+from silx.gui.plot.tools.compare.profile import ProfileImageDirectedLineROI
+
+try:
+ import PIL
+except ImportError:
+ PIL = None
+
+
+_logger = logging.getLogger(__name__)
+
+
+def _get_image_from_file(urlPath: str) -> typing.Optional[numpy.ndarray]:
+ """Returns a dataset from an image file.
+
+ The returned layout shape is supposed to be `rows, columns, channels (rgb[a])`.
+ """
+ if PIL is None:
+ return None
+ return numpy.asarray(PIL.Image.open(urlPath))
+
+
+class CompareImagesWindow(qt.QMainWindow):
+ def __init__(self, backend=None, settings=None):
+ qt.QMainWindow.__init__(self, parent=None)
+ self.setWindowTitle("Silx compare")
+
+ silxIcon = icons.getQIcon("silx")
+ self.setWindowIcon(silxIcon)
+
+ self._plot = CompareImages(parent=self, backend=backend)
+ self._plot.setAutoResetZoom(False)
+
+ self.__manager = ProfileManager(self, self._plot.getPlot())
+ virtualItem = self._plot._getVirtualPlotItem()
+ self.__manager.setPlotItem(virtualItem)
+
+ directedLineAction = self.__manager.createProfileAction(
+ ProfileImageDirectedLineROI, self
+ )
+
+ profileToolBar = qt.QToolBar(self)
+ profileToolBar.setWindowTitle("Profile")
+ profileToolBar.addAction(directedLineAction)
+ self.__profileToolBar = profileToolBar
+ self._plot.addToolBar(profileToolBar)
+
+ self._selectionTable = UrlSelectionTable(parent=self)
+ self._selectionTable.setAcceptDrops(True)
+
+ self.__settings = settings
+ if settings:
+ self.restoreSettings(settings)
+
+ spliter = qt.QSplitter(self)
+ spliter.addWidget(self._selectionTable)
+ spliter.addWidget(self._plot)
+ spliter.setStretchFactor(1, 1)
+ spliter.setCollapsible(0, False)
+ spliter.setCollapsible(1, False)
+ self.__splitter = spliter
+
+ self.setCentralWidget(spliter)
+
+ self._selectionTable.sigImageAChanged.connect(self._updateImageA)
+ self._selectionTable.sigImageBChanged.connect(self._updateImageB)
+
+ def setUrls(self, urls):
+ self.clear()
+ for url in urls:
+ self._selectionTable.addUrl(url)
+ url1 = urls[0].path() if len(urls) >= 1 else None
+ url2 = urls[1].path() if len(urls) >= 2 else None
+ self._selectionTable.setUrlSelection(url_img_a=url1, url_img_b=url2)
+ self._plot.resetZoom()
+ self._plot.centerLines()
+
+ def clear(self):
+ self._plot.clear()
+ self._selectionTable.clear()
+
+ def _updateImageA(self, urlPath):
+ try:
+ data = self.readData(urlPath)
+ except Exception as e:
+ _logger.error("Error while loading URL %s", urlPath, exc_info=True)
+ self._selectionTable.setError(urlPath, e.args[0])
+ data = None
+ self._plot.setImage1(data)
+
+ def _updateImageB(self, urlPath):
+ try:
+ data = self.readData(urlPath)
+ except Exception as e:
+ _logger.error("Error while loading URL %s", urlPath, exc_info=True)
+ self._selectionTable.setError(urlPath, e.args[0])
+ data = None
+ self._plot.setImage2(data)
+
+ def readData(self, urlPath: str):
+ """Read an URL as an image"""
+ if urlPath in ("", None):
+ return None
+
+ data = None
+ _, ext = os.path.splitext(urlPath)
+ if ext in {".jpg", ".jpeg", ".png"}:
+ try:
+ data = _get_image_from_file(urlPath)
+ except Exception:
+ _logger.debug("Error while loading image with PIL", exc_info=True)
+
+ if data is None:
+ try:
+ data = silx.io.utils.get_data(urlPath)
+ except Exception:
+ raise ValueError(f"Data from '{urlPath}' is not readable")
+
+ if not isinstance(data, numpy.ndarray):
+ raise ValueError(f"URL '{urlPath}' does not link to a numpy array")
+ if data.dtype.kind not in set(["f", "u", "i", "b"]):
+ raise ValueError(f"URL '{urlPath}' does not link to a numeric numpy array")
+
+ if data.ndim == 2:
+ return data
+ if data.ndim == 3 and data.shape[2] in {3, 4}:
+ return data
+
+ raise ValueError(f"URL '{urlPath}' does not link to an numpy image")
+
+ def closeEvent(self, event):
+ settings = self.__settings
+ if settings:
+ self.saveSettings(self.__settings)
+
+ def saveSettings(self, settings):
+ """Save the window settings to this settings object
+
+ :param qt.QSettings settings: Initialized settings
+ """
+ isFullScreen = bool(self.windowState() & qt.Qt.WindowFullScreen)
+ if isFullScreen:
+ # show in normal to catch the normal geometry
+ self.showNormal()
+
+ settings.beginGroup("comparewindow")
+ settings.setValue("size", self.size())
+ settings.setValue("pos", self.pos())
+ settings.setValue("full-screen", isFullScreen)
+ settings.setValue("spliter", self.__splitter.sizes())
+ # NOTE: isInverted returns a numpy bool
+ settings.setValue(
+ "y-axis-inverted", bool(self._plot.getPlot().getYAxis().isInverted())
+ )
+
+ settings.setValue("visualization-mode", self._plot.getVisualizationMode().name)
+ settings.setValue("alignment-mode", self._plot.getAlignmentMode().name)
+ settings.setValue("display-keypoints", self._plot.getKeypointsVisible())
+
+ displayKeypoints = settings.value("display-keypoints", False)
+ displayKeypoints = parseutils.to_bool(displayKeypoints, False)
+
+ # self._plot.getAlignmentMode()
+ # self._plot.getVisualizationMode()
+ # self._plot.getKeypointsVisible()
+ settings.endGroup()
+
+ if isFullScreen:
+ self.showFullScreen()
+
+ def restoreSettings(self, settings):
+ """Restore the window settings using this settings object
+
+ :param qt.QSettings settings: Initialized settings
+ """
+ settings.beginGroup("comparewindow")
+ size = settings.value("size", qt.QSize(640, 480))
+ pos = settings.value("pos", qt.QPoint())
+ isFullScreen = settings.value("full-screen", False)
+ isFullScreen = parseutils.to_bool(isFullScreen, False)
+ yAxisInverted = settings.value("y-axis-inverted", False)
+ yAxisInverted = parseutils.to_bool(yAxisInverted, False)
+
+ visualizationMode = settings.value("visualization-mode", "")
+ visualizationMode = parseutils.to_enum(
+ visualizationMode,
+ CompareImages.VisualizationMode,
+ CompareImages.VisualizationMode.VERTICAL_LINE,
+ )
+ alignmentMode = settings.value("alignment-mode", "")
+ alignmentMode = parseutils.to_enum(
+ alignmentMode,
+ CompareImages.AlignmentMode,
+ CompareImages.AlignmentMode.ORIGIN,
+ )
+ displayKeypoints = settings.value("display-keypoints", False)
+ displayKeypoints = parseutils.to_bool(displayKeypoints, False)
+
+ try:
+ data = settings.value("spliter")
+ data = [int(d) for d in data]
+ self.__splitter.setSizes(data)
+ except Exception:
+ _logger.debug("Backtrace", exc_info=True)
+ settings.endGroup()
+
+ if not pos.isNull():
+ self.move(pos)
+ if not size.isNull():
+ self.resize(size)
+ if isFullScreen:
+ self.showFullScreen()
+ self._plot.setVisualizationMode(visualizationMode)
+ self._plot.setAlignmentMode(alignmentMode)
+ self._plot.setKeypointsVisible(displayKeypoints)
+ self._plot.getPlot().getYAxis().setInverted(yAxisInverted)
diff --git a/src/silx/app/view/setup.py b/src/silx/app/compare/__init__.py
index fa076cb..e5ec4c6 100644
--- a/src/silx/app/view/setup.py
+++ b/src/silx/app/compare/__init__.py
@@ -1,6 +1,5 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016 European Synchrotron Radiation Facility
+# Copyright (C) 2022-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -21,20 +20,8 @@
# THE SOFTWARE.
#
# ############################################################################*/
+"""Package containing source code of the `silx compare` application"""
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "06/06/2018"
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('view', parent_package, top_path)
- config.add_subpackage('test')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
+__date__ = "04/13/2023"
diff --git a/src/silx/app/compare/main.py b/src/silx/app/compare/main.py
new file mode 100644
index 0000000..79c33f1
--- /dev/null
+++ b/src/silx/app/compare/main.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# /*##########################################################################
+#
+# Copyright (c) 2016-2021 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.
+#
+# ###########################################################################*/
+"""GUI to compare images"""
+
+import sys
+import logging
+import argparse
+import silx
+from silx.gui import qt
+from silx.app.utils import parseutils
+from silx.app.compare.CompareImagesWindow import CompareImagesWindow
+
+_logger = logging.getLogger(__name__)
+
+
+file_description = """
+Image data to compare (HDF5 file with path, EDF files, JPEG/PNG image files).
+Data from HDF5 files can be accessed using dataset path and slicing as an URL: silx:../my_file.h5?path=/entry/data&slice=10
+EDF file frames also can can be accessed using URL: fabio:../my_file.edf?slice=10
+Using URL in command like usually have to be quoted: "URL".
+"""
+
+
+def createParser():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("files", nargs=argparse.ZERO_OR_MORE, help=file_description)
+ parser.add_argument(
+ "--debug",
+ dest="debug",
+ action="store_true",
+ default=False,
+ help="Set logging system in debug mode",
+ )
+ parser.add_argument(
+ "--use-opengl-plot",
+ dest="use_opengl_plot",
+ action="store_true",
+ default=False,
+ help="Use OpenGL for plots (instead of matplotlib)",
+ )
+ return parser
+
+
+def mainQt(options):
+ """Part of the main depending on Qt"""
+ if options.debug:
+ logging.root.setLevel(logging.DEBUG)
+
+ if options.use_opengl_plot:
+ backend = "gl"
+ else:
+ backend = "mpl"
+
+ settings = qt.QSettings(
+ qt.QSettings.IniFormat, qt.QSettings.UserScope, "silx", "silx-compare", None
+ )
+
+ urls = list(parseutils.filenames_to_dataurls(options.files))
+
+ if options.use_opengl_plot:
+ # It have to be done after the settings (after the Viewer creation)
+ silx.config.DEFAULT_PLOT_BACKEND = "opengl"
+
+ app = qt.QApplication([])
+ window = CompareImagesWindow(backend=backend, settings=settings)
+ window.setAttribute(qt.Qt.WA_DeleteOnClose, True)
+
+ # Note: Have to be before setUrls to have a proper resetZoom
+ window.setVisible(True)
+
+ window.setUrls(urls)
+
+ app.exec()
+
+
+def main(argv):
+ parser = createParser()
+ options = parser.parse_args(argv[1:])
+ mainQt(options)
+
+
+if __name__ == "__main__":
+ main(sys.argv)
diff --git a/src/silx/app/compare/test/__init__.py b/src/silx/app/compare/test/__init__.py
new file mode 100644
index 0000000..1d8207b
--- /dev/null
+++ b/src/silx/app/compare/test/__init__.py
@@ -0,0 +1,23 @@
+# /*##########################################################################
+#
+# Copyright (c) 2016-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.
+#
+# ###########################################################################*/
diff --git a/src/silx/app/view/utils.py b/src/silx/app/compare/test/test_compare.py
index 80167c8..45c6838 100644
--- a/src/silx/app/view/utils.py
+++ b/src/silx/app/compare/test/test_compare.py
@@ -1,6 +1,6 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2018 European Synchrotron Radiation Facility
+#
+# Copyright (c) 2016-2020 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
@@ -20,26 +20,30 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
-# ############################################################################*/
-"""Browse a data file with a GUI"""
+# ###########################################################################*/
+"""Module testing silx.app.view"""
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "28/05/2018"
+__date__ = "07/06/2023"
+
+import weakref
+import pytest
+from silx.app.compare.CompareImagesWindow import CompareImagesWindow
+from silx.gui.utils.testutils import TestCaseQt
-_trueStrings = set(["yes", "true", "1"])
-_falseStrings = set(["no", "false", "0"])
+@pytest.mark.usefixtures("qapp")
+class TestCompare(TestCaseQt):
+ """Test for Viewer class"""
-def stringToBool(string):
- """Returns a boolean from a string.
+ def testConstruct(self):
+ widget = CompareImagesWindow()
+ self.qWaitForWindowExposed(widget)
- :raise ValueError: If the string do not contains a boolean information.
- """
- lower = string.lower()
- if lower in _trueStrings:
- return True
- if lower in _falseStrings:
- return False
- raise ValueError("'%s' is not a valid boolean" % string)
+ def testDestroy(self):
+ widget = CompareImagesWindow()
+ ref = weakref.ref(widget)
+ widget = None
+ self.qWaitForDestroy(ref)
diff --git a/src/silx/app/compare/test/test_launcher.py b/src/silx/app/compare/test/test_launcher.py
new file mode 100644
index 0000000..a42b762
--- /dev/null
+++ b/src/silx/app/compare/test/test_launcher.py
@@ -0,0 +1,142 @@
+# /*##########################################################################
+#
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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.
+#
+# ###########################################################################*/
+"""Module testing silx.app.view"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "07/06/2023"
+
+
+import os
+import sys
+import shutil
+import logging
+import subprocess
+import pytest
+
+from .. import main
+from silx import __main__ as silx_main
+
+_logger = logging.getLogger(__name__)
+
+
+def test_help(qapp):
+ # option -h must cause a raise SystemExit or a return 0
+ try:
+ parser = main.createParser()
+ parser.parse_args(["compare", "--help"])
+ result = 0
+ except SystemExit as e:
+ result = e.args[0]
+ assert result == 0
+
+
+def test_wrong_option(qapp):
+ try:
+ parser = main.createParser()
+ parser.parse_args(["compare", "--foo"])
+ assert False
+ except SystemExit as e:
+ result = e.args[0]
+ assert result != 0
+
+
+def test_wrong_file(qapp):
+ try:
+ parser = main.createParser()
+ result = parser.parse_args(["compare", "__file.not.found__"])
+ result = 0
+ except SystemExit as e:
+ result = e.args[0]
+ assert result == 0
+
+
+def _create_test_env():
+ """
+ Returns an associated environment with a working project.
+ """
+ env = dict((str(k), str(v)) for k, v in os.environ.items())
+ env["PYTHONPATH"] = os.pathsep.join(sys.path)
+ return env
+
+
+@pytest.fixture
+def execute_as_script(tmp_path):
+ """Execute a command line.
+
+ Log output as debug in case of bad return code.
+ """
+
+ def execute_as_script(filename, *args):
+ env = _create_test_env()
+
+ # Copy file to temporary dir to avoid import from current dir.
+ script = os.path.join(tmp_path, "launcher.py")
+ shutil.copyfile(filename, script)
+ command_line = [sys.executable, script] + list(args)
+
+ _logger.info("Execute: %s", " ".join(command_line))
+ p = subprocess.Popen(
+ command_line, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
+ )
+ out, err = p.communicate()
+ _logger.info("Return code: %d", p.returncode)
+ try:
+ out = out.decode("utf-8")
+ except UnicodeError:
+ pass
+ try:
+ err = err.decode("utf-8")
+ except UnicodeError:
+ pass
+
+ if p.returncode != 0:
+ _logger.error("stdout:")
+ _logger.error("%s", out)
+ _logger.error("stderr:")
+ _logger.error("%s", err)
+ else:
+ _logger.debug("stdout:")
+ _logger.debug("%s", out)
+ _logger.debug("stderr:")
+ _logger.debug("%s", err)
+ assert p.returncode == 0
+
+ return execute_as_script
+
+
+def test_execute_compare_help(qapp, execute_as_script):
+ """Test if the main module is well connected.
+
+ Uses subprocess to avoid to parasite the current environment.
+ """
+ execute_as_script(main.__file__, "--help")
+
+
+def test_execute_silx_compare_help(qapp, execute_as_script):
+ """Test if the main module is well connected.
+
+ Uses subprocess to avoid to parasite the current environment.
+ """
+ execute_as_script(silx_main.__file__, "view", "--help")
diff --git a/src/silx/app/convert.py b/src/silx/app/convert.py
index 43baf7e..e20a448 100644
--- a/src/silx/app/convert.py
+++ b/src/silx/app/convert.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
# Copyright (C) 2017-2021 European Synchrotron Radiation Facility
#
@@ -86,8 +85,10 @@ def drop_indices_before_begin(filenames, regex, begin):
m = re.match(regex, fname)
file_indices = list(map(int, m.groups()))
if len(file_indices) != len(begin_indices):
- raise IOError("Number of indices found in filename "
- "does not match number of parsed end indices.")
+ raise IOError(
+ "Number of indices found in filename "
+ "does not match number of parsed end indices."
+ )
good_indices = True
for i, fidx in enumerate(file_indices):
if fidx < begin_indices[i]:
@@ -111,8 +112,10 @@ def drop_indices_after_end(filenames, regex, end):
m = re.match(regex, fname)
file_indices = list(map(int, m.groups()))
if len(file_indices) != len(end_indices):
- raise IOError("Number of indices found in filename "
- "does not match number of parsed end indices.")
+ raise IOError(
+ "Number of indices found in filename "
+ "does not match number of parsed end indices."
+ )
good_indices = True
for i, fidx in enumerate(file_indices):
if fidx > end_indices[i]:
@@ -134,15 +137,17 @@ def are_files_missing_in_series(filenames, regex):
previous_indices = None
for fname in filenames:
m = re.match(regex, fname)
- assert m is not None, \
- "regex %s does not match filename %s" % (fname, regex)
+ assert m is not None, "regex %s does not match filename %s" % (fname, regex)
new_indices = list(map(int, m.groups()))
if previous_indices is not None:
for old_idx, new_idx in zip(previous_indices, new_indices):
if (new_idx - old_idx) > 1:
- _logger.error("Index increment > 1 in file series: "
- "previous idx %d, next idx %d",
- old_idx, new_idx)
+ _logger.error(
+ "Index increment > 1 in file series: "
+ "previous idx %d, next idx %d",
+ old_idx,
+ new_idx,
+ )
return True
previous_indices = new_indices
return False
@@ -197,116 +202,134 @@ def main(argv):
"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
- 'input_files',
+ "input_files",
nargs="*",
- help='Input files (EDF, TIFF, FIO, SPEC...). When specifying '
- 'multiple files, you cannot specify both fabio images '
- 'and SPEC (or FIO) files. Multiple SPEC or FIO files will '
- 'simply be concatenated, with one entry per scan. '
- 'Multiple image files will be merged into a single '
- 'entry with a stack of images.')
+ help="Input files (EDF, TIFF, FIO, SPEC...). When specifying "
+ "multiple files, you cannot specify both fabio images "
+ "and SPEC (or FIO) files. Multiple SPEC or FIO files will "
+ "simply be concatenated, with one entry per scan. "
+ "Multiple image files will be merged into a single "
+ "entry with a stack of images.",
+ )
# input_files and --filepattern are mutually exclusive
parser.add_argument(
- '--file-pattern',
- help='File name pattern for loading a series of indexed image files '
- '(toto_%%04d.edf). This argument is incompatible with argument '
- 'input_files. If an output URI with a HDF5 path is provided, '
- 'only the content of the NXdetector group will be copied there. '
- 'If no HDF5 path, or just "/", is given, a complete NXdata '
- 'structure will be created.')
+ "--file-pattern",
+ help="File name pattern for loading a series of indexed image files "
+ "(toto_%%04d.edf). This argument is incompatible with argument "
+ "input_files. If an output URI with a HDF5 path is provided, "
+ "only the content of the NXdetector group will be copied there. "
+ 'If no HDF5 path, or just "/", is given, a complete NXdata '
+ "structure will be created.",
+ )
parser.add_argument(
- '-o', '--output-uri',
- default=time.strftime("%Y%m%d-%H%M%S") + '.h5',
- help='Output file name (HDF5). An URI can be provided to write'
- ' the data into a specific group in the output file: '
- '/path/to/file::/path/to/group. '
- 'If not provided, the filename defaults to a timestamp:'
- ' YYYYmmdd-HHMMSS.h5')
+ "-o",
+ "--output-uri",
+ default=time.strftime("%Y%m%d-%H%M%S") + ".h5",
+ help="Output file name (HDF5). An URI can be provided to write"
+ " the data into a specific group in the output file: "
+ "/path/to/file::/path/to/group. "
+ "If not provided, the filename defaults to a timestamp:"
+ " YYYYmmdd-HHMMSS.h5",
+ )
parser.add_argument(
- '-m', '--mode',
+ "-m",
+ "--mode",
default="w-",
help='Write mode: "r+" (read/write, file must exist), '
- '"w" (write, existing file is lost), '
- '"w-" (write, fail if file exists) or '
- '"a" (read/write if exists, create otherwise)')
+ '"w" (write, existing file is lost), '
+ '"w-" (write, fail if file exists) or '
+ '"a" (read/write if exists, create otherwise)',
+ )
parser.add_argument(
- '--begin',
- help='First file index, or first file indices to be considered. '
- 'This argument only makes sense when used together with '
- '--file-pattern. Provide as many start indices as there '
- 'are indices in the file pattern, separated by commas. '
- 'Examples: "--filepattern toto_%%d.edf --begin 100", '
- ' "--filepattern toto_%%d_%%04d_%%02d.edf --begin 100,2000,5".')
+ "--begin",
+ help="First file index, or first file indices to be considered. "
+ "This argument only makes sense when used together with "
+ "--file-pattern. Provide as many start indices as there "
+ "are indices in the file pattern, separated by commas. "
+ 'Examples: "--filepattern toto_%%d.edf --begin 100", '
+ ' "--filepattern toto_%%d_%%04d_%%02d.edf --begin 100,2000,5".',
+ )
parser.add_argument(
- '--end',
- help='Last file index, or last file indices to be considered. '
- 'The same rules as with argument --begin apply. '
- 'Example: "--filepattern toto_%%d_%%d.edf --end 199,1999"')
+ "--end",
+ help="Last file index, or last file indices to be considered. "
+ "The same rules as with argument --begin apply. "
+ 'Example: "--filepattern toto_%%d_%%d.edf --end 199,1999"',
+ )
parser.add_argument(
- '--add-root-group',
+ "--add-root-group",
action="store_true",
- help='This option causes each input file to be written to a '
- 'specific root group with the same name as the file. When '
- 'merging multiple input files, this can help preventing conflicts'
- ' when datasets have the same name (see --overwrite-data). '
- 'This option is ignored when using --file-pattern.')
+ help="This option causes each input file to be written to a "
+ "specific root group with the same name as the file. When "
+ "merging multiple input files, this can help preventing conflicts"
+ " when datasets have the same name (see --overwrite-data). "
+ "This option is ignored when using --file-pattern.",
+ )
parser.add_argument(
- '--overwrite-data',
+ "--overwrite-data",
action="store_true",
- help='If the output path exists and an input dataset has the same'
- ' name as an existing output dataset, overwrite the output '
- 'dataset (in modes "r+" or "a").')
+ help="If the output path exists and an input dataset has the same"
+ " name as an existing output dataset, overwrite the output "
+ 'dataset (in modes "r+" or "a").',
+ )
parser.add_argument(
- '--min-size',
+ "--min-size",
type=int,
default=500,
- help='Minimum number of elements required to be in a dataset to '
- 'apply compression or chunking (default 500).')
+ help="Minimum number of elements required to be in a dataset to "
+ "apply compression or chunking (default 500).",
+ )
parser.add_argument(
- '--chunks',
+ "--chunks",
nargs="?",
const="auto",
- help='Chunk shape. Provide an argument that evaluates as a python '
- 'tuple (e.g. "(1024, 768)"). If this option is provided without '
- 'specifying an argument, the h5py library will guess a chunk for '
- 'you. Note that if you specify an explicit chunking shape, it '
- 'will be applied identically to all datasets with a large enough '
- 'size (see --min-size). ')
+ help="Chunk shape. Provide an argument that evaluates as a python "
+ 'tuple (e.g. "(1024, 768)"). If this option is provided without '
+ "specifying an argument, the h5py library will guess a chunk for "
+ "you. Note that if you specify an explicit chunking shape, it "
+ "will be applied identically to all datasets with a large enough "
+ "size (see --min-size). ",
+ )
parser.add_argument(
- '--compression',
+ "--compression",
nargs="?",
const="gzip",
- help='Compression filter. By default, the datasets in the output '
- 'file are not compressed. If this option is specified without '
- 'argument, the GZIP compression is used. Additional compression '
- 'filters may be available, depending on your HDF5 installation.')
+ help="Compression filter. By default, the datasets in the output "
+ "file are not compressed. If this option is specified without "
+ "argument, the GZIP compression is used. Additional compression "
+ "filters may be available, depending on your HDF5 installation.",
+ )
def check_gzip_compression_opts(value):
ivalue = int(value)
if ivalue < 0 or ivalue > 9:
raise argparse.ArgumentTypeError(
- "--compression-opts must be an int from 0 to 9")
+ "--compression-opts must be an int from 0 to 9"
+ )
return ivalue
parser.add_argument(
- '--compression-opts',
+ "--compression-opts",
type=check_gzip_compression_opts,
help='Compression options. For "gzip", this may be an integer from '
- '0 to 9, with a default of 4. This is only supported for GZIP.')
+ "0 to 9, with a default of 4. This is only supported for GZIP.",
+ )
parser.add_argument(
- '--shuffle',
+ "--shuffle",
action="store_true",
- help='Enables the byte shuffle filter. This may improve the compression '
- 'ratio for block oriented compressors like GZIP or LZF.')
+ help="Enables the byte shuffle filter. This may improve the compression "
+ "ratio for block oriented compressors like GZIP or LZF.",
+ )
parser.add_argument(
- '--fletcher32',
+ "--fletcher32",
action="store_true",
- help='Adds a checksum to each chunk to detect data corruption.')
+ help="Adds a checksum to each chunk to detect data corruption.",
+ )
parser.add_argument(
- '--debug',
+ "--debug",
action="store_true",
default=False,
- help='Set logging system in debug mode')
+ help="Set logging system in debug mode",
+ )
options = parser.parse_args(argv[1:])
@@ -330,8 +353,10 @@ def main(argv):
write_to_h5 = None
if hdf5plugin is None:
- message = "Module 'hdf5plugin' is not installed. It supports additional hdf5"\
- + " compressions. You can install it using \"pip install hdf5plugin\"."
+ message = (
+ "Module 'hdf5plugin' is not installed. It supports additional hdf5"
+ + ' compressions. You can install it using "pip install hdf5plugin".'
+ )
_logger.debug(message)
# Process input arguments (mutually exclusive arguments)
@@ -361,33 +386,40 @@ def main(argv):
dirname = os.path.dirname(options.file_pattern)
file_pattern_re = c_format_string_to_re(options.file_pattern) + "$"
files_in_dir = glob(os.path.join(dirname, "*"))
- _logger.debug("""
+ _logger.debug(
+ """
Processing file_pattern
dirname: %s
file_pattern_re: %s
files_in_dir: %s
- """, dirname, file_pattern_re, files_in_dir)
-
- options.input_files = sorted(list(filter(lambda name: re.match(file_pattern_re, name),
- files_in_dir)))
+ """,
+ dirname,
+ file_pattern_re,
+ files_in_dir,
+ )
+
+ options.input_files = sorted(
+ list(filter(lambda name: re.match(file_pattern_re, name), files_in_dir))
+ )
_logger.debug("options.input_files: %s", options.input_files)
if options.begin is not None:
- options.input_files = drop_indices_before_begin(options.input_files,
- file_pattern_re,
- options.begin)
- _logger.debug("options.input_files after applying --begin: %s",
- options.input_files)
+ options.input_files = drop_indices_before_begin(
+ options.input_files, file_pattern_re, options.begin
+ )
+ _logger.debug(
+ "options.input_files after applying --begin: %s", options.input_files
+ )
if options.end is not None:
- options.input_files = drop_indices_after_end(options.input_files,
- file_pattern_re,
- options.end)
- _logger.debug("options.input_files after applying --end: %s",
- options.input_files)
-
- if are_files_missing_in_series(options.input_files,
- file_pattern_re):
+ options.input_files = drop_indices_after_end(
+ options.input_files, file_pattern_re, options.end
+ )
+ _logger.debug(
+ "options.input_files after applying --end: %s", options.input_files
+ )
+
+ if are_files_missing_in_series(options.input_files, file_pattern_re):
_logger.error("File missing in the file series. Aborting.")
return -1
@@ -403,37 +435,39 @@ def main(argv):
if os.path.isfile(output_name):
if options.mode == "w-":
- _logger.error("Output file %s exists and mode is 'w-' (default)."
- " Aborting. To append data to an existing file, "
- "use 'a' or 'r+'.",
- output_name)
+ _logger.error(
+ "Output file %s exists and mode is 'w-' (default)."
+ " Aborting. To append data to an existing file, "
+ "use 'a' or 'r+'.",
+ output_name,
+ )
return -1
elif not os.access(output_name, os.W_OK):
- _logger.error("Output file %s exists and is not writeable.",
- output_name)
+ _logger.error("Output file %s exists and is not writeable.", output_name)
return -1
elif options.mode == "w":
- _logger.info("Output file %s exists and mode is 'w'. "
- "Overwriting existing file.", output_name)
+ _logger.info(
+ "Output file %s exists and mode is 'w'. " "Overwriting existing file.",
+ output_name,
+ )
elif options.mode in ["a", "r+"]:
- _logger.info("Appending data to existing file %s.",
- output_name)
+ _logger.info("Appending data to existing file %s.", output_name)
else:
if options.mode == "r+":
- _logger.error("Output file %s does not exist and mode is 'r+'"
- " (append, file must exist). Aborting.",
- output_name)
+ _logger.error(
+ "Output file %s does not exist and mode is 'r+'"
+ " (append, file must exist). Aborting.",
+ output_name,
+ )
return -1
else:
- _logger.info("Creating new output file %s.",
- output_name)
+ _logger.info("Creating new output file %s.", output_name)
# Test that all input files exist and are readable
bad_input = False
for fname in options.input_files:
if not os.access(fname, os.R_OK):
- _logger.error("Cannot read input file %s.",
- fname)
+ _logger.error("Cannot read input file %s.", fname)
bad_input = True
if bad_input:
_logger.error("Aborting.")
@@ -457,10 +491,12 @@ def main(argv):
nitems = numpy.prod(chunks)
nbytes = nitems * 8
if nbytes > 10**6:
- _logger.warning("Requested chunk size might be larger than"
- " the default 1MB chunk cache, for float64"
- " data. This can dramatically affect I/O "
- "performances.")
+ _logger.warning(
+ "Requested chunk size might be larger than"
+ " the default 1MB chunk cache, for float64"
+ " data. This can dramatically affect I/O "
+ "performances."
+ )
create_dataset_args["chunks"] = chunks
if options.compression is not None:
@@ -479,61 +515,78 @@ def main(argv):
if options.fletcher32:
create_dataset_args["fletcher32"] = True
- if (len(options.input_files) > 1 and
- not contains_specfile(options.input_files) and
- not contains_fiofile(options.input_files) and
- not options.add_root_group) or options.file_pattern is not None:
+ if (
+ len(options.input_files) > 1
+ and not contains_specfile(options.input_files)
+ and not contains_fiofile(options.input_files)
+ and not options.add_root_group
+ ) or options.file_pattern is not None:
# File series -> stack of images
input_group = fabioh5.File(file_series=options.input_files)
if hdf5_path != "/":
# we want to append only data and headers to an existing file
input_group = input_group["/scan_0/instrument/detector_0"]
with h5py.File(output_name, mode=options.mode) as h5f:
- write_to_h5(input_group, h5f,
- h5path=hdf5_path,
- overwrite_data=options.overwrite_data,
- create_dataset_args=create_dataset_args,
- min_size=options.min_size)
-
- elif len(options.input_files) == 1 or \
- are_all_specfile(options.input_files) or\
- are_all_fiofile(options.input_files) or\
- options.add_root_group:
+ write_to_h5(
+ input_group,
+ h5f,
+ h5path=hdf5_path,
+ overwrite_data=options.overwrite_data,
+ create_dataset_args=create_dataset_args,
+ min_size=options.min_size,
+ )
+
+ elif (
+ len(options.input_files) == 1
+ or are_all_specfile(options.input_files)
+ or are_all_fiofile(options.input_files)
+ or options.add_root_group
+ ):
# single file, or spec files
h5paths_and_groups = []
for input_name in options.input_files:
hdf5_path_for_file = hdf5_path
if options.add_root_group:
- hdf5_path_for_file = hdf5_path.rstrip("/") + "/" + os.path.basename(input_name)
+ hdf5_path_for_file = (
+ hdf5_path.rstrip("/") + "/" + os.path.basename(input_name)
+ )
try:
- h5paths_and_groups.append((hdf5_path_for_file,
- silx.io.open(input_name)))
+ h5paths_and_groups.append(
+ (hdf5_path_for_file, silx.io.open(input_name))
+ )
except IOError:
- _logger.error("Cannot read file %s. If this is a file format "
- "supported by the fabio library, you can try to"
- " install fabio (`pip install fabio`)."
- " Aborting conversion.",
- input_name)
+ _logger.error(
+ "Cannot read file %s. If this is a file format "
+ "supported by the fabio library, you can try to"
+ " install fabio (`pip install fabio`)."
+ " Aborting conversion.",
+ input_name,
+ )
return -1
with h5py.File(output_name, mode=options.mode) as h5f:
for hdf5_path_for_file, input_group in h5paths_and_groups:
- write_to_h5(input_group, h5f,
- h5path=hdf5_path_for_file,
- overwrite_data=options.overwrite_data,
- create_dataset_args=create_dataset_args,
- min_size=options.min_size)
+ write_to_h5(
+ input_group,
+ h5f,
+ h5path=hdf5_path_for_file,
+ overwrite_data=options.overwrite_data,
+ create_dataset_args=create_dataset_args,
+ min_size=options.min_size,
+ )
else:
# multiple file, SPEC and fabio images mixed
- _logger.error("Multiple files with incompatible formats specified. "
- "You can provide multiple SPEC files or multiple image "
- "files, but not both.")
+ _logger.error(
+ "Multiple files with incompatible formats specified. "
+ "You can provide multiple SPEC files or multiple image "
+ "files, but not both."
+ )
return -1
with h5py.File(output_name, mode="r+") as h5f:
# append "silx convert" to the creator attribute, for NeXus files
- previous_creator = h5f.attrs.get("creator", u"")
+ previous_creator = h5f.attrs.get("creator", "")
creator = "silx convert (v%s)" % silx.version
# only if it not already there
if creator not in previous_creator:
@@ -542,7 +595,7 @@ def main(argv):
else:
new_creator = previous_creator + "; " + creator
h5f.attrs["creator"] = numpy.array(
- new_creator,
- dtype=h5py.special_dtype(vlen=str))
+ new_creator, dtype=h5py.special_dtype(vlen=str)
+ )
return 0
diff --git a/src/silx/app/test/__init__.py b/src/silx/app/test/__init__.py
index 7790ee5..1d8207b 100644
--- a/src/silx/app/test/__init__.py
+++ b/src/silx/app/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/app/test/test_convert.py b/src/silx/app/test/test_convert.py
index 2148db5..7ff94a3 100644
--- a/src/silx/app/test/test_convert.py
+++ b/src/silx/app/test/test_convert.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,7 +29,6 @@ __date__ = "17/01/2018"
import os
-import sys
import tempfile
import unittest
import io
@@ -121,16 +119,12 @@ class TestConvertCommand(unittest.TestCase):
# write a temporary SPEC file
specname = os.path.join(tempdir, "input.dat")
with io.open(specname, "wb") as fd:
- if sys.version_info < (3, ):
- fd.write(sftext)
- else:
- fd.write(bytes(sftext, 'ascii'))
+ fd.write(bytes(sftext, "ascii"))
# convert it
h5name = os.path.join(tempdir, "output.h5")
assert not os.path.isfile(h5name)
- command_list = ["convert", "-m", "w",
- specname, "-o", h5name]
+ command_list = ["convert", "-m", "w", specname, "-o", h5name]
result = convert.main(command_list)
self.assertEqual(result, 0)
@@ -138,15 +132,10 @@ class TestConvertCommand(unittest.TestCase):
with h5py.File(h5name, "r") as h5f:
title12 = h5py_read_dataset(h5f["/1.2/title"])
- if sys.version_info < (3, ):
- title12 = title12.encode("utf-8")
- self.assertEqual(title12,
- "aaaaaa")
+ self.assertEqual(title12, "aaaaaa")
creator = h5f.attrs.get("creator")
self.assertIsNotNone(creator, "No creator attribute in NXroot group")
- if sys.version_info < (3, ):
- creator = creator.encode("utf-8")
self.assertIn("silx convert (v%s)" % silx.version, creator)
# delete input file
diff --git a/src/silx/app/test_.py b/src/silx/app/test_.py
index 2b6bdf8..9696eb2 100644
--- a/src/silx/app/test_.py
+++ b/src/silx/app/test_.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
#
diff --git a/src/silx/app/setup.py b/src/silx/app/utils/__init__.py
index 85c3662..97ef4a5 100644
--- a/src/silx/app/setup.py
+++ b/src/silx/app/utils/__init__.py
@@ -1,6 +1,5 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -21,21 +20,8 @@
# THE SOFTWARE.
#
# ############################################################################*/
+"""Package containing utils related to applications"""
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "23/04/2018"
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('app', parent_package, top_path)
- config.add_subpackage('test')
- config.add_subpackage('view')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
+__date__ = "07/06/2023"
diff --git a/src/silx/app/utils/parseutils.py b/src/silx/app/utils/parseutils.py
new file mode 100644
index 0000000..4135290
--- /dev/null
+++ b/src/silx/app/utils/parseutils.py
@@ -0,0 +1,133 @@
+# /*##########################################################################
+# Copyright (C) 2018-2023 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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.
+#
+# ############################################################################*/
+"""Utils related to parsing"""
+
+from __future__ import annotations
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "28/05/2018"
+
+from collections.abc import Sequence
+import glob
+import logging
+from typing import Generator, Iterable, Union, Any, Optional
+from pathlib import Path
+
+
+_logger = logging.getLogger(__name__)
+"""Module logger"""
+
+
+_trueStrings = {"yes", "true", "1"}
+_falseStrings = {"no", "false", "0"}
+
+
+def _string_to_bool(string: str) -> bool:
+ """Returns a boolean from a string.
+
+ :raise ValueError: If the string do not contains a boolean information.
+ """
+ lower = string.lower()
+ if lower in _trueStrings:
+ return True
+ if lower in _falseStrings:
+ return False
+ raise ValueError("'%s' is not a valid boolean" % string)
+
+
+def to_bool(thing: Any, default: Optional[bool] = None) -> bool:
+ """Returns a boolean from an object.
+
+ :raise ValueError: If the thing can't be interpreted as a boolean and
+ no default is set
+ """
+ if isinstance(thing, bool):
+ return thing
+ try:
+ return _string_to_bool(thing)
+ except ValueError:
+ if default is not None:
+ return default
+ raise
+
+
+def filenames_to_dataurls(
+ filenames: Iterable[Union[str, Path]],
+ slices: Sequence[int] = tuple(),
+) -> Generator[object, None, None]:
+ """Expand filenames and HDF5 data path in files input argument"""
+ # Imports here so they are performed after setting HDF5_USE_FILE_LOCKING and logging level
+ import silx.io
+ from silx.io.utils import match
+ from silx.io.url import DataUrl
+ import silx.utils.files
+
+ extra_slices = tuple(slices)
+
+ for filename in filenames:
+ url = DataUrl(filename)
+
+ for file_path in sorted(silx.utils.files.expand_filenames([url.file_path()])):
+ if url.data_path() is not None and glob.has_magic(url.data_path()):
+ try:
+ with silx.io.open(file_path) as f:
+ data_paths = list(match(f, url.data_path()))
+ except BaseException as e:
+ _logger.error(
+ f"Error searching HDF5 path pattern '{url.data_path()}' in file '{file_path}': Ignored"
+ )
+ _logger.error(e.args[0])
+ _logger.debug("Backtrace", exc_info=True)
+ continue
+ else:
+ data_paths = [url.data_path()]
+
+ if not extra_slices:
+ data_slices = (url.data_slice(),)
+ elif not url.data_slice():
+ data_slices = extra_slices
+ else:
+ data_slices = [tuple(url.data_slice()) + (s,) for s in extra_slices]
+
+ for data_path in data_paths:
+ for data_slice in data_slices:
+ yield DataUrl(
+ file_path=file_path,
+ data_path=data_path,
+ data_slice=data_slice,
+ scheme=url.scheme(),
+ )
+
+
+def to_enum(thing: Any, enum_type, default: Optional[object] = None):
+ """Parse this string as this enum_type."""
+ try:
+ v = getattr(enum_type, str(thing))
+ if isinstance(v, enum_type):
+ return v
+ raise ValueError(f"{thing} is not a {enum_type.__name__}")
+ except (AttributeError, ValueError) as e:
+ if default is not None:
+ return default
+ raise
diff --git a/src/silx/app/utils/test/__init__.py b/src/silx/app/utils/test/__init__.py
new file mode 100644
index 0000000..f94d0a3
--- /dev/null
+++ b/src/silx/app/utils/test/__init__.py
@@ -0,0 +1,23 @@
+# /*##########################################################################
+#
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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.
+#
+# ###########################################################################*/
diff --git a/src/silx/app/utils/test/test_parseutils.py b/src/silx/app/utils/test/test_parseutils.py
new file mode 100644
index 0000000..9570bb7
--- /dev/null
+++ b/src/silx/app/utils/test/test_parseutils.py
@@ -0,0 +1,68 @@
+# /*##########################################################################
+# Copyright (C) 2018-2023 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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.
+#
+# ############################################################################*/
+
+import pytest
+import h5py
+from ..parseutils import filenames_to_dataurls
+
+
+@pytest.fixture(scope="module")
+def data_path(tmp_path_factory):
+ tmp_path = tmp_path_factory.mktemp("silx_app_utils")
+ with h5py.File(tmp_path / "test1.h5", "w") as h5:
+ h5["g1/sub1/data1"] = 1
+ h5["g1/sub1/data2"] = 2
+ h5["g1/sub2/data1"] = 3
+ return tmp_path
+
+
+def test_h5__datapath(data_path):
+ urls = filenames_to_dataurls([data_path / "test1.h5::g1/sub1/data1"])
+ urls = list(urls)
+ assert len(urls) == 1
+ assert urls[0].data_path().replace("\\", "/") == "g1/sub1/data1"
+
+
+def test_h5__datapath_not_existing(data_path):
+ urls = filenames_to_dataurls([data_path / "test1.h5::g1/sub0/data1"])
+ urls = list(urls)
+ assert len(urls) == 1
+ assert urls[0].data_path().replace("\\", "/") == "g1/sub0/data1"
+
+
+def test_h5__datapath_with_magic(data_path):
+ urls = filenames_to_dataurls([data_path / "test1.h5::g1/sub*/data*"])
+ urls = list(urls)
+ assert len(urls) == 3
+
+
+def test_h5__datapath_with_magic_not_existing(data_path):
+ urls = filenames_to_dataurls([data_path / "test1.h5::g1/sub0/data*"])
+ urls = list(urls)
+ assert len(urls) == 0
+
+
+def test_h5__datapath_with_recursive_magic(data_path):
+ urls = filenames_to_dataurls([data_path / "test1.h5::**/data1"])
+ urls = list(urls)
+ assert len(urls) == 2
diff --git a/src/silx/app/view/About.py b/src/silx/app/view/About.py
index 85f1450..350337d 100644
--- a/src/silx/app/view/About.py
+++ b/src/silx/app/view/About.py
@@ -1,6 +1,5 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2022 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
@@ -25,7 +24,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/07/2018"
+__date__ = "18/01/2022"
import os
import sys
@@ -116,10 +115,9 @@ class About(qt.QDialog):
:rtype: str
"""
from silx._version import __date__ as date
+
year = date.split("/")[2]
- info = dict(
- year=year
- )
+ info = dict(year=year)
textLicense = _LICENSE_TEMPLATE.format(**info)
return textLicense
@@ -192,6 +190,7 @@ class About(qt.QDialog):
# Previous versions only return True if the filter was first used
# to decode a dataset
import h5py.h5z
+
FILTER_LZ4 = 32004
FILTER_BITSHUFFLE = 32008
filters = [
@@ -202,7 +201,11 @@ class About(qt.QDialog):
isAvailable = h5py.h5z.filter_avail(filterId)
optionals.append(self.__formatOptionalFilters(name, isAvailable))
else:
- optionals.append(self.__formatOptionalLibraries("hdf5plugin", "hdf5plugin" in sys.modules))
+ optionals.append(
+ self.__formatOptionalLibraries(
+ "hdf5plugin", "hdf5plugin" in sys.modules
+ )
+ )
# Access to the logo in SVG or PNG
logo = icons.getQFile("silx:" + os.path.join("gui", "logo", "silx"))
@@ -218,7 +221,7 @@ class About(qt.QDialog):
qt_version=qt.qVersion(),
python_version=sys.version.replace("\n", "<br />"),
optional_lib="<br />".join(optionals),
- silx_image_path=logo.fileName()
+ silx_image_path=logo.fileName(),
)
self.__label.setText(message.format(**info))
@@ -226,14 +229,18 @@ class About(qt.QDialog):
def __updateSize(self):
"""Force the size to a QMessageBox like size."""
- if qt.BINDING in ("PySide2", "PyQt5"):
- screenSize = qt.QApplication.desktop().availableGeometry(qt.QCursor.pos()).size()
+ if qt.BINDING == "PyQt5":
+ screenSize = (
+ qt.QApplication.desktop().availableGeometry(qt.QCursor.pos()).size()
+ )
else: # Qt6
- screenSize = qt.QApplication.instance().primaryScreen().availableGeometry().size()
+ screenSize = (
+ qt.QApplication.instance().primaryScreen().availableGeometry().size()
+ )
hardLimit = min(screenSize.width() - 480, 1000)
if screenSize.width() <= 1024:
hardLimit = screenSize.width()
- softLimit = min(screenSize.width() / 2, 420)
+ softLimit = min(screenSize.width() // 2, 420)
layoutMinimumSize = self.layout().totalMinimumSize()
width = layoutMinimumSize.width()
@@ -243,7 +250,7 @@ class About(qt.QDialog):
width = hardLimit
height = layoutMinimumSize.height()
- self.setFixedSize(width, height)
+ self.setFixedSize(int(width), int(height))
@staticmethod
def about(parent, applicationName):
diff --git a/src/silx/app/view/ApplicationContext.py b/src/silx/app/view/ApplicationContext.py
index 324f3b8..157b8cc 100644
--- a/src/silx/app/view/ApplicationContext.py
+++ b/src/silx/app/view/ApplicationContext.py
@@ -1,6 +1,5 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,6 +28,7 @@ __date__ = "23/05/2018"
import weakref
import logging
+from collections.abc import Sequence
import silx
from silx.gui.data.DataViews import DataViewHooks
@@ -70,15 +70,20 @@ class ApplicationContext(DataViewHooks):
if settings is None:
return
settings.beginGroup("library")
+ mplTightLayout = settings.value("mpl.tight_layout", False, bool)
plotBackend = settings.value("plot.backend", "")
plotImageYAxisOrientation = settings.value("plot-image.y-axis-orientation", "")
settings.endGroup()
# Use matplotlib backend by default
- silx.config.DEFAULT_PLOT_BACKEND = \
- "opengl" if plotBackend == "opengl" else "matplotlib"
+ silx.config.DEFAULT_PLOT_BACKEND = (
+ ("opengl", "matplotlib") if plotBackend == "opengl" else "matplotlib"
+ )
if plotImageYAxisOrientation != "":
- silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = plotImageYAxisOrientation
+ silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = (
+ plotImageYAxisOrientation
+ )
+ silx.config._MPL_TIGHT_LAYOUT = mplTightLayout
def restoreSettings(self):
"""Restore the settings of all the application"""
@@ -122,8 +127,12 @@ class ApplicationContext(DataViewHooks):
settings.endGroup()
settings.beginGroup("library")
- settings.setValue("plot.backend", silx.config.DEFAULT_PLOT_BACKEND)
- settings.setValue("plot-image.y-axis-orientation", silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION)
+ settings.setValue("plot.backend", self.getDefaultPlotBackend())
+ settings.setValue(
+ "plot-image.y-axis-orientation",
+ silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION,
+ )
+ settings.setValue("mpl.tight_layout", silx.config._MPL_TIGHT_LAYOUT)
settings.endGroup()
settings.beginGroup("recent-files")
@@ -163,8 +172,7 @@ class ApplicationContext(DataViewHooks):
self.__recentFiles.pop()
def clearRencentFiles(self):
- """Clear the history of the rencent files.
- """
+ """Clear the history of the rencent files."""
self.__recentFiles[:] = []
def getColormap(self, view):
@@ -193,3 +201,17 @@ class ApplicationContext(DataViewHooks):
dialog.setModal(False)
self.__defaultColormapDialog = dialog
return self.__defaultColormapDialog
+
+ @staticmethod
+ def getDefaultPlotBackend() -> str:
+ """Returns default plot backend as a str from current config"""
+ backend = silx.config.DEFAULT_PLOT_BACKEND
+ if isinstance(backend, str):
+ return backend
+ if (
+ isinstance(backend, Sequence)
+ and len(backend)
+ and isinstance(backend[0], str)
+ ):
+ return backend[0]
+ return "matplotlib" # fallback
diff --git a/src/silx/app/view/CustomNxdataWidget.py b/src/silx/app/view/CustomNxdataWidget.py
index 8c6cd39..3ec62c0 100644
--- a/src/silx/app/view/CustomNxdataWidget.py
+++ b/src/silx/app/view/CustomNxdataWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
#
@@ -569,7 +568,7 @@ class _Model(qt.QStandardItemModel):
"""
if isinstance(item, _NxDataItem):
parent = item.parent()
- assert(parent is None)
+ assert parent is None
model = item.model()
model.removeRow(item.row())
else:
@@ -694,7 +693,7 @@ class CustomNxDataToolBar(qt.QToolBar):
def setCustomNxDataWidget(self, widget):
"""Set the linked CustomNxdataWidget to this toolbar."""
- assert(isinstance(widget, CustomNxdataWidget))
+ assert isinstance(widget, CustomNxdataWidget)
if self.__nxdataWidget is not None:
selectionModel = self.__nxdataWidget.selectionModel()
selectionModel.currentChanged.disconnect(self.__currentSelectionChanged)
@@ -714,7 +713,9 @@ class CustomNxDataToolBar(qt.QToolBar):
item = model.itemFromIndex(index)
self.__removeNxDataAction.setEnabled(isinstance(item, _NxDataItem))
self.__removeNxDataAxisAction.setEnabled(isinstance(item, _DatasetAxisItemRow))
- self.__addNxDataAxisAction.setEnabled(isinstance(item, _NxDataItem) or isinstance(item, _DatasetItemRow))
+ self.__addNxDataAxisAction.setEnabled(
+ isinstance(item, _NxDataItem) or isinstance(item, _DatasetItemRow)
+ )
class _HashDropZones(qt.QStyledItemDelegate):
@@ -848,7 +849,9 @@ class CustomNxdataWidget(qt.QTreeView):
if isinstance(item, _NxDataItem):
action = qt.QAction("Add a new axis", menu)
- action.triggered.connect(lambda: weakself.model().appendAxisToNxdataItem(item))
+ action.triggered.connect(
+ lambda: weakself.model().appendAxisToNxdataItem(item)
+ )
action.setIcon(icons.getQIcon("nxdata-axis-add"))
action.setIconVisibleInMenu(True)
menu.addAction(action)
diff --git a/src/silx/app/view/DataPanel.py b/src/silx/app/view/DataPanel.py
index 5d87381..592a520 100644
--- a/src/silx/app/view/DataPanel.py
+++ b/src/silx/app/view/DataPanel.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
# Copyright (C) 2018 European Synchrotron Radiation Facility
#
@@ -38,7 +37,6 @@ _logger = logging.getLogger(__name__)
class _HeaderLabel(qt.QLabel):
-
def __init__(self, parent=None):
qt.QLabel.__init__(self, parent=parent)
self.setFrameShape(qt.QFrame.StyledPanel)
@@ -90,7 +88,6 @@ class _HeaderLabel(qt.QLabel):
class DataPanel(qt.QWidget):
-
def __init__(self, parent=None, context=None):
qt.QWidget.__init__(self, parent=parent)
diff --git a/src/silx/app/view/Viewer.py b/src/silx/app/view/Viewer.py
index 7e5e4c9..12426a1 100644
--- a/src/silx/app/view/Viewer.py
+++ b/src/silx/app/view/Viewer.py
@@ -1,6 +1,5 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -23,15 +22,19 @@
# ############################################################################*/
"""Browse a data file with a GUI"""
+from __future__ import annotations
+
__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "15/01/2019"
import os
-import collections
import logging
import functools
+import traceback
+from types import TracebackType
+from typing import Optional
import silx.io.nxdata
from silx.gui import qt
@@ -40,7 +43,7 @@ import silx.gui.hdf5
from .ApplicationContext import ApplicationContext
from .CustomNxdataWidget import CustomNxdataWidget
from .CustomNxdataWidget import CustomNxDataToolBar
-from . import utils
+from ..utils import parseutils
from silx.gui.utils import projecturl
from .DataPanel import DataPanel
@@ -65,6 +68,8 @@ class Viewer(qt.QMainWindow):
silxIcon = icons.getQIcon("silx")
self.setWindowIcon(silxIcon)
+ self.__error = ""
+
self.__context = self.createApplicationContext(settings)
self.__context.restoreLibrarySettings()
@@ -87,7 +92,9 @@ class Viewer(qt.QMainWindow):
treeModel.sigH5pyObjectRemoved.connect(self.__h5FileRemoved)
treeModel.sigH5pyObjectSynchronized.connect(self.__h5FileSynchonized)
treeModel.setDatasetDragEnabled(True)
- self.__treeModelSorted = silx.gui.hdf5.NexusSortFilterProxyModel(self.__treeview)
+ self.__treeModelSorted = silx.gui.hdf5.NexusSortFilterProxyModel(
+ self.__treeview
+ )
self.__treeModelSorted.setSourceModel(treeModel)
self.__treeModelSorted.sort(0, qt.Qt.AscendingOrder)
self.__treeModelSorted.setSortCaseSensitivity(qt.Qt.CaseInsensitive)
@@ -142,8 +149,8 @@ class Viewer(qt.QMainWindow):
columns.insert(1, treeModel.DESCRIPTION_COLUMN)
self.__treeview.header().setSections(columns)
- self._iconUpward = icons.getQIcon('plot-yup')
- self._iconDownward = icons.getQIcon('plot-ydown')
+ self._iconUpward = icons.getQIcon("plot-yup")
+ self._iconDownward = icons.getQIcon("plot-ydown")
self.createActions()
self.createMenus()
@@ -162,23 +169,22 @@ class Viewer(qt.QMainWindow):
action.setText("Refresh")
action.setToolTip("Refresh all selected items")
action.triggered.connect(self.__refreshSelected)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5))
+ action.setShortcuts(
+ [
+ qt.QKeySequence(qt.Qt.Key_F5),
+ qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_R),
+ ]
+ )
toolbar.addAction(action)
treeView.addAction(action)
self.__refreshAction = action
- # Another shortcut for refresh
- action = qt.QAction(toolbar)
- action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_R))
- treeView.addAction(action)
- action.triggered.connect(self.__refreshSelected)
-
action = qt.QAction(toolbar)
# action.setIcon(icons.getQIcon("view-refresh"))
action.setText("Close")
action.setToolTip("Close selected item")
action.triggered.connect(self.__removeSelected)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_Delete))
+ action.setShortcut(qt.QKeySequence.Delete)
treeView.addAction(action)
self.__closeAction = action
@@ -189,7 +195,7 @@ class Viewer(qt.QMainWindow):
action.setText("Expand all")
action.setToolTip("Expand all selected items")
action.triggered.connect(self.__expandAllSelected)
- action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_Plus))
+ action.setShortcut(qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_Plus))
toolbar.addAction(action)
treeView.addAction(action)
self.__expandAllAction = action
@@ -199,7 +205,7 @@ class Viewer(qt.QMainWindow):
action.setText("Collapse all")
action.setToolTip("Collapse all selected items")
action.triggered.connect(self.__collapseAllSelected)
- action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_Minus))
+ action.setShortcut(qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_Minus))
toolbar.addAction(action)
treeView.addAction(action)
self.__collapseAllAction = action
@@ -254,20 +260,18 @@ class Viewer(qt.QMainWindow):
qt.QApplication.restoreOverrideCursor()
def __refreshSelected(self):
- """Refresh all selected items
- """
+ """Refresh all selected items"""
qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
selection = self.__treeview.selectionModel()
indexes = selection.selectedIndexes()
selectedItems = []
model = self.__treeview.model()
- h5files = set([])
+ h5files = []
while len(indexes) > 0:
index = indexes.pop(0)
if index.column() != 0:
continue
- h5 = model.data(index, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
rootIndex = index
# Reach the root of the tree
while rootIndex.parent().isValid():
@@ -275,15 +279,21 @@ class Viewer(qt.QMainWindow):
rootRow = rootIndex.row()
relativePath = self.__getRelativePath(model, rootIndex, index)
selectedItems.append((rootRow, relativePath))
- h5files.add(h5.file)
+ h5 = model.data(
+ rootIndex, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE
+ )
+ item = model.data(
+ rootIndex, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_ITEM_ROLE
+ )
+ h5files.append((h5, item._openedPath))
if len(h5files) == 0:
qt.QApplication.restoreOverrideCursor()
return
model = self.__treeview.findHdf5TreeModel()
- for h5 in h5files:
- self.__synchronizeH5pyObject(h5)
+ for h5, filename in h5files:
+ self.__synchronizeH5pyObject(h5, filename)
model = self.__treeview.model()
itemSelection = qt.QItemSelection()
@@ -298,14 +308,15 @@ class Viewer(qt.QMainWindow):
qt.QApplication.restoreOverrideCursor()
- def __synchronizeH5pyObject(self, h5):
+ def __synchronizeH5pyObject(self, h5, filename: Optional[str] = None):
model = self.__treeview.findHdf5TreeModel()
# This is buggy right now while h5py do not allow to close a file
# while references are still used.
# FIXME: The architecture have to be reworked to support this feature.
# model.synchronizeH5pyObject(h5)
- filename = h5.filename
+ if filename is None:
+ filename = f"{h5.file.filename}::{h5.name}"
row = model.h5pyObjectRow(h5)
index = self.__treeview.model().index(row, 0, qt.QModelIndex())
paths = self.__getPathFromExpandedNodes(self.__treeview, index)
@@ -348,7 +359,7 @@ class Viewer(qt.QMainWindow):
path = node._getCanonicalName()
if rootPath is None:
rootPath = path
- path = path[len(rootPath):]
+ path = path[len(rootPath) :]
paths.append(path)
for child in range(model.rowCount(index)):
@@ -453,9 +464,9 @@ class Viewer(qt.QMainWindow):
layout.addWidget(customNxdataWidget)
return widget
- def __h5FileLoaded(self, loadedH5):
+ def __h5FileLoaded(self, loadedH5, filename):
self.__context.pushRecentFile(loadedH5.file.filename)
- if loadedH5.file.filename == self.__displayIt:
+ if filename == self.__displayIt:
self.__displayIt = None
self.displayData(loadedH5)
@@ -519,11 +530,7 @@ class Viewer(qt.QMainWindow):
size = settings.value("size", qt.QSize(640, 480))
pos = settings.value("pos", qt.QPoint())
isFullScreen = settings.value("full-screen", False)
- try:
- if not isinstance(isFullScreen, bool):
- isFullScreen = utils.stringToBool(isFullScreen)
- except ValueError:
- isFullScreen = False
+ isFullScreen = parseutils.to_bool(isFullScreen, False)
settings.endGroup()
settings.beginGroup("mainlayout")
@@ -540,23 +547,14 @@ class Viewer(qt.QMainWindow):
except Exception:
_logger.debug("Backtrace", exc_info=True)
isVisible = settings.value("custom-nxdata-window-visible", False)
- try:
- if not isinstance(isVisible, bool):
- isVisible = utils.stringToBool(isVisible)
- except ValueError:
- isVisible = False
+ isVisible = parseutils.to_bool(isVisible, False)
self.__customNxdataWindow.setVisible(isVisible)
self._displayCustomNxdataWindow.setChecked(isVisible)
-
settings.endGroup()
settings.beginGroup("content")
isSorted = settings.value("is-sorted", True)
- try:
- if not isinstance(isSorted, bool):
- isSorted = utils.stringToBool(isSorted)
- except ValueError:
- isSorted = True
+ isSorted = parseutils.to_bool(isSorted, True)
self.setContentSorted(isSorted)
settings.endGroup()
@@ -569,12 +567,13 @@ class Viewer(qt.QMainWindow):
def createActions(self):
action = qt.QAction("E&xit", self)
- action.setShortcuts(qt.QKeySequence.Quit)
+ action.setShortcut(qt.QKeySequence.Quit)
action.setStatusTip("Exit the application")
action.triggered.connect(self.close)
self._exitAction = action
action = qt.QAction("&Open...", self)
+ action.setShortcut(qt.QKeySequence.Open)
action.setStatusTip("Open a file")
action.triggered.connect(self.open)
self._openAction = action
@@ -584,6 +583,7 @@ class Viewer(qt.QMainWindow):
self._openRecentMenu = menu
action = qt.QAction("Close All", self)
+ action.setShortcut(qt.QKeySequence.Close)
action.setStatusTip("Close all opened files")
action.triggered.connect(self.closeAll)
self._closeAllAction = action
@@ -625,9 +625,11 @@ class Viewer(qt.QMainWindow):
# Plot image orientation
self._plotImageOrientationMenu = qt.QMenu(
- "Default plot image y-axis orientation", self)
+ "Default plot image y-axis orientation", self
+ )
self._plotImageOrientationMenu.setStatusTip(
- "Select the default y-axis orientation used by plot displaying images")
+ "Select the default y-axis orientation used by plot displaying images"
+ )
group = qt.QActionGroup(self)
group.setExclusive(True)
@@ -650,10 +652,19 @@ class Viewer(qt.QMainWindow):
self._plotImageOrientationMenu.addAction(action)
self._useYAxisOrientationUpward = action
+ # mpl layout
+
+ action = qt.QAction("Use MPL tight layout", self)
+ action.setCheckable(True)
+ action.triggered.connect(self.__forceMplTightLayout)
+ self._useMplTightLayout = action
+
# Windows
action = qt.QAction("Show custom NXdata selector", self)
- action.setStatusTip("Show a widget which allow to create plot by selecting data and axes")
+ action.setStatusTip(
+ "Show a widget which allow to create plot by selecting data and axes"
+ )
action.setCheckable(True)
action.setShortcut(qt.QKeySequence(qt.Qt.Key_F6))
action.toggled.connect(self.__toggleCustomNxdataWindow)
@@ -672,7 +683,9 @@ class Viewer(qt.QMainWindow):
baseName = os.path.basename(filePath)
action = qt.QAction(baseName, self)
action.setToolTip(filePath)
- action.triggered.connect(functools.partial(self.__openRecentFile, filePath))
+ action.triggered.connect(
+ functools.partial(self.__openRecentFile, filePath)
+ )
self._openRecentMenu.addAction(action)
self._openRecentMenu.addSeparator()
baseName = os.path.basename(filePath)
@@ -694,17 +707,18 @@ class Viewer(qt.QMainWindow):
# plot backend
title = self._plotBackendMenu.title().split(": ", 1)[0]
- self._plotBackendMenu.setTitle("%s: %s" % (title, silx.config.DEFAULT_PLOT_BACKEND))
+ backend = self.__context.getDefaultPlotBackend()
+ self._plotBackendMenu.setTitle(f"{title}: {backend}")
action = self._usePlotWithMatplotlib
- action.setChecked(silx.config.DEFAULT_PLOT_BACKEND in ["matplotlib", "mpl"])
+ action.setChecked(backend in ["matplotlib", "mpl"])
title = action.text().split(" (", 1)[0]
if not action.isChecked():
title += " (applied after application restart)"
action.setText(title)
action = self._usePlotWithOpengl
- action.setChecked(silx.config.DEFAULT_PLOT_BACKEND in ["opengl", "gl"])
+ action.setChecked(backend in ["opengl", "gl"])
title = action.text().split(" (", 1)[0]
if not action.isChecked():
title += " (applied after application restart)"
@@ -719,19 +733,28 @@ class Viewer(qt.QMainWindow):
menu.setIcon(self._iconUpward)
action = self._useYAxisOrientationDownward
- action.setChecked(silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward")
+ action.setChecked(
+ silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward"
+ )
title = action.text().split(" (", 1)[0]
if not action.isChecked():
title += " (applied after application restart)"
action.setText(title)
action = self._useYAxisOrientationUpward
- action.setChecked(silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION != "downward")
+ action.setChecked(
+ silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION != "downward"
+ )
title = action.text().split(" (", 1)[0]
if not action.isChecked():
title += " (applied after application restart)"
action.setText(title)
+ # mpl
+
+ action = self._useMplTightLayout
+ action.setChecked(silx.config._MPL_TIGHT_LAYOUT)
+
def createMenus(self):
fileMenu = self.menuBar().addMenu("&File")
fileMenu.addAction(self._openAction)
@@ -744,6 +767,7 @@ class Viewer(qt.QMainWindow):
optionMenu = self.menuBar().addMenu("&Options")
optionMenu.addMenu(self._plotImageOrientationMenu)
optionMenu.addMenu(self._plotBackendMenu)
+ optionMenu.addAction(self._useMplTightLayout)
optionMenu.aboutToShow.connect(self.__updateOptionMenu)
viewMenu = self.menuBar().addMenu("&Views")
@@ -753,6 +777,17 @@ class Viewer(qt.QMainWindow):
helpMenu.addAction(self._aboutAction)
helpMenu.addAction(self._documentationAction)
+ self.__errorButton = qt.QToolButton(self)
+ self.__errorButton.setIcon(
+ self.style().standardIcon(qt.QStyle.SP_MessageBoxWarning)
+ )
+ self.__errorButton.setToolTip(
+ "An error occured!\nClick to display last error\nor check messages in the console"
+ )
+ self.__errorButton.setVisible(False)
+ self.__errorButton.clicked.connect(self.__errorButtonClicked)
+ self.menuBar().setCornerWidget(self.__errorButton)
+
def open(self):
dialog = self.createFileDialog()
if self.__dialogState is None:
@@ -782,7 +817,7 @@ class Viewer(qt.QMainWindow):
dialog.setModal(True)
# NOTE: hdf5plugin have to be loaded before
- extensions = collections.OrderedDict()
+ extensions = {}
for description, ext in silx.io.supported_extensions().items():
extensions[description] = " ".join(sorted(list(ext)))
@@ -810,6 +845,7 @@ class Viewer(qt.QMainWindow):
def about(self):
from .About import About
+
About.about(self, "Silx viewer")
def showDocumentation(self):
@@ -824,7 +860,6 @@ class Viewer(qt.QMainWindow):
"""
sort = bool(sort)
if sort != self.isContentSorted():
-
# save expanded nodes
pathss = []
root = qt.QModelIndex()
@@ -835,7 +870,8 @@ class Viewer(qt.QMainWindow):
pathss.append(paths)
self.__treeview.setModel(
- self.__treeModelSorted if sort else self.__treeModelSorted.sourceModel())
+ self.__treeModelSorted if sort else self.__treeModelSorted.sourceModel()
+ )
self._sortContentAction.setChecked(self.isContentSorted())
# restore expanded nodes
@@ -862,7 +898,10 @@ class Viewer(qt.QMainWindow):
silx.config.DEFAULT_PLOT_BACKEND = "matplotlib"
def __forceOpenglBackend(self):
- silx.config.DEFAULT_PLOT_BACKEND = "opengl"
+ silx.config.DEFAULT_PLOT_BACKEND = "opengl", "matplotlib"
+
+ def __forceMplTightLayout(self):
+ silx.config._MPL_TIGHT_LAYOUT = self._useMplTightLayout.isChecked()
def appendFile(self, filename):
if self.__displayIt is None:
@@ -871,8 +910,7 @@ class Viewer(qt.QMainWindow):
self.__treeview.findHdf5TreeModel().appendFile(filename)
def displaySelectedData(self):
- """Called to update the dataviewer with the selected data.
- """
+ """Called to update the dataviewer with the selected data."""
selected = list(self.__treeview.selectedH5Nodes(ignoreBrokenLinks=False))
if len(selected) == 1:
# Update the viewer for a single selection
@@ -882,8 +920,7 @@ class Viewer(qt.QMainWindow):
_logger.debug("Too many data selected")
def displayData(self, data):
- """Called to update the dataviewer with a secific data.
- """
+ """Called to update the dataviewer with a secific data."""
self.__dataPanel.setData(data)
def displaySelectedCustomData(self):
@@ -955,8 +992,42 @@ class Viewer(qt.QMainWindow):
if silx.io.is_file(h5):
action = qt.QAction("Close %s" % obj.local_filename, event.source())
- action.triggered.connect(lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(h5))
+ action.triggered.connect(
+ lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(h5)
+ )
menu.addAction(action)
- action = qt.QAction("Synchronize %s" % obj.local_filename, event.source())
+ action = qt.QAction(
+ "Synchronize %s" % obj.local_filename, event.source()
+ )
action.triggered.connect(lambda: self.__synchronizeH5pyObject(h5))
menu.addAction(action)
+
+ def __errorButtonClicked(self):
+ button = qt.QMessageBox.warning(
+ self,
+ "Error",
+ self.getError(),
+ qt.QMessageBox.Reset | qt.QMessageBox.Close,
+ qt.QMessageBox.Close,
+ )
+ if button == qt.QMessageBox.Reset:
+ self.setError("")
+
+ def getError(self) -> str:
+ """Returns error information string"""
+ return self.__error
+
+ def setError(self, error: str):
+ """Set error information string"""
+ if error == self.__error:
+ return
+
+ self.__error = error
+ self.__errorButton.setVisible(error != "")
+
+ def setErrorFromException(
+ self, type_: type[BaseException], value: BaseException, trace: TracebackType
+ ):
+ """Set information about the last exception that occured"""
+ formattedTrace = "\n".join(traceback.format_tb(trace))
+ self.setError(f"{type_.__name__}:\n{value}\n\n{formattedTrace}")
diff --git a/src/silx/app/view/__init__.py b/src/silx/app/view/__init__.py
index 229c44e..97c64ef 100644
--- a/src/silx/app/view/__init__.py
+++ b/src/silx/app/view/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
#
diff --git a/src/silx/app/view/main.py b/src/silx/app/view/main.py
index dbc6a2b..f6c5274 100644
--- a/src/silx/app/view/main.py
+++ b/src/silx/app/view/main.py
@@ -1,6 +1,5 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,6 +31,8 @@ import logging
import os
import signal
import sys
+import traceback
+from silx.app.utils import parseutils
_logger = logging.getLogger(__name__)
@@ -41,38 +42,53 @@ _logger = logging.getLogger(__name__)
def createParser():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
- 'files',
+ "files",
nargs=argparse.ZERO_OR_MORE,
- help='Data file to show (h5 file, edf files, spec files)')
+ help="Data file to show (h5 file, edf files, spec files)",
+ )
parser.add_argument(
- '--debug',
+ "--slices",
+ dest="slices",
+ default=tuple(),
+ type=int,
+ nargs="+",
+ help="List of slice indices to open (Only for dataset)",
+ )
+ parser.add_argument(
+ "--debug",
dest="debug",
action="store_true",
default=False,
- help='Set logging system in debug mode')
+ help="Set logging system in debug mode",
+ )
parser.add_argument(
- '--use-opengl-plot',
+ "--use-opengl-plot",
dest="use_opengl_plot",
action="store_true",
default=False,
- help='Use OpenGL for plots (instead of matplotlib)')
+ help="Use OpenGL for plots (instead of matplotlib)",
+ )
parser.add_argument(
- '-f', '--fresh',
+ "-f",
+ "--fresh",
dest="fresh_preferences",
action="store_true",
default=False,
- help='Start the application using new fresh user preferences')
+ help="Start the application using new fresh user preferences",
+ )
parser.add_argument(
- '--hdf5-file-locking',
+ "--hdf5-file-locking",
dest="hdf5_file_locking",
action="store_true",
default=False,
- help='Start the application with HDF5 file locking enabled (it is disabled by default)')
+ help="Start the application with HDF5 file locking enabled (it is disabled by default)",
+ )
return parser
def createWindow(parent, settings):
from .Viewer import Viewer
+
window = Viewer(parent=None, settings=settings)
return window
@@ -92,7 +108,7 @@ def mainQt(options):
except ImportError:
_logger.debug("No resource module available")
else:
- if hasattr(resource, 'RLIMIT_NOFILE'):
+ if hasattr(resource, "RLIMIT_NOFILE"):
try:
hard_nofile = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
resource.setrlimit(resource.RLIMIT_NOFILE, (hard_nofile, hard_nofile))
@@ -102,9 +118,9 @@ def mainQt(options):
_logger.debug("Set max opened files to %d", hard_nofile)
# This needs to be done prior to load HDF5
- hdf5_file_locking = 'TRUE' if options.hdf5_file_locking else 'FALSE'
- _logger.info('Set HDF5_USE_FILE_LOCKING=%s', hdf5_file_locking)
- os.environ['HDF5_USE_FILE_LOCKING'] = hdf5_file_locking
+ hdf5_file_locking = "TRUE" if options.hdf5_file_locking else "FALSE"
+ _logger.info("Set HDF5_USE_FILE_LOCKING=%s", hdf5_file_locking)
+ os.environ["HDF5_USE_FILE_LOCKING"] = hdf5_file_locking
try:
# it should be loaded before h5py
@@ -115,8 +131,8 @@ def mainQt(options):
import h5py
import silx
- import silx.utils.files
from silx.gui import qt
+
# Make sure matplotlib is configured
# Needed for Debian 8: compatibility between Qt4/Qt5 and old matplotlib
import silx.gui.utils.matplotlib # noqa
@@ -129,7 +145,6 @@ def mainQt(options):
qt.QApplication.quit()
signal.signal(signal.SIGINT, sigintHandler)
- sys.excepthook = qt.exceptionHandler
timer = qt.QTimer()
timer.start(500)
@@ -137,28 +152,33 @@ def mainQt(options):
# catched
timer.timeout.connect(lambda: None)
- settings = qt.QSettings(qt.QSettings.IniFormat,
- qt.QSettings.UserScope,
- "silx",
- "silx-view",
- None)
+ settings = qt.QSettings(
+ qt.QSettings.IniFormat, qt.QSettings.UserScope, "silx", "silx-view", None
+ )
if options.fresh_preferences:
settings.clear()
window = createWindow(parent=None, settings=settings)
window.setAttribute(qt.Qt.WA_DeleteOnClose, True)
+ def exceptHook(type_, value, trace):
+ _logger.error("An error occured in silx view:")
+ _logger.error("%s %s %s", type_, value, "".join(traceback.format_tb(trace)))
+ try:
+ window.setErrorFromException(type_, value, trace)
+ except Exception:
+ pass
+
+ sys.excepthook = exceptHook
+
if options.use_opengl_plot:
# It have to be done after the settings (after the Viewer creation)
silx.config.DEFAULT_PLOT_BACKEND = "opengl"
- # NOTE: under Windows, cmd does not convert `*.tif` into existing files
- options.files = silx.utils.files.expand_filenames(options.files)
-
- for filename in options.files:
+ for url in parseutils.filenames_to_dataurls(options.files, options.slices):
# TODO: Would be nice to add a process widget and a cancel button
try:
- window.appendFile(filename)
+ window.appendFile(url.path())
except IOError as e:
_logger.error(e.args[0])
_logger.debug("Backtrace", exc_info=True)
@@ -182,5 +202,5 @@ def main(argv):
mainQt(options)
-if __name__ == '__main__':
+if __name__ == "__main__":
main(sys.argv)
diff --git a/src/silx/app/view/test/__init__.py b/src/silx/app/view/test/__init__.py
index 7790ee5..1d8207b 100644
--- a/src/silx/app/view/test/__init__.py
+++ b/src/silx/app/view/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/app/view/test/test_launcher.py b/src/silx/app/view/test/test_launcher.py
index 4f7aaa5..49b1032 100644
--- a/src/silx/app/view/test/test_launcher.py
+++ b/src/silx/app/view/test/test_launcher.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
@@ -85,23 +84,22 @@ class TestLauncher(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmpdir:
# Copy file to temporary dir to avoid import from current dir.
- script = os.path.join(tmpdir, 'launcher.py')
+ script = os.path.join(tmpdir, "launcher.py")
shutil.copyfile(filename, script)
command_line = [sys.executable, script] + list(args)
_logger.info("Execute: %s", " ".join(command_line))
- p = subprocess.Popen(command_line,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- env=env)
+ p = subprocess.Popen(
+ command_line, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
+ )
out, err = p.communicate()
_logger.info("Return code: %d", p.returncode)
try:
- out = out.decode('utf-8')
+ out = out.decode("utf-8")
except UnicodeError:
pass
try:
- err = err.decode('utf-8')
+ err = err.decode("utf-8")
except UnicodeError:
pass
diff --git a/src/silx/app/view/test/test_view.py b/src/silx/app/view/test/test_view.py
index e236e42..1eb588b 100644
--- a/src/silx/app/view/test/test_view.py
+++ b/src/silx/app/view/test/test_view.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
@@ -116,7 +115,6 @@ class TestAbout(TestCaseQt):
@pytest.mark.usefixtures("qapp")
@pytest.mark.usefixtures("data_class_attr")
class TestDataPanel(TestCaseQt):
-
def testConstruct(self):
widget = DataPanel()
self.qWaitForWindowExposed(widget)
@@ -170,7 +168,7 @@ class TestDataPanel(TestCaseQt):
self.assertIs(widget.getCustomNxdataItem(), data)
def testRemoveDatasetsFrom(self):
- f = h5py.File(self.data_h5, mode='r')
+ f = h5py.File(self.data_h5, mode="r")
try:
widget = DataPanel()
widget.setData(f["arrays/scalar"])
@@ -181,8 +179,8 @@ class TestDataPanel(TestCaseQt):
f.close()
def testReplaceDatasetsFrom(self):
- f = h5py.File(self.data_h5, mode='r')
- f2 = h5py.File(self.data2_h5, mode='r')
+ f = h5py.File(self.data_h5, mode="r")
+ f2 = h5py.File(self.data2_h5, mode="r")
try:
widget = DataPanel()
widget.setData(f["arrays/scalar"])
@@ -198,7 +196,6 @@ class TestDataPanel(TestCaseQt):
@pytest.mark.usefixtures("qapp")
@pytest.mark.usefixtures("data_class_attr")
class TestCustomNxdataWidget(TestCaseQt):
-
def testConstruct(self):
widget = CustomNxdataWidget()
self.qWaitForWindowExposed(widget)
@@ -251,7 +248,7 @@ class TestCustomNxdataWidget(TestCaseQt):
self.assertFalse(item.isValid())
def testRemoveDatasetsFrom(self):
- f = h5py.File(self.data_h5, mode='r')
+ f = h5py.File(self.data_h5, mode="r")
try:
widget = CustomNxdataWidget()
model = widget.model()
@@ -263,8 +260,8 @@ class TestCustomNxdataWidget(TestCaseQt):
f.close()
def testReplaceDatasetsFrom(self):
- f = h5py.File(self.data_h5, mode='r')
- f2 = h5py.File(self.data2_h5, mode='r')
+ f = h5py.File(self.data_h5, mode="r")
+ f2 = h5py.File(self.data2_h5, mode="r")
try:
widget = CustomNxdataWidget()
model = widget.model()
@@ -300,14 +297,18 @@ class TestCustomNxdataWidgetInteraction(TestCaseQt):
def testSelectedNxdata(self):
index = self.model.index(0, 0)
- self.selectionModel.setCurrentIndex(index, qt.QItemSelectionModel.ClearAndSelect)
+ self.selectionModel.setCurrentIndex(
+ index, qt.QItemSelectionModel.ClearAndSelect
+ )
nxdata = self.widget.selectedNxdata()
self.assertEqual(len(nxdata), 1)
self.assertIsNot(nxdata[0], None)
def testSelectedItems(self):
index = self.model.index(0, 0)
- self.selectionModel.setCurrentIndex(index, qt.QItemSelectionModel.ClearAndSelect)
+ self.selectionModel.setCurrentIndex(
+ index, qt.QItemSelectionModel.ClearAndSelect
+ )
items = self.widget.selectedItems()
self.assertEqual(len(items), 1)
self.assertIsNot(items[0], None)