diff options
Diffstat (limited to 'silx/gui/utils')
-rw-r--r-- | silx/gui/utils/__init__.py | 29 | ||||
-rw-r--r-- | silx/gui/utils/concurrent.py | 103 | ||||
-rw-r--r-- | silx/gui/utils/image.py | 143 | ||||
-rw-r--r-- | silx/gui/utils/test/__init__.py | 48 | ||||
-rw-r--r-- | silx/gui/utils/test/test_async.py | 136 | ||||
-rw-r--r-- | silx/gui/utils/test/test_image.py | 90 | ||||
-rw-r--r-- | silx/gui/utils/testutils.py | 520 |
7 files changed, 0 insertions, 1069 deletions
diff --git a/silx/gui/utils/__init__.py b/silx/gui/utils/__init__.py deleted file mode 100644 index 51c4fac..0000000 --- a/silx/gui/utils/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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. -# -# ###########################################################################*/ -"""Miscellaneous helpers for Qt""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "09/03/2018" diff --git a/silx/gui/utils/concurrent.py b/silx/gui/utils/concurrent.py deleted file mode 100644 index 48fff91..0000000 --- a/silx/gui/utils/concurrent.py +++ /dev/null @@ -1,103 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module allows to run a function in Qt main thread from another thread -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "09/03/2018" - - -from silx.third_party.concurrent_futures import Future - -from .. import qt - - -class _QtExecutor(qt.QObject): - """Executor of tasks in Qt main thread""" - - __sigSubmit = qt.Signal(Future, object, tuple, dict) - """Signal used to run tasks.""" - - def __init__(self): - super(_QtExecutor, self).__init__(parent=None) - - # Makes sure the executor lives in the main thread - app = qt.QApplication.instance() - assert app is not None - mainThread = app.thread() - if self.thread() != mainThread: - self.moveToThread(mainThread) - - self.__sigSubmit.connect(self.__run) - - def submit(self, fn, *args, **kwargs): - """Submit fn(*args, **kwargs) to Qt main thread - - :param callable fn: Function to call in main thread - :return: Future object to retrieve result - :rtype: concurrent.future.Future - """ - future = Future() - self.__sigSubmit.emit(future, fn, args, kwargs) - return future - - def __run(self, future, fn, args, kwargs): - """Run task in Qt main thread - - :param concurrent.future.Future future: - :param callable fn: Function to run - :param tuple args: Arguments - :param dict kwargs: Keyword arguments - """ - if not future.set_running_or_notify_cancel(): - return - - try: - result = fn(*args, **kwargs) - except BaseException as e: - future.set_exception(e) - else: - future.set_result(result) - - -_executor = None -"""QObject running the tasks in main thread""" - - -def submitToQtMainThread(fn, *args, **kwargs): - """Run fn(args, kwargs) in Qt's main thread. - - If not called from the main thread, this is run asynchronously. - - :param callable fn: Function to call in main thread. - :return: A future object to retrieve the result - :rtype: concurrent.future.Future - """ - global _executor - if _executor is None: # Lazy-loading - _executor = _QtExecutor() - - return _executor.submit(fn, *args, **kwargs) diff --git a/silx/gui/utils/image.py b/silx/gui/utils/image.py deleted file mode 100644 index 3ac737f..0000000 --- a/silx/gui/utils/image.py +++ /dev/null @@ -1,143 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides conversions between numpy.ndarray and QImage - -- :func:`convertArrayToQImage` -- :func:`convertQImageToArray` -""" - -from __future__ import division - - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "04/09/2018" - - -import sys -import numpy -from numpy.lib.stride_tricks import as_strided as _as_strided - -from .. import qt - - -def convertArrayToQImage(array): - """Convert an array-like image to a QImage. - - The created QImage is using a copy of the array data. - - Limitation: Only RGB or RGBA images with 8 bits per channel are supported. - - :param array: Array-like image data of shape (height, width, channels) - Channels are expected to be either RGB or RGBA. - :type array: numpy.ndarray of uint8 - :return: Corresponding Qt image with RGB888 or ARGB32 format. - :rtype: QImage - """ - array = numpy.array(array, copy=False, order='C', dtype=numpy.uint8) - - if array.ndim != 3 or array.shape[2] not in (3, 4): - raise ValueError( - 'Image must be a 3D array with 3 or 4 channels per pixel') - - if array.shape[2] == 4: - format_ = qt.QImage.Format_ARGB32 - # RGBA -> ARGB + take care of endianness - if sys.byteorder == 'little': # RGBA -> BGRA - array = array[:, :, (2, 1, 0, 3)] - else: # big endian: RGBA -> ARGB - array = array[:, :, (3, 0, 1, 2)] - - array = numpy.array(array, order='C') # Make a contiguous array - - else: # array.shape[2] == 3 - format_ = qt.QImage.Format_RGB888 - - height, width, depth = array.shape - qimage = qt.QImage( - array.data, - width, - height, - array.strides[0], # bytesPerLine - format_) - - return qimage.copy() # Making a copy of the image and its data - - -def convertQImageToArray(image): - """Convert a QImage to a numpy array. - - If QImage format is not Format_RGB888, Format_RGBA8888 or Format_ARGB32, - it is first converted to one of this format depending on - the presence of an alpha channel. - - The created numpy array is using a copy of the QImage data. - - :param QImage image: The QImage to convert. - :return: The image array of RGB or RGBA channels of shape - (height, width, channels (3 or 4)) - :rtype: numpy.ndarray of uint8 - """ - rgba8888 = getattr(qt.QImage, 'Format_RGBA8888', None) # Only in Qt5 - - # Convert to supported format if needed - if image.format() not in (qt.QImage.Format_ARGB32, - qt.QImage.Format_RGB888, - rgba8888): - if image.hasAlphaChannel(): - image = image.convertToFormat( - rgba8888 if rgba8888 is not None else qt.QImage.Format_ARGB32) - else: - image = image.convertToFormat(qt.QImage.Format_RGB888) - - format_ = image.format() - channels = 3 if format_ == qt.QImage.Format_RGB888 else 4 - - ptr = image.bits() - if qt.BINDING not in ('PySide', 'PySide2'): - ptr.setsize(image.byteCount()) - if qt.BINDING == 'PyQt4' and sys.version_info[0] == 2: - ptr = ptr.asstring() - elif sys.version_info[0] == 3: # PySide with Python3 - ptr = ptr.tobytes() - - # Create an array view on QImage internal data - view = _as_strided( - numpy.frombuffer(ptr, dtype=numpy.uint8), - shape=(image.height(), image.width(), channels), - strides=(image.bytesPerLine(), channels, 1)) - - if format_ == qt.QImage.Format_ARGB32: - # Convert from ARGB to RGBA - # Not a byte-ordered format: do care about endianness - if sys.byteorder == 'little': # BGRA -> RGBA - view = view[:, :, (2, 1, 0, 3)] - else: # big endian: ARGB -> RGBA - view = view[:, :, (1, 2, 3, 0)] - - # Format_RGB888 and Format_RGBA8888 do not need reshuffling channels: - # They are byte-ordered and already in the right order - - return numpy.array(view, copy=True, order='C') diff --git a/silx/gui/utils/test/__init__.py b/silx/gui/utils/test/__init__.py deleted file mode 100644 index 9e50170..0000000 --- a/silx/gui/utils/test/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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. -# -# ###########################################################################*/ -"""silx.gui.utils tests""" - - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "24/04/2018" - - -import unittest - -from . import test_async -from . import test_image - - -def suite(): - """Test suite for module silx.image.test""" - test_suite = unittest.TestSuite() - test_suite.addTest(test_async.suite()) - test_suite.addTest(test_image.suite()) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/utils/test/test_async.py b/silx/gui/utils/test/test_async.py deleted file mode 100644 index dabfb3c..0000000 --- a/silx/gui/utils/test/test_async.py +++ /dev/null @@ -1,136 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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. -# -# ###########################################################################*/ -"""Test of async module.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "09/03/2018" - - -import threading -import unittest - - -from silx.third_party.concurrent_futures import wait -from silx.gui import qt -from silx.gui.utils.testutils import TestCaseQt - -from silx.gui.utils import concurrent - - -class TestSubmitToQtThread(TestCaseQt): - """Test submission of tasks to Qt main thread""" - - def setUp(self): - # Reset executor to test lazy-loading in different conditions - concurrent._executor = None - super(TestSubmitToQtThread, self).setUp() - - def _task(self, value1, value2): - return value1, value2 - - def _taskWithException(self, *args, **kwargs): - raise RuntimeError('task exception') - - def testFromMainThread(self): - """Call submitToQtMainThread from the main thread""" - value1, value2 = 0, 1 - future = concurrent.submitToQtMainThread(self._task, value1, value2=value2) - self.assertTrue(future.done()) - self.assertEqual(future.result(1), (value1, value2)) - self.assertIsNone(future.exception(1)) - - future = concurrent.submitToQtMainThread(self._taskWithException) - self.assertTrue(future.done()) - with self.assertRaises(RuntimeError): - future.result(1) - self.assertIsInstance(future.exception(1), RuntimeError) - - def _threadedTest(self): - """Function run in a thread for the tests""" - value1, value2 = 0, 1 - future = concurrent.submitToQtMainThread(self._task, value1, value2=value2) - - wait([future], 3) - - self.assertTrue(future.done()) - self.assertEqual(future.result(1), (value1, value2)) - self.assertIsNone(future.exception(1)) - - future = concurrent.submitToQtMainThread(self._taskWithException) - - wait([future], 3) - - self.assertTrue(future.done()) - with self.assertRaises(RuntimeError): - future.result(1) - self.assertIsInstance(future.exception(1), RuntimeError) - - def testFromPythonThread(self): - """Call submitToQtMainThread from a Python thread""" - thread = threading.Thread(target=self._threadedTest) - thread.start() - for i in range(100): # Loop over for 10 seconds - self.qapp.processEvents() - thread.join(0.1) - if not thread.is_alive(): - break - else: - self.fail(('Thread task still running')) - - def testFromQtThread(self): - """Call submitToQtMainThread from a Qt thread pool""" - class Runner(qt.QRunnable): - def __init__(self, fn): - super(Runner, self).__init__() - self._fn = fn - - def run(self): - self._fn() - - def autoDelete(self): - return True - - threadPool = qt.silxGlobalThreadPool() - runner = Runner(self._threadedTest) - threadPool.start(runner) - for i in range(100): # Loop over for 10 seconds - self.qapp.processEvents() - done = threadPool.waitForDone(100) - if done: - break - else: - self.fail('Thread pool task still running') - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase( - TestSubmitToQtThread)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/utils/test/test_image.py b/silx/gui/utils/test/test_image.py deleted file mode 100644 index cda7d95..0000000 --- a/silx/gui/utils/test/test_image.py +++ /dev/null @@ -1,90 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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. -# -# ###########################################################################*/ -"""Test of utils module.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "16/01/2017" - -import numpy -import unittest - -from silx.gui import qt -from silx.utils.testutils import ParametricTestCase -from silx.gui.utils.testutils import TestCaseQt -from silx.gui.utils.image import convertArrayToQImage, convertQImageToArray - - -class TestQImageConversion(TestCaseQt, ParametricTestCase): - """Tests conversion of QImage to/from numpy array.""" - - def testConvertArrayToQImage(self): - """Test conversion of numpy array to QImage""" - for format_, channels in [('Format_RGB888', 3), - ('Format_ARGB32', 4)]: - with self.subTest(format_): - image = numpy.arange( - 3*3*channels, dtype=numpy.uint8).reshape(3, 3, channels) - qimage = convertArrayToQImage(image) - - self.assertEqual(qimage.height(), image.shape[0]) - self.assertEqual(qimage.width(), image.shape[1]) - self.assertEqual(qimage.format(), getattr(qt.QImage, format_)) - - for row in range(3): - for col in range(3): - # Qrgb has no alpha channel, not compared - # Qt uses x,y while array is row,col... - self.assertEqual(qt.QColor(qimage.pixel(col, row)), - qt.QColor(*image[row, col, :3])) - - - def testConvertQImageToArray(self): - """Test conversion of QImage to numpy array""" - for format_, channels in [ - ('Format_RGB888', 3), # Native support - ('Format_ARGB32', 4), # Native support - ('Format_RGB32', 3)]: # Conversion to RGB - with self.subTest(format_): - color = numpy.arange(channels) # RGB(A) values - qimage = qt.QImage(3, 3, getattr(qt.QImage, format_)) - qimage.fill(qt.QColor(*color)) - image = convertQImageToArray(qimage) - - self.assertEqual(qimage.height(), image.shape[0]) - self.assertEqual(qimage.width(), image.shape[1]) - self.assertEqual(image.shape[2], len(color)) - self.assertTrue(numpy.all(numpy.equal(image, color))) - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase( - TestQImageConversion)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/utils/testutils.py b/silx/gui/utils/testutils.py deleted file mode 100644 index 35085fc..0000000 --- a/silx/gui/utils/testutils.py +++ /dev/null @@ -1,520 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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. -# -# ###########################################################################*/ -"""Helper class to write Qt widget unittests.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "05/10/2018" - - -import gc -import logging -import unittest -import time -import functools -import sys -import os - -_logger = logging.getLogger(__name__) - -from silx.gui import qt - -if qt.BINDING == 'PySide': - from PySide.QtTest import QTest -elif qt.BINDING == 'PySide2': - from PySide2.QtTest import QTest -elif qt.BINDING == 'PyQt5': - from PyQt5.QtTest import QTest -elif qt.BINDING == 'PyQt4': - from PyQt4.QtTest import QTest -else: - raise ImportError('Unsupported Qt bindings') - -# Qt4/Qt5 compatibility wrapper -if qt.BINDING in ('PySide', 'PyQt4'): - _logger.info("QTest.qWaitForWindowExposed not available," + - "using QTest.qWaitForWindowShown instead.") - - def qWaitForWindowExposed(window, timeout=None): - """Mimic QTest.qWaitForWindowExposed for Qt4.""" - QTest.qWaitForWindowShown(window) - return True -else: - qWaitForWindowExposed = QTest.qWaitForWindowExposed - - -def qWaitForWindowExposedAndActivate(window, timeout=None): - """Waits until the window is shown in the screen. - - It also activates the window and raises it. - - See QTest.qWaitForWindowExposed for details. - """ - if timeout is None: - result = qWaitForWindowExposed(window) - else: - result = qWaitForWindowExposed(window, timeout) - - if result: - # Makes sure window is active and on top - window.activateWindow() - window.raise_() - - return result - - -# Placeholder for QApplication -_qapp = None - - -class TestCaseQt(unittest.TestCase): - """Base class to write test for Qt stuff. - - It creates a QApplication before running the tests. - WARNING: The QApplication is shared by all tests, which might have side - effects. - - After each test, this class is checking for widgets remaining alive. - To allow some widgets to remain alive at the end of a test, set the - allowedLeakingWidgets attribute to the number of widgets that can remain - alive at the end of the test. - With PySide, this test is not run for now as it seems PySide - is leaking widgets internally. - - All keyboard and mouse event simulation methods call qWait(20) after - simulating the event (as QTest does on Mac OSX). - This was introduced to fix issues with continuous integration tests - running with Xvfb on Linux. - """ - - DEFAULT_TIMEOUT_WAIT = 100 - """Default timeout for qWait""" - - TIMEOUT_WAIT = 0 - """Extra timeout in millisecond to add to qSleep, qWait and - qWaitForWindowExposed. - - Intended purpose is for debugging, to add extra time to waits in order to - allow to view the tested widgets. - """ - - @classmethod - def exceptionHandler(cls, exceptionClass, exception, stack): - import traceback - message = (''.join(traceback.format_tb(stack))) - template = 'Traceback (most recent call last):\n{2}{0}: {1}' - message = template.format(exceptionClass.__name__, exception, message) - cls._exceptions.append(message) - - @classmethod - def setUpClass(cls): - """Makes sure Qt is inited""" - cls._oldExceptionHook = sys.excepthook - sys.excepthook = cls.exceptionHandler - - global _qapp - if _qapp is None: - # Makes sure a QApplication exists and do it once for all - _qapp = qt.QApplication.instance() or qt.QApplication([]) - - # Makes sure QDesktopWidget is init - # Otherwise it happens randomly during the tests - cls._desktopWidget = _qapp.desktop() - _qapp.processEvents() - - @classmethod - def tearDownClass(cls): - sys.excepthook = cls._oldExceptionHook - - def setUp(self): - """Get the list of existing widgets.""" - self.allowedLeakingWidgets = 0 - self.__previousWidgets = self.qapp.allWidgets() - self.__class__._exceptions = [] - - def _currentTestSucceeded(self): - if hasattr(self, '_outcome'): - # For Python >= 3.4 - result = self.defaultTestResult() # these 2 methods have no side effects - self._feedErrorsToResult(result, self._outcome.errors) - else: - # For Python < 3.4 - result = getattr(self, '_outcomeForDoCleanups', self._resultForDoCleanups) - - skipped = self.id() in [case.id() for case, _ in result.skipped] - error = self.id() in [case.id() for case, _ in result.errors] - failure = self.id() in [case.id() for case, _ in result.failures] - return not error and not failure and not skipped - - def _checkForUnreleasedWidgets(self): - """Test fixture checking that no more widgets exists.""" - gc.collect() - - widgets = [widget for widget in self.qapp.allWidgets() - if widget not in self.__previousWidgets] - del self.__previousWidgets - - if qt.BINDING in ('PySide', 'PySide2'): - return # Do not test for leaking widgets with PySide - - allowedLeakingWidgets = self.allowedLeakingWidgets - self.allowedLeakingWidgets = 0 - - if widgets and len(widgets) <= allowedLeakingWidgets: - _logger.info( - '%s: %d remaining widgets after test' % (self.id(), - len(widgets))) - - if len(widgets) > allowedLeakingWidgets: - raise RuntimeError( - "Test ended with widgets alive: %s" % str(widgets)) - - def tearDown(self): - if len(self.__class__._exceptions) > 0: - messages = "\n".join(self.__class__._exceptions) - raise AssertionError("Exception occured in Qt thread:\n" + messages) - - if self._currentTestSucceeded(): - self._checkForUnreleasedWidgets() - - @property - def qapp(self): - """The QApplication currently running.""" - return qt.QApplication.instance() - - # Proxy to QTest - - Press = QTest.Press - """Key press action code""" - - Release = QTest.Release - """Key release action code""" - - Click = QTest.Click - """Key click action code""" - - QTest = property(lambda self: QTest, - doc="""The Qt QTest class from the used Qt binding.""") - - def keyClick(self, widget, key, modifier=qt.Qt.NoModifier, delay=-1): - """Simulate clicking a key. - - See QTest.keyClick for details. - """ - QTest.keyClick(widget, key, modifier, delay) - self.qWait(20) - - def keyClicks(self, widget, sequence, modifier=qt.Qt.NoModifier, delay=-1): - """Simulate clicking a sequence of keys. - - See QTest.keyClick for details. - """ - QTest.keyClicks(widget, sequence, modifier, delay) - self.qWait(20) - - def keyEvent(self, action, widget, key, - modifier=qt.Qt.NoModifier, delay=-1): - """Sends a Qt key event. - - See QTest.keyEvent for details. - """ - QTest.keyEvent(action, widget, key, modifier, delay) - self.qWait(20) - - def keyPress(self, widget, key, modifier=qt.Qt.NoModifier, delay=-1): - """Sends a Qt key press event. - - See QTest.keyPress for details. - """ - QTest.keyPress(widget, key, modifier, delay) - self.qWait(20) - - def keyRelease(self, widget, key, modifier=qt.Qt.NoModifier, delay=-1): - """Sends a Qt key release event. - - See QTest.keyRelease for details. - """ - QTest.keyRelease(widget, key, modifier, delay) - self.qWait(20) - - def mouseClick(self, widget, button, modifier=None, pos=None, delay=-1): - """Simulate clicking a mouse button. - - See QTest.mouseClick for details. - """ - if modifier is None: - modifier = qt.Qt.KeyboardModifiers() - pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint() - QTest.mouseClick(widget, button, modifier, pos, delay) - self.qWait(20) - - def mouseDClick(self, widget, button, modifier=None, pos=None, delay=-1): - """Simulate double clicking a mouse button. - - See QTest.mouseDClick for details. - """ - if modifier is None: - modifier = qt.Qt.KeyboardModifiers() - pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint() - QTest.mouseDClick(widget, button, modifier, pos, delay) - self.qWait(20) - - def mouseMove(self, widget, pos=None, delay=-1): - """Simulate moving the mouse. - - See QTest.mouseMove for details. - """ - pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint() - QTest.mouseMove(widget, pos, delay) - self.qWait(20) - - def mousePress(self, widget, button, modifier=None, pos=None, delay=-1): - """Simulate pressing a mouse button. - - See QTest.mousePress for details. - """ - if modifier is None: - modifier = qt.Qt.KeyboardModifiers() - pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint() - QTest.mousePress(widget, button, modifier, pos, delay) - self.qWait(20) - - def mouseRelease(self, widget, button, modifier=None, pos=None, delay=-1): - """Simulate releasing a mouse button. - - See QTest.mouseRelease for details. - """ - if modifier is None: - modifier = qt.Qt.KeyboardModifiers() - pos = qt.QPoint(pos[0], pos[1]) if pos is not None else qt.QPoint() - QTest.mouseRelease(widget, button, modifier, pos, delay) - self.qWait(20) - - def qSleep(self, ms): - """Sleep for ms milliseconds, blocking the execution of the test. - - See QTest.qSleep for details. - """ - QTest.qSleep(ms + self.TIMEOUT_WAIT) - - @classmethod - def qWait(cls, ms=None): - """Waits for ms milliseconds, events will be processed. - - See QTest.qWait for details. - """ - if ms is None: - ms = cls.DEFAULT_TIMEOUT_WAIT - - if qt.BINDING in ('PySide', 'PySide2'): - # PySide has no qWait, provide a replacement - timeout = int(ms) - endTimeMS = int(time.time() * 1000) + timeout - while timeout > 0: - _qapp.processEvents(qt.QEventLoop.AllEvents, - maxtime=timeout) - timeout = endTimeMS - int(time.time() * 1000) - else: - QTest.qWait(ms + cls.TIMEOUT_WAIT) - - def qWaitForWindowExposed(self, window, timeout=None): - """Waits until the window is shown in the screen. - - See QTest.qWaitForWindowExposed for details. - """ - result = qWaitForWindowExposedAndActivate(window, timeout) - - if self.TIMEOUT_WAIT: - QTest.qWait(self.TIMEOUT_WAIT) - - return result - - _qobject_destroyed = False - - @classmethod - def _aboutToDestroy(cls): - cls._qobject_destroyed = True - - @classmethod - def qWaitForDestroy(cls, ref): - """ - Wait for Qt object destruction. - - Use a weakref as parameter to avoid any strong references to the - object. - - It have to be used as following. Removing the reference to the object - before calling the function looks to be expected, else - :meth:`deleteLater` will not work. - - .. code-block:: python - - ref = weakref.ref(self.obj) - self.obj = None - self.qWaitForDestroy(ref) - - :param weakref ref: A weakref to an object to avoid any reference - :return: True if the object was destroyed - :rtype: bool - """ - cls._qobject_destroyed = False - if qt.BINDING == 'PyQt4': - # Without this, QWidget will be still alive on PyQt4 - # (at least on Windows Python 2.7) - # If it is not skipped on PySide, silx.gui.dialog tests will - # segfault (at least on Windows Python 2.7) - import gc - gc.collect() - qobject = ref() - if qobject is None: - return True - qobject.destroyed.connect(cls._aboutToDestroy) - qobject.deleteLater() - qobject = None - for _ in range(10): - if cls._qobject_destroyed: - break - cls.qWait(10) - else: - _logger.debug("Object was not destroyed") - - return ref() is None - - def logScreenShot(self, level=logging.ERROR): - """Take a screenshot and log it into the logging system if the - logger is enabled for the expected level. - - The screenshot is stored in the directory "./build/test-debug", and - the logging system only log the path to this file. - - :param level: Logging level - """ - if not _logger.isEnabledFor(level): - return - basedir = os.path.abspath(os.path.join("build", "test-debug")) - if not os.path.exists(basedir): - os.makedirs(basedir) - filename = "Screenshot_%s.png" % self.id() - filename = os.path.join(basedir, filename) - - if not hasattr(self.qapp, "primaryScreen"): - # Qt4 - winId = qt.QApplication.desktop().winId() - pixmap = qt.QPixmap.grabWindow(winId) - else: - # Qt5 - screen = self.qapp.primaryScreen() - pixmap = screen.grabWindow(0) - pixmap.save(filename) - _logger.log(level, "Screenshot saved at %s", filename) - - -class SignalListener(object): - """Util to listen a Qt event and store parameters - """ - - def __init__(self): - self.__calls = [] - - def __call__(self, *args, **kargs): - self.__calls.append((args, kargs)) - - def clear(self): - """Clear stored data""" - self.__calls = [] - - def callCount(self): - """ - Returns how many times the listener was called. - - :rtype: int - """ - return len(self.__calls) - - def arguments(self, callIndex=None, argumentIndex=None): - """Returns positional arguments optionally filtered by call count id - or argument index. - - :param int callIndex: Index of the called data - :param int argumentIndex: Index of the positional argument. - """ - if callIndex is not None: - result = self.__calls[callIndex][0] - if argumentIndex is not None: - result = result[argumentIndex] - else: - result = [x[0] for x in self.__calls] - if argumentIndex is not None: - result = [x[argumentIndex] for x in result] - return result - - def karguments(self, callIndex=None, argumentName=None): - """Returns positional arguments optionally filtered by call count id - or name of the keyword argument. - - :param int callIndex: Index of the called data - :param int argumentName: Name of the keyword argument. - """ - if callIndex is not None: - result = self.__calls[callIndex][1] - if argumentName is not None: - result = result[argumentName] - else: - result = [x[1] for x in self.__calls] - if argumentName is not None: - result = [x[argumentName] for x in result] - return result - - def partial(self, *args, **kargs): - """Returns a new partial object which when called will behave like this - listener called with the positional arguments args and keyword - arguments keywords. If more arguments are supplied to the call, they - are appended to args. If additional keyword arguments are supplied, - they extend and override keywords. - """ - return functools.partial(self, *args, **kargs) - - -def getQToolButtonFromAction(action): - """Return a QToolButton corresponding to a QAction. - - :param QAction action: The QAction from which to get QToolButton. - :return: A QToolButton associated to action or None. - """ - for widget in action.associatedWidgets(): - if isinstance(widget, qt.QToolButton): - return widget - return None - - -def findChildren(parent, kind, name=None): - if qt.BINDING in ("PySide", "PySide2") and name is not None: - result = [] - for obj in parent.findChildren(kind): - if obj.objectName() == name: - result.append(obj) - return result - else: - return parent.findChildren(kind, name=name) |