diff options
Diffstat (limited to 'silx/app')
-rw-r--r-- | silx/app/__init__.py | 29 | ||||
-rw-r--r-- | silx/app/convert.py | 283 | ||||
-rw-r--r-- | silx/app/qtutils.py | 243 | ||||
-rw-r--r-- | silx/app/setup.py | 40 | ||||
-rw-r--r-- | silx/app/test/__init__.py | 39 | ||||
-rw-r--r-- | silx/app/test/test_convert.py | 182 | ||||
-rw-r--r-- | silx/app/test/test_view.py | 152 | ||||
-rw-r--r-- | silx/app/test_.py | 175 | ||||
-rw-r--r-- | silx/app/view.py | 296 |
9 files changed, 1439 insertions, 0 deletions
diff --git a/silx/app/__init__.py b/silx/app/__init__.py new file mode 100644 index 0000000..9cbb8bb --- /dev/null +++ b/silx/app/__init__.py @@ -0,0 +1,29 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Application provided by the launcher""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "30/03/2017" diff --git a/silx/app/convert.py b/silx/app/convert.py new file mode 100644 index 0000000..a092ec1 --- /dev/null +++ b/silx/app/convert.py @@ -0,0 +1,283 @@ +# coding: utf-8 +# /*########################################################################## +# Copyright (C) 2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################*/ +"""Convert silx supported data files into HDF5 files""" + +import ast +import sys +import os +import argparse +from glob import glob +import logging +import numpy +import silx + + +__authors__ = ["P. Knobel"] +__license__ = "MIT" +__date__ = "12/09/2017" + + +_logger = logging.getLogger(__name__) +"""Module logger""" + + +def main(argv): + """ + Main function to launch the converter as an application + + :param argv: Command line arguments + :returns: exit status + """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + 'input_files', + nargs="+", + help='Input files (EDF, SPEC)') + parser.add_argument( + '-o', '--output-uri', + nargs="?", + help='Output file (HDF5). If omitted, it will be the ' + 'concatenated input file names, with a ".h5" suffix added.' + ' An URI can be provided to write the data into a specific ' + 'group in the output file: /path/to/file::/path/to/group') + parser.add_argument( + '-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)') + parser.add_argument( + '--no-root-group', + action="store_true", + help='This option disables the default behavior of creating a ' + 'root group (entry) for each file to be converted. When ' + 'merging multiple input files, this can cause conflicts ' + 'when datasets have the same name (see --overwrite-data).') + parser.add_argument( + '--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").') + parser.add_argument( + '--min-size', + type=int, + default=500, + help='Minimum number of elements required to be in a dataset to ' + 'apply compression or chunking (default 500).') + parser.add_argument( + '--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). ') + parser.add_argument( + '--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.') + + 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") + return ivalue + + parser.add_argument( + '--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.') + parser.add_argument( + '--shuffle', + action="store_true", + help='Enables the byte shuffle filter, may improve the compression ' + 'ratio for block oriented compressors like GZIP or LZF.') + parser.add_argument( + '--fletcher32', + action="store_true", + help='Adds a checksum to each chunk to detect data corruption.') + parser.add_argument( + '--debug', + action="store_true", + default=False, + help='Set logging system in debug mode') + + options = parser.parse_args(argv[1:]) + + # some shells (windows) don't interpret wildcard characters (*, ?, []) + old_input_list = list(options.input_files) + options.input_files = [] + for fname in old_input_list: + globbed_files = glob(fname) + if not globbed_files: + # no files found, keep the name as it is, to raise an error later + options.input_files += [fname] + else: + options.input_files += globbed_files + old_input_list = None + + if options.debug: + logging.root.setLevel(logging.DEBUG) + + # Import most of the things here to be sure to use the right logging level + try: + # it should be loaded before h5py + import hdf5plugin # noqa + except ImportError: + _logger.debug("Backtrace", exc_info=True) + hdf5plugin = None + + try: + import h5py + from silx.io.convert import write_to_h5 + except ImportError: + _logger.debug("Backtrace", exc_info=True) + h5py = None + write_to_h5 = None + + if h5py is None: + message = "Module 'h5py' is not installed but is mandatory."\ + + " You can install it using \"pip install h5py\"." + _logger.error(message) + return -1 + + if hdf5plugin is None: + message = "Module 'hdf5plugin' is not installed. It supports additional hdf5"\ + + " compressions. You can install it using \"pip install hdf5plugin\"." + _logger.debug(message) + + # Test that the output path is writeable + if options.output_uri is None: + input_basenames = [os.path.basename(name) for name in options.input_files] + output_name = ''.join(input_basenames) + ".h5" + _logger.info("No output file specified, using %s", output_name) + hdf5_path = "/" + else: + if "::" in options.output_uri: + output_name, hdf5_path = options.output_uri.split("::") + else: + output_name, hdf5_path = options.output_uri, "/" + + if os.path.isfile(output_name): + if options.mode == "w-": + _logger.error("Output file %s exists and mode is 'w-'" + " (write, file must not exist). Aborting.", + 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) + return -1 + elif options.mode == "w": + _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) + else: + if options.mode == "r+": + _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) + + # 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) + bad_input = True + if bad_input: + _logger.error("Aborting.") + return -1 + + # create_dataset special args + create_dataset_args = {} + if options.chunks is not None: + if options.chunks.lower() in ["auto", "true"]: + create_dataset_args["chunks"] = True + else: + try: + chunks = ast.literal_eval(options.chunks) + except (ValueError, SyntaxError): + _logger.error("Invalid --chunks argument %s", options.chunks) + return -1 + if not isinstance(chunks, (tuple, list)): + _logger.error("--chunks argument str does not evaluate to a tuple") + return -1 + else: + 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.") + create_dataset_args["chunks"] = chunks + + if options.compression is not None: + create_dataset_args["compression"] = options.compression + + if options.compression_opts is not None: + create_dataset_args["compression_opts"] = options.compression_opts + + if options.shuffle: + create_dataset_args["shuffle"] = True + + if options.fletcher32: + create_dataset_args["fletcher32"] = True + + with h5py.File(output_name, mode=options.mode) as h5f: + for input_name in options.input_files: + hdf5_path_for_file = hdf5_path + if not options.no_root_group: + hdf5_path_for_file = hdf5_path.rstrip("/") + "/" + os.path.basename(input_name) + write_to_h5(input_name, h5f, + h5path=hdf5_path_for_file, + overwrite_data=options.overwrite_data, + create_dataset_args=create_dataset_args, + min_size=options.min_size) + + # append the convert command to the creator attribute, for NeXus files + creator = h5f[hdf5_path_for_file].attrs.get("creator", b"").decode() + convert_command = " ".join(argv) + if convert_command not in creator: + h5f[hdf5_path_for_file].attrs["creator"] = \ + numpy.string_(creator + "; convert command: %s" % " ".join(argv)) + + return 0 diff --git a/silx/app/qtutils.py b/silx/app/qtutils.py new file mode 100644 index 0000000..4c29c84 --- /dev/null +++ b/silx/app/qtutils.py @@ -0,0 +1,243 @@ +# coding: utf-8 +# /*########################################################################## +# 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. +# +# ############################################################################*/ +"""Qt utils for Silx applications""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "22/09/2017" + +import sys + +try: + # it should be loaded before h5py + import hdf5plugin # noqa +except ImportError: + hdf5plugin = None + +try: + import h5py +except ImportError: + h5py = None + +try: + import fabio +except ImportError: + fabio = None + +from silx.gui import qt +from silx.gui import icons + +_LICENSE_TEMPLATE = """<p align="center"> +<b>Copyright (C) {year} European Synchrotron Radiation Facility</b> +</p> + +<p align="justify"> +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: +</p> + +<p align="justify"> +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +</p> + +<p align="justify"> +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. +</p> +""" + + +class About(qt.QDialog): + """ + Util dialog to display an common about box for all the silx GUIs. + """ + + def __init__(self, parent=None): + """ + :param files_: List of HDF5 or Spec files (pathes or + :class:`silx.io.spech5.SpecH5` or :class:`h5py.File` + instances) + """ + super(About, self).__init__(parent) + self.__createLayout() + self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed) + self.setModal(True) + self.setApplicationName(None) + + def __createLayout(self): + layout = qt.QVBoxLayout(self) + layout.setContentsMargins(24, 15, 24, 20) + layout.setSpacing(8) + + self.__label = qt.QLabel(self) + self.__label.setWordWrap(True) + flags = self.__label.textInteractionFlags() + flags = flags | qt.Qt.TextSelectableByKeyboard + flags = flags | qt.Qt.TextSelectableByMouse + self.__label.setTextInteractionFlags(flags) + self.__label.setOpenExternalLinks(True) + self.__label.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Preferred) + + licenseButton = qt.QPushButton(self) + licenseButton.setText("License...") + licenseButton.clicked.connect(self.__displayLicense) + licenseButton.setAutoDefault(False) + + self.__options = qt.QDialogButtonBox() + self.__options.addButton(licenseButton, qt.QDialogButtonBox.ActionRole) + okButton = self.__options.addButton(qt.QDialogButtonBox.Ok) + okButton.setDefault(True) + okButton.clicked.connect(self.accept) + + layout.addWidget(self.__label) + layout.addWidget(self.__options) + layout.setStretch(0, 100) + layout.setStretch(1, 0) + + def getHtmlLicense(self): + """Returns the text license in HTML format. + + :rtype: str + """ + from silx._version import __date__ as date + year = date.split("/")[2] + info = dict( + year=year + ) + textLicense = _LICENSE_TEMPLATE.format(**info) + return textLicense + + def __displayLicense(self): + """Displays the license used by silx.""" + text = self.getHtmlLicense() + licenseDialog = qt.QMessageBox(self) + licenseDialog.setWindowTitle("License") + licenseDialog.setText(text) + licenseDialog.exec_() + + def setApplicationName(self, name): + self.__applicationName = name + if name is None: + self.setWindowTitle("About") + else: + self.setWindowTitle("About %s" % name) + self.__updateText() + + @staticmethod + def __formatOptionalLibraries(name, isAvailable): + """Utils to format availability of features""" + if isAvailable: + template = '<b>%s</b> is <font color="green">installed</font>' + else: + template = '<b>%s</b> is <font color="red">not installed</font>' + return template % name + + def __updateText(self): + """Update the content of the dialog according to the settings.""" + import silx._version + + message = """<table> + <tr><td width="50%" align="center" valign="middle"> + <img src="{silx_image_path}" width="100" /> + </td><td width="50%" align="center" valign="middle"> + <b>{application_name}</b> + <br /> + <br />{silx_version} + <br /> + <br /><a href="{project_url}">Upstream project on GitHub</a> + </td></tr> + </table> + <dl> + <dt><b>Silx version</b></dt><dd>{silx_version}</dd> + <dt><b>Qt version</b></dt><dd>{qt_version}</dd> + <dt><b>Qt binding</b></dt><dd>{qt_binding}</dd> + <dt><b>Python version</b></dt><dd>{python_version}</dd> + <dt><b>Optional libraries</b></dt><dd>{optional_lib}</dd> + </dl> + <p> + Copyright (C) <a href="{esrf_url}">European Synchrotron Radiation Facility</a> + </p> + """ + optional_lib = [] + optional_lib.append(self.__formatOptionalLibraries("FabIO", fabio is not None)) + optional_lib.append(self.__formatOptionalLibraries("H5py", h5py is not None)) + optional_lib.append(self.__formatOptionalLibraries("hdf5plugin", hdf5plugin is not None)) + + # Access to the logo in SVG or PNG + logo = icons.getQFile("../logo/silx") + + info = dict( + application_name=self.__applicationName, + esrf_url="http://www.esrf.eu", + project_url="https://github.com/silx-kit/silx", + silx_version=silx._version.version, + qt_binding=qt.BINDING, + qt_version=qt.qVersion(), + python_version=sys.version.replace("\n", "<br />"), + optional_lib="<br />".join(optional_lib), + silx_image_path=logo.fileName() + ) + + self.__label.setText(message.format(**info)) + self.__updateSize() + + def __updateSize(self): + """Force the size to a QMessageBox like size.""" + screenSize = qt.QApplication.desktop().availableGeometry(qt.QCursor.pos()).size() + hardLimit = min(screenSize.width() - 480, 1000) + if screenSize.width() <= 1024: + hardLimit = screenSize.width() + softLimit = min(screenSize.width() / 2, 420) + + layoutMinimumSize = self.layout().totalMinimumSize() + width = layoutMinimumSize.width() + if width > softLimit: + width = softLimit + if width > hardLimit: + width = hardLimit + + height = layoutMinimumSize.height() + self.setFixedSize(width, height) + + @staticmethod + def about(parent, applicationName): + """Displays a silx about box with title and text text. + + :param qt.QWidget parent: The parent widget + :param str title: The title of the dialog + :param str applicationName: The content of the dialog + """ + dialog = About(parent) + dialog.setApplicationName(applicationName) + dialog.exec_() diff --git a/silx/app/setup.py b/silx/app/setup.py new file mode 100644 index 0000000..bf6f3af --- /dev/null +++ b/silx/app/setup.py @@ -0,0 +1,40 @@ +# coding: utf-8 +# /*########################################################################## +# Copyright (C) 2016 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. +# +# ############################################################################*/ + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "30/03/2017" + +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') + return config + + +if __name__ == "__main__": + from numpy.distutils.core import setup + setup(configuration=configuration) diff --git a/silx/app/test/__init__.py b/silx/app/test/__init__.py new file mode 100644 index 0000000..0c22386 --- /dev/null +++ b/silx/app/test/__init__.py @@ -0,0 +1,39 @@ +# coding: utf-8 +# /*########################################################################## +# +# 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. +# +# ###########################################################################*/ +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "30/03/2017" + +import unittest + +from . import test_view +from . import test_convert + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(test_view.suite()) + test_suite.addTest(test_convert.suite()) + return test_suite diff --git a/silx/app/test/test_convert.py b/silx/app/test/test_convert.py new file mode 100644 index 0000000..3215460 --- /dev/null +++ b/silx/app/test/test_convert.py @@ -0,0 +1,182 @@ +# coding: utf-8 +# /*########################################################################## +# +# 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. +# +# ###########################################################################*/ +"""Module testing silx.app.convert""" + +__authors__ = ["P. Knobel"] +__license__ = "MIT" +__date__ = "12/09/2017" + + +import os +import sys +import tempfile +import unittest +import io +import gc + +try: + import h5py +except ImportError: + h5py = None + +import silx +from .. import convert +from silx.test import utils + + + +# content of a spec file +sftext = """#F /tmp/sf.dat +#E 1455180875 +#D Thu Feb 11 09:54:35 2016 +#C imaging User = opid17 +#O0 Pslit HGap MRTSlit UP MRTSlit DOWN +#O1 Sslit1 VOff Sslit1 HOff Sslit1 VGap +#o0 pshg mrtu mrtd +#o2 ss1vo ss1ho ss1vg + +#J0 Seconds IA ion.mono Current +#J1 xbpmc2 idgap1 Inorm + +#S 1 ascan ss1vo -4.55687 -0.556875 40 0.2 +#D Thu Feb 11 09:55:20 2016 +#T 0.2 (Seconds) +#P0 180.005 -0.66875 0.87125 +#P1 14.74255 16.197579 12.238283 +#N 4 +#L MRTSlit UP second column 3rd_col +-1.23 5.89 8 +8.478100E+01 5 1.56 +3.14 2.73 -3.14 +1.2 2.3 3.4 + +#S 1 aaaaaa +#D Thu Feb 11 10:00:32 2016 +#@MCADEV 1 +#@MCA %16C +#@CHANN 3 0 2 1 +#@CALIB 1 2 3 +#N 3 +#L uno duo +1 2 +@A 0 1 2 +@A 10 9 8 +3 4 +@A 3.1 4 5 +@A 7 6 5 +5 6 +@A 6 7.7 8 +@A 4 3 2 +""" + + +class TestConvertCommand(unittest.TestCase): + """Test command line parsing""" + + def testHelp(self): + # option -h must cause a `raise SystemExit` or a `return 0` + try: + result = convert.main(["convert", "--help"]) + except SystemExit as e: + result = e.args[0] + self.assertEqual(result, 0) + + @unittest.skipUnless(h5py is None, + "h5py is installed, this test is specific to h5py missing") + @utils.test_logging(convert._logger.name, error=1) + def testH5pyNotInstalled(self): + result = convert.main(["convert", "foo.spec", "bar.edf"]) + # we explicitly return -1 if h5py is not imported + self.assertNotEqual(result, 0) + + @unittest.skipIf(h5py is None, "h5py is required to test convert") + def testWrongOption(self): + # presence of a wrong option must cause a SystemExit or a return + # with a non-zero status + try: + result = convert.main(["convert", "--foo"]) + except SystemExit as e: + result = e.args[0] + self.assertNotEqual(result, 0) + + @unittest.skipIf(h5py is None, "h5py is required to test convert") + @utils.test_logging(convert._logger.name, error=3) + # one error log per missing file + one "Aborted" error log + def testWrongFiles(self): + result = convert.main(["convert", "foo.spec", "bar.edf"]) + self.assertNotEqual(result, 0) + + @unittest.skipIf(h5py is None, "h5py is required to test convert") + def testFile(self): + # create a writable temp directory + tempdir = tempfile.mkdtemp() + + # write a temporary SPEC file + specname = os.path.join(tempdir, "input.dat") + with io.open(specname, "wb") as fd: + if sys.version < '3.0': + fd.write(sftext) + else: + fd.write(bytes(sftext, 'ascii')) + + # convert it + h5name = os.path.join(tempdir, "output.h5") + command_list = ["convert", "-m", "w", + "--no-root-group", specname, "-o", h5name] + result = convert.main(command_list) + + self.assertEqual(result, 0) + self.assertTrue(os.path.isfile(h5name)) + + with h5py.File(h5name, "r") as h5f: + title12 = h5f["/1.2/title"][()] + if sys.version > '3.0': + title12 = title12.decode() + self.assertEqual(title12, + "1 aaaaaa") + + creator = h5f.attrs.get("creator") + self.assertIsNotNone(creator, "No creator attribute in NXroot group") + creator = creator.decode() # make sure we can compare creator with native string + self.assertTrue(creator.startswith("silx %s" % silx.version)) + command = " ".join(command_list) + self.assertTrue(creator.endswith(command)) + + # delete input file + gc.collect() # necessary to free spec file on Windows + os.unlink(specname) + os.unlink(h5name) + os.rmdir(tempdir) + + +def suite(): + test_suite = unittest.TestSuite() + loader = unittest.defaultTestLoader.loadTestsFromTestCase + test_suite.addTest(loader(TestConvertCommand)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/app/test/test_view.py b/silx/app/test/test_view.py new file mode 100644 index 0000000..e55e4f3 --- /dev/null +++ b/silx/app/test/test_view.py @@ -0,0 +1,152 @@ +# coding: utf-8 +# /*########################################################################## +# +# 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. +# +# ###########################################################################*/ +"""Module testing silx.app.view""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "29/09/2017" + + +import unittest +import sys +import os + + +# TODO: factor this code with silx.gui.test +with_qt = False +if sys.platform.startswith('linux') and not os.environ.get('DISPLAY', ''): + reason = 'test disabled (DISPLAY env. variable not set)' + view = None + TestCaseQt = unittest.TestCase +elif os.environ.get('WITH_QT_TEST', 'True') == 'False': + reason = "test disabled (env. variable WITH_QT_TEST=False)" + view = None + TestCaseQt = unittest.TestCase +else: + from silx.gui.test.utils import TestCaseQt + from .. import view + with_qt = True + reason = "" + + +class QApplicationMock(object): + + def __init__(self, args): + pass + + def exec_(self): + return 0 + + def deleteLater(self): + pass + + +class ViewerMock(object): + + def __init__(self): + super(ViewerMock, self).__init__() + self.__class__._instance = self + self.appendFileCalls = [] + + def appendFile(self, filename): + self.appendFileCalls.append(filename) + + def resize(self, size): + pass + + def show(self): + pass + + +@unittest.skipUnless(with_qt, "Qt binding required for TestLauncher") +class TestLauncher(unittest.TestCase): + """Test command line parsing""" + + @classmethod + def setUpClass(cls): + super(TestLauncher, cls).setUpClass() + cls._Viewer = view.Viewer + view.Viewer = ViewerMock + cls._QApplication = view.qt.QApplication + view.qt.QApplication = QApplicationMock + + @classmethod + def tearDownClass(cls): + view.Viewer = cls._Viewer + view.qt.QApplication = cls._QApplication + cls._Viewer = None + super(TestLauncher, cls).tearDownClass() + + def testHelp(self): + # option -h must cause a raise SystemExit or a return 0 + try: + result = view.main(["view", "--help"]) + except SystemExit as e: + result = e.args[0] + self.assertEqual(result, 0) + + def testWrongOption(self): + try: + result = view.main(["view", "--foo"]) + except SystemExit as e: + result = e.args[0] + self.assertNotEqual(result, 0) + + def testWrongFile(self): + try: + result = view.main(["view", "__file.not.found__"]) + except SystemExit as e: + result = e.args[0] + self.assertEqual(result, 0) + + def testFile(self): + # sys.executable is an existing readable file + result = view.main(["view", sys.executable]) + self.assertEqual(result, 0) + viewer = ViewerMock._instance + self.assertEqual(viewer.appendFileCalls, [sys.executable]) + ViewerMock._instance = None + + +class TestViewer(TestCaseQt): + """Test for Viewer class""" + + @unittest.skipUnless(with_qt, reason) + def testConstruct(self): + if view is not None: + widget = view.Viewer() + self.qWaitForWindowExposed(widget) + + +def suite(): + test_suite = unittest.TestSuite() + loader = unittest.defaultTestLoader.loadTestsFromTestCase + test_suite.addTest(loader(TestViewer)) + test_suite.addTest(loader(TestLauncher)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/app/test_.py b/silx/app/test_.py new file mode 100644 index 0000000..7f95085 --- /dev/null +++ b/silx/app/test_.py @@ -0,0 +1,175 @@ +# coding: utf-8 +# /*########################################################################## +# Copyright (C) 2016 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. +# +# ############################################################################*/ +"""Launch unittests of the library""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "04/08/2017" + +import sys +import os +import argparse +import logging +import unittest + + +class StreamHandlerUnittestReady(logging.StreamHandler): + """The unittest class TestResult redefine sys.stdout/err to capture + stdout/err from tests and to display them only when a test fail. + + This class allow to use unittest stdout-capture by using the last sys.stdout + and not a cached one. + """ + + def emit(self, record): + """ + :type record: logging.LogRecord + """ + self.stream = sys.stderr + super(StreamHandlerUnittestReady, self).emit(record) + + def flush(self): + pass + + +def createBasicHandler(): + """Create the handler using the basic configuration""" + hdlr = StreamHandlerUnittestReady() + fs = logging.BASIC_FORMAT + dfs = None + fmt = logging.Formatter(fs, dfs) + hdlr.setFormatter(fmt) + return hdlr + + +# Use an handler compatible with unittests, else use_buffer is not working +for h in logging.root.handlers: + logging.root.removeHandler(h) +logging.root.addHandler(createBasicHandler()) +logging.captureWarnings(True) + +_logger = logging.getLogger(__name__) +"""Module logger""" + + +class TextTestResultWithSkipList(unittest.TextTestResult): + """Override default TextTestResult to display list of skipped tests at the + end + """ + + def printErrors(self): + unittest.TextTestResult.printErrors(self) + # Print skipped tests at the end + self.printErrorList("SKIPPED", self.skipped) + + +def main(argv): + """ + Main function to launch the unittests as an application + + :param argv: Command line arguments + :returns: exit status + """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-v", "--verbose", default=0, + action="count", dest="verbose", + help="Increase verbosity. Option -v prints additional " + + "INFO messages. Use -vv for full verbosity, " + + "including debug messages and test help strings.") + parser.add_argument("-x", "--no-gui", dest="gui", default=True, + action="store_false", + help="Disable the test of the graphical use interface") + parser.add_argument("-g", "--no-opengl", dest="opengl", default=True, + action="store_false", + help="Disable tests using OpenGL") + parser.add_argument("-o", "--no-opencl", dest="opencl", default=True, + action="store_false", + help="Disable the test of the OpenCL part") + parser.add_argument("-l", "--low-mem", dest="low_mem", default=False, + action="store_true", + help="Disable test with large memory consumption (>100Mbyte") + parser.add_argument("--qt-binding", dest="qt_binding", default=None, + help="Force using a Qt binding, from 'PyQt4', 'PyQt5', or 'PySide'") + + options = parser.parse_args(argv[1:]) + + test_verbosity = 1 + use_buffer = True + if options.verbose == 1: + logging.root.setLevel(logging.INFO) + _logger.info("Set log level: INFO") + test_verbosity = 2 + use_buffer = False + elif options.verbose > 1: + logging.root.setLevel(logging.DEBUG) + _logger.info("Set log level: DEBUG") + test_verbosity = 2 + use_buffer = False + + if not options.gui: + os.environ["WITH_QT_TEST"] = "False" + + if not options.opencl: + os.environ["SILX_OPENCL"] = "False" + + if not options.opengl: + os.environ["WITH_GL_TEST"] = "False" + + if options.low_mem: + os.environ["SILX_TEST_LOW_MEM"] = "True" + + if options.qt_binding: + binding = options.qt_binding.lower() + if binding == "pyqt4": + _logger.info("Force using PyQt4") + import PyQt4.QtCore # noqa + elif binding == "pyqt5": + _logger.info("Force using PyQt5") + import PyQt5.QtCore # noqa + elif binding == "pyside": + _logger.info("Force using PySide") + import PySide.QtCore # noqa + else: + raise ValueError("Qt binding '%s' is unknown" % options.qt_binding) + + # Run the tests + runnerArgs = {} + runnerArgs["verbosity"] = test_verbosity + runnerArgs["buffer"] = use_buffer + runner = unittest.TextTestRunner(**runnerArgs) + runner.resultclass = TextTestResultWithSkipList + + # Display the result when using CTRL-C + unittest.installHandler() + + import silx.test + test_suite = unittest.TestSuite() + test_suite.addTest(silx.test.suite()) + result = runner.run(test_suite) + + if result.wasSuccessful(): + exit_status = 0 + else: + exit_status = 1 + return exit_status diff --git a/silx/app/view.py b/silx/app/view.py new file mode 100644 index 0000000..e8507f4 --- /dev/null +++ b/silx/app/view.py @@ -0,0 +1,296 @@ +# coding: utf-8 +# /*########################################################################## +# 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. +# +# ############################################################################*/ +"""Browse a data file with a GUI""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "02/10/2017" + +import sys +import os +import argparse +import logging +import collections + +_logger = logging.getLogger(__name__) +"""Module logger""" + +from silx.gui import qt + + +class Viewer(qt.QMainWindow): + """ + This window allows to browse a data file like images or HDF5 and it's + content. + """ + + def __init__(self): + """ + :param files_: List of HDF5 or Spec files (pathes or + :class:`silx.io.spech5.SpecH5` or :class:`h5py.File` + instances) + """ + # Import it here to be sure to use the right logging level + import silx.gui.hdf5 + from silx.gui.data.DataViewerFrame import DataViewerFrame + + qt.QMainWindow.__init__(self) + self.setWindowTitle("Silx viewer") + + self.__asyncload = False + self.__dialogState = None + self.__treeview = silx.gui.hdf5.Hdf5TreeView(self) + """Silx HDF5 TreeView""" + + self.__dataViewer = DataViewerFrame(self) + vSpliter = qt.QSplitter(qt.Qt.Vertical) + vSpliter.addWidget(self.__dataViewer) + vSpliter.setSizes([10, 0]) + + spliter = qt.QSplitter(self) + spliter.addWidget(self.__treeview) + spliter.addWidget(vSpliter) + spliter.setStretchFactor(1, 1) + + main_panel = qt.QWidget(self) + layout = qt.QVBoxLayout() + layout.addWidget(spliter) + layout.setStretchFactor(spliter, 1) + main_panel.setLayout(layout) + + self.setCentralWidget(main_panel) + + model = self.__treeview.selectionModel() + model.selectionChanged.connect(self.displayData) + self.__treeview.addContextMenuCallback(self.closeAndSyncCustomContextMenu) + + treeModel = self.__treeview.findHdf5TreeModel() + columns = list(treeModel.COLUMN_IDS) + columns.remove(treeModel.DESCRIPTION_COLUMN) + columns.remove(treeModel.NODE_COLUMN) + self.__treeview.header().setSections(columns) + + self.createActions() + self.createMenus() + + def createActions(self): + action = qt.QAction("E&xit", self) + action.setShortcuts(qt.QKeySequence.Quit) + action.setStatusTip("Exit the application") + action.triggered.connect(self.close) + self._exitAction = action + + action = qt.QAction("&Open", self) + action.setStatusTip("Open a file") + action.triggered.connect(self.open) + self._openAction = action + + action = qt.QAction("&About", self) + action.setStatusTip("Show the application's About box") + action.triggered.connect(self.about) + self._aboutAction = action + + def createMenus(self): + fileMenu = self.menuBar().addMenu("&File") + fileMenu.addAction(self._openAction) + fileMenu.addSeparator() + fileMenu.addAction(self._exitAction) + helpMenu = self.menuBar().addMenu("&Help") + helpMenu.addAction(self._aboutAction) + + def open(self): + dialog = self.createFileDialog() + if self.__dialogState is None: + currentDirectory = os.getcwd() + dialog.setDirectory(currentDirectory) + else: + dialog.restoreState(self.__dialogState) + + result = dialog.exec_() + if not result: + return + + self.__dialogState = dialog.saveState() + + filenames = dialog.selectedFiles() + for filename in filenames: + self.appendFile(filename) + + def createFileDialog(self): + dialog = qt.QFileDialog(self) + dialog.setWindowTitle("Open") + dialog.setModal(True) + + extensions = collections.OrderedDict() + # expect h5py + extensions["HDF5 files"] = "*.h5 *.hdf" + extensions["NeXus files"] = "*.nx *.nxs *.h5 *.hdf" + # no dependancy + extensions["NeXus layout from spec files"] = "*.dat *.spec *.mca" + extensions["Numpy binary files"] = "*.npz *.npy" + # expect fabio + extensions["NeXus layout from raster images"] = "*.edf *.tif *.tiff *.cbf *.mccd" + extensions["NeXus layout from EDF files"] = "*.edf" + extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff" + extensions["NeXus layout from CBF files"] = "*.cbf" + extensions["NeXus layout from MarCCD image files"] = "*.mccd" + + filters = [] + filters.append("All supported files (%s)" % " ".join(extensions.values())) + for name, extension in extensions.items(): + filters.append("%s (%s)" % (name, extension)) + filters.append("All files (*)") + + dialog.setNameFilters(filters) + dialog.setFileMode(qt.QFileDialog.ExistingFiles) + return dialog + + def about(self): + from . import qtutils + qtutils.About.about(self, "Silx viewer") + + def appendFile(self, filename): + self.__treeview.findHdf5TreeModel().appendFile(filename) + + def displayData(self): + """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 + data = selected[0] + self.__dataViewer.setData(data) + + def useAsyncLoad(self, useAsync): + self.__asyncload = useAsync + + def closeAndSyncCustomContextMenu(self, event): + """Called to populate the context menu + + :param silx.gui.hdf5.Hdf5ContextMenuEvent event: Event + containing expected information to populate the context menu + """ + selectedObjects = event.source().selectedH5Nodes(ignoreBrokenLinks=False) + menu = event.menu() + + if len(menu.children()): + menu.addSeparator() + + # Import it here to be sure to use the right logging level + import h5py + for obj in selectedObjects: + if obj.ntype is h5py.File: + action = qt.QAction("Remove %s" % obj.local_filename, event.source()) + action.triggered.connect(lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(obj.h5py_object)) + menu.addAction(action) + action = qt.QAction("Synchronize %s" % obj.local_filename, event.source()) + action.triggered.connect(lambda: self.__treeview.findHdf5TreeModel().synchronizeH5pyObject(obj.h5py_object)) + menu.addAction(action) + + +def main(argv): + """ + Main function to launch the viewer as an application + + :param argv: Command line arguments + :returns: exit status + """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + 'files', + nargs=argparse.ZERO_OR_MORE, + help='Data file to show (h5 file, edf files, spec files)') + 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)') + + options = parser.parse_args(argv[1:]) + + if options.debug: + logging.root.setLevel(logging.DEBUG) + + # + # Import most of the things here to be sure to use the right logging level + # + + try: + # it should be loaded before h5py + import hdf5plugin # noqa + except ImportError: + _logger.debug("Backtrace", exc_info=True) + hdf5plugin = None + + try: + import h5py + except ImportError: + _logger.debug("Backtrace", exc_info=True) + h5py = None + + if h5py is None: + message = "Module 'h5py' is not installed but is mandatory."\ + + " You can install it using \"pip install h5py\"." + _logger.error(message) + return -1 + + if hdf5plugin is None: + message = "Module 'hdf5plugin' is not installed. It supports some hdf5"\ + + " compressions. You can install it using \"pip install hdf5plugin\"." + _logger.warning(message) + + # + # Run the application + # + + if options.use_opengl_plot: + from silx.gui.plot import PlotWidget + PlotWidget.setDefaultBackend("opengl") + + app = qt.QApplication([]) + qt.QLocale.setDefault(qt.QLocale.c()) + + sys.excepthook = qt.exceptionHandler + window = Viewer() + window.resize(qt.QSize(640, 480)) + + for filename in options.files: + try: + window.appendFile(filename) + except IOError as e: + _logger.error(e.args[0]) + _logger.debug("Backtrace", exc_info=True) + + window.show() + result = app.exec_() + # remove ending warnings relative to QTimer + app.deleteLater() + return result |