summaryrefslogtreecommitdiff
path: root/silx/app
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2017-10-07 07:59:01 +0200
committerPicca Frédéric-Emmanuel <picca@debian.org>2017-10-07 07:59:01 +0200
commitbfa4dba15485b4192f8bbe13345e9658c97ecf76 (patch)
treefb9c6e5860881fbde902f7cbdbd41dc4a3a9fb5d /silx/app
parentf7bdc2acff3c13a6d632c28c4569690ab106eed7 (diff)
New upstream version 0.6.0+dfsg
Diffstat (limited to 'silx/app')
-rw-r--r--silx/app/convert.py283
-rw-r--r--silx/app/qtutils.py243
-rw-r--r--silx/app/test/__init__.py10
-rw-r--r--silx/app/test/test_convert.py182
-rw-r--r--silx/app/test/test_view.py33
-rw-r--r--silx/app/test_.py175
-rw-r--r--silx/app/view.py184
7 files changed, 989 insertions, 121 deletions
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/test/__init__.py b/silx/app/test/__init__.py
index 54241dc..0c22386 100644
--- a/silx/app/test/__init__.py
+++ b/silx/app/test/__init__.py
@@ -26,16 +26,14 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "30/03/2017"
-
-import logging
-import os
-import sys
import unittest
-
-_logger = logging.getLogger(__name__)
+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
index 774bc01..e55e4f3 100644
--- a/silx/app/test/test_view.py
+++ b/silx/app/test/test_view.py
@@ -26,13 +26,29 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "12/04/2017"
+__date__ = "29/09/2017"
import unittest
-from silx.gui.test.utils import TestCaseQt
-from .. import view
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):
@@ -64,6 +80,7 @@ class ViewerMock(object):
pass
+@unittest.skipUnless(with_qt, "Qt binding required for TestLauncher")
class TestLauncher(unittest.TestCase):
"""Test command line parsing"""
@@ -83,9 +100,9 @@ class TestLauncher(unittest.TestCase):
super(TestLauncher, cls).tearDownClass()
def testHelp(self):
+ # option -h must cause a raise SystemExit or a return 0
try:
result = view.main(["view", "--help"])
- self.assertNotEqual(result, 0)
except SystemExit as e:
result = e.args[0]
self.assertEqual(result, 0)
@@ -93,7 +110,6 @@ class TestLauncher(unittest.TestCase):
def testWrongOption(self):
try:
result = view.main(["view", "--foo"])
- self.assertNotEqual(result, 0)
except SystemExit as e:
result = e.args[0]
self.assertNotEqual(result, 0)
@@ -101,10 +117,9 @@ class TestLauncher(unittest.TestCase):
def testWrongFile(self):
try:
result = view.main(["view", "__file.not.found__"])
- self.assertNotEqual(result, 0)
except SystemExit as e:
result = e.args[0]
- self.assertNotEqual(result, 0)
+ self.assertEqual(result, 0)
def testFile(self):
# sys.executable is an existing readable file
@@ -118,8 +133,10 @@ class TestLauncher(unittest.TestCase):
class TestViewer(TestCaseQt):
"""Test for Viewer class"""
+ @unittest.skipUnless(with_qt, reason)
def testConstruct(self):
- widget = view.Viewer()
+ if view is not None:
+ widget = view.Viewer()
self.qWaitForWindowExposed(widget)
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
index 8fdabde..e8507f4 100644
--- a/silx/app/view.py
+++ b/silx/app/view.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016 European Synchrotron Radiation Facility
+# 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
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "12/04/2017"
+__date__ = "02/10/2017"
import sys
import os
@@ -33,30 +33,10 @@ import argparse
import logging
import collections
-
-logging.basicConfig()
_logger = logging.getLogger(__name__)
"""Module logger"""
-try:
- # it should be loaded before h5py
- import hdf5plugin # noqa
-except ImportError:
- hdf5plugin = None
-
-try:
- import h5py
- import silx.gui.hdf5
-except ImportError:
- h5py = None
-
-try:
- import fabio
-except ImportError:
- fabio = None
-
from silx.gui import qt
-from silx.gui.data.DataViewerFrame import DataViewerFrame
class Viewer(qt.QMainWindow):
@@ -71,6 +51,10 @@ class Viewer(qt.QMainWindow):
: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")
@@ -97,14 +81,15 @@ class Viewer(qt.QMainWindow):
self.setCentralWidget(main_panel)
- self.__treeview.selectionModel().selectionChanged.connect(self.displayData)
+ model = self.__treeview.selectionModel()
+ model.selectionChanged.connect(self.displayData)
+ self.__treeview.addContextMenuCallback(self.closeAndSyncCustomContextMenu)
- self.__treeview.addContextMenuCallback(self.customContextMenu)
- # lambda function will never be called cause we store it as weakref
- self.__treeview.addContextMenuCallback(lambda event: None)
- # you have to store it first
- self.__store_lambda = lambda event: self.closeAndSyncCustomContextMenu(event)
- self.__treeview.addContextMenuCallback(self.__store_lambda)
+ 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()
@@ -159,15 +144,17 @@ class Viewer(qt.QMainWindow):
extensions = collections.OrderedDict()
# expect h5py
- extensions["HDF5 files"] = "*.h5"
+ extensions["HDF5 files"] = "*.h5 *.hdf"
+ extensions["NeXus files"] = "*.nx *.nxs *.h5 *.hdf"
# no dependancy
- extensions["Spec files"] = "*.dat *.spec *.mca"
+ extensions["NeXus layout from spec files"] = "*.dat *.spec *.mca"
+ extensions["Numpy binary files"] = "*.npz *.npy"
# expect fabio
- extensions["EDF files"] = "*.edf"
- extensions["TIFF image files"] = "*.tif *.tiff"
- extensions["NumPy binary files"] = "*.npy"
- extensions["CBF files"] = "*.cbf"
- extensions["MarCCD image files"] = "*.mccd"
+ 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()))
@@ -180,48 +167,8 @@ class Viewer(qt.QMainWindow):
return dialog
def about(self):
- import silx._version
- message = """<p align="center"><b>Silx viewer</b>
- <br />
- <br />{silx_version}
- <br />
- <br /><a href="{project_url}">Upstream project on GitHub</a>
- </p>
- <p align="left">
- <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>
- <p>
- Copyright (C) <a href="{esrf_url}">European Synchrotron Radiation Facility</a>
- </p>
- """
- def format_optional_lib(name, isAvailable):
- 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
-
- optional_lib = []
- optional_lib.append(format_optional_lib("FabIO", fabio is not None))
- optional_lib.append(format_optional_lib("H5py", h5py is not None))
- optional_lib.append(format_optional_lib("hdf5plugin", hdf5plugin is not None))
-
- info = dict(
- 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)
- )
- qt.QMessageBox.about(self, "About Menu", message.format(**info))
+ from . import qtutils
+ qtutils.About.about(self, "Silx viewer")
def appendFile(self, filename):
self.__treeview.findHdf5TreeModel().appendFile(filename)
@@ -229,7 +176,7 @@ class Viewer(qt.QMainWindow):
def displayData(self):
"""Called to update the dataviewer with the selected data.
"""
- selected = list(self.__treeview.selectedH5Nodes())
+ selected = list(self.__treeview.selectedH5Nodes(ignoreBrokenLinks=False))
if len(selected) == 1:
# Update the viewer for a single selection
data = selected[0]
@@ -238,40 +185,20 @@ class Viewer(qt.QMainWindow):
def useAsyncLoad(self, useAsync):
self.__asyncload = useAsync
- def customContextMenu(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()
- menu = event.menu()
-
- hasDataset = False
- for obj in selectedObjects:
- if obj.ntype is h5py.Dataset:
- hasDataset = True
- break
-
- if len(menu.children()):
- menu.addSeparator()
-
- if hasDataset:
- action = qt.QAction("Do something on the datasets", event.source())
- menu.addAction(action)
-
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()
+ 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())
@@ -292,12 +219,43 @@ def main(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'files',
- type=argparse.FileType('rb'),
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\"."
@@ -309,15 +267,27 @@ def main(argv):
+ " 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 f in options.files:
- filename = f.name
- f.close()
- window.appendFile(filename)
+ 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_()