Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ install:

script:
- python setup.py develop
- catchsegv xvfb-run coverage run --source=pytestqt setup.py test
# change screen size and depth in attempt to fix waitActive test (#160)
- catchsegv xvfb-run --server-args="-screen 0 640x480x24" coverage run --source=pytestqt setup.py test
- tox -e lint

after_success:
Expand Down
17 changes: 16 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
2.1
---

- ``waitSignal`` and ``waitSignals`` now provide much more detailed messages
when expected signals are not emitted. Many thanks to `@MShekow`_ for the PR
(`#153`_).

- ``qtbot`` fixture now can capture Qt virtual method exceptions in a block using
``capture_exceptions`` (`#154`_). Thanks to `@fogo`_ for the PR.
``captureExceptions`` (`#154`_). Thanks to `@fogo`_ for the PR.

- New `qtbot.waitActive`_ and `qtbot.waitExposed`_ methods for PyQt5.
Thanks `@The-Compiler`_ for the request (`#158`_).

- ``SignalTimeoutError`` has been renamed to ``TimeoutError``. ``SignalTimeoutError`` is kept as
a backward compatibility alias.

.. _qtbot.waitActive: http://pytest-qt.readthedocs.io/en/latest/reference.html#pytestqt.qtbot.QtBot.waitActive
.. _qtbot.waitExposed: http://pytest-qt.readthedocs.io/en/latest/reference.html#pytestqt.qtbot.QtBot.waitExposed

.. _#153: https://github.com/pytest-dev/pytest-qt/issues/153
.. _#154: https://github.com/pytest-dev/pytest-qt/issues/154
.. _#158: https://github.com/pytest-dev/pytest-qt/issues/158

2.0
---
Expand Down
11 changes: 8 additions & 3 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ QtBot
.. module:: pytestqt.qtbot
.. autoclass:: QtBot

TimeoutError
------------

.. autoclass:: TimeoutError

SignalBlocker
-------------

Expand All @@ -17,13 +22,13 @@ MultiSignalBlocker

.. autoclass:: MultiSignalBlocker

SignalTimeoutError
SignalEmittedError
------------------

.. autoclass:: SignalTimeoutError
.. autoclass:: SignalEmittedError

Record
------

.. module:: pytestqt.logging
.. autoclass:: Record
.. autoclass:: Record
17 changes: 17 additions & 0 deletions pytestqt/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,20 @@ def _is_exception_capture_enabled(item):
disabled = item.get_marker('qt_no_exception_capture') or \
item.config.getini('qt_no_exception_capture')
return not disabled


class TimeoutError(Exception):
"""
.. versionadded:: 2.1

Exception thrown by :class:`pytestqt.qtbot.QtBot` methods.

.. note::
In versions prior to ``2.1``, this exception was called ``SignalTimeoutError``.
An alias is kept for backward compatibility.
"""
pass


# backward compatibility alias
SignalTimeoutError = TimeoutError
4 changes: 2 additions & 2 deletions pytestqt/plugin.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import pytest

from pytestqt.exceptions import capture_exceptions, format_captured_exceptions, \
_is_exception_capture_enabled, _QtExceptionCaptureManager
_is_exception_capture_enabled, _QtExceptionCaptureManager, SignalTimeoutError
from pytestqt.logging import QtLoggingPlugin, _QtMessageCapture, Record
from pytestqt.qt_compat import qt_api
from pytestqt.qtbot import QtBot, _close_widgets
from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker, SignalTimeoutError
from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker

# classes/functions imported here just for backward compatibility before we
# split the implementation of this file in several modules
Expand Down
2 changes: 2 additions & 0 deletions pytestqt/qt_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def _import_module(module_name):
self.Property = QtCore.Property
self.QApplication = QtGui.QApplication
self.QWidget = QtGui.QWidget
self.QLineEdit = QtGui.QLineEdit
self.QStringListModel = QtGui.QStringListModel
self.qInstallMsgHandler = QtCore.qInstallMsgHandler

Expand Down Expand Up @@ -136,6 +137,7 @@ def make_variant(value=None):
_QtWidgets = _import_module('QtWidgets')
self.QApplication = _QtWidgets.QApplication
self.QWidget = _QtWidgets.QWidget
self.QLineEdit = _QtWidgets.QLineEdit
self.qInstallMessageHandler = QtCore.qInstallMessageHandler

self.QStringListModel = QtCore.QStringListModel
Expand Down
138 changes: 121 additions & 17 deletions pytestqt/qtbot.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import functools
import contextlib
import functools
import weakref

from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker, SignalTimeoutError, SignalEmittedSpy
from pytestqt.exceptions import SignalTimeoutError, TimeoutError
from pytestqt.qt_compat import qt_api
from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker, SignalEmittedSpy, SignalEmittedError


def _parse_ini_boolean(value):
Expand All @@ -26,6 +27,9 @@ class QtBot(object):
**Widgets**

.. automethod:: addWidget
.. automethod:: captureExceptions
.. automethod:: waitActive
.. automethod:: waitExposed
.. automethod:: waitForWindowShown
.. automethod:: stopForInteraction
.. automethod:: wait
Expand Down Expand Up @@ -141,6 +145,66 @@ def addWidget(self, widget):

add_widget = addWidget # pep-8 alias

def waitActive(self, widget, timeout=1000):
"""
Context manager that waits for ``timeout`` milliseconds or until the window is active.
If window is not exposed within ``timeout`` milliseconds, raise ``TimeoutError``.

This is mainly useful for asynchronous systems like X11, where a window will be mapped to screen
some time after being asked to show itself on the screen.

.. code-block:: python

with qtbot.waitActive(widget, timeout=500):
show_action()

:param QWidget widget:
Widget to wait for.

:param int|None timeout:
How many milliseconds to wait for.

.. note::
This function is only available in PyQt5, raising a ``RuntimeError`` if called from
``PyQt4`` or ``PySide``.

.. note:: This method is also available as ``wait_active`` (pep-8 alias)
"""
__tracebackhide__ = True
return _WaitWidgetContextManager('qWaitForWindowActive', 'activated', widget, timeout)

wait_active = waitActive # pep-8 alias

def waitExposed(self, widget, timeout=1000):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same changes as above in this docstring 😉

"""
Context manager that waits for ``timeout`` milliseconds or until the window is exposed.
If the window is not exposed within ``timeout`` milliseconds, raise ``TimeoutError``.

This is mainly useful for asynchronous systems like X11, where a window will be mapped to screen
some time after being asked to show itself on the screen.

.. code-block:: python

with qtbot.waitExposed(splash, timeout=500):
startup()

:param QWidget widget:
Widget to wait for.

:param int|None timeout:
How many milliseconds to wait for.

.. note::
This function is only available in PyQt5, raising a ``RuntimeError`` if called from
``PyQt4`` or ``PySide``.

.. note:: This method is also available as ``wait_exposed`` (pep-8 alias)
"""
__tracebackhide__ = True
return _WaitWidgetContextManager('qWaitForWindowExposed', 'exposed', widget, timeout)

wait_exposed = waitExposed # pep-8 alias

def waitForWindowShown(self, widget):
"""
Waits until the window is shown in the screen. This is mainly useful for asynchronous
Expand All @@ -150,17 +214,14 @@ def waitForWindowShown(self, widget):
:param QWidget widget:
Widget to wait on.

.. note:: In Qt5, the actual method called is qWaitForWindowExposed,
but this name is kept for backward compatibility
.. note:: In ``PyQt5`` this function is considered deprecated in favor of :meth:`waitExposed`.

.. note:: This method is also available as ``wait_for_window_shown`` (pep-8 alias)
"""
if hasattr(qt_api.QtTest.QTest, 'qWaitForWindowShown'): # pragma: no cover
# PyQt4 and PySide
qt_api.QtTest.QTest.qWaitForWindowShown(widget)
else: # pragma: no cover
# PyQt5
qt_api.QtTest.QTest.qWaitForWindowExposed(widget)
if qt_api.pytest_qt_api == 'pyqt5':
return qt_api.QtTest.QTest.qWaitForWindowExposed(widget)
else:
return qt_api.QtTest.QTest.qWaitForWindowShown(widget)

wait_for_window_shown = waitForWindowShown # pep-8 alias

Expand Down Expand Up @@ -221,11 +282,11 @@ def waitSignal(self, signal=None, timeout=1000, raising=None, check_params_cb=No

:param Signal signal:
A signal to wait for, or a tuple ``(signal, signal_name_as_str)`` to improve the error message that is part
of ``SignalTimeoutError``. Set to ``None`` to just use timeout.
of ``TimeoutError``. Set to ``None`` to just use timeout.
:param int timeout:
How many milliseconds to wait before resuming control flow.
:param bool raising:
If :class:`QtBot.SignalTimeoutError <pytestqt.plugin.SignalTimeoutError>`
If :class:`QtBot.TimeoutError <pytestqt.plugin.TimeoutError>`
should be raised if a timeout occurred.
This defaults to ``True`` unless ``qt_wait_signal_raising = false``
is set in the config.
Expand Down Expand Up @@ -280,12 +341,12 @@ def waitSignals(self, signals=None, timeout=1000, raising=None, check_params_cbs

:param list signals:
A list of :class:`Signal` objects to wait for. Alternatively: a list of (``Signal, str``) tuples of the form
``(signal, signal_name_as_str)`` to improve the error message that is part of ``SignalTimeoutError``.
``(signal, signal_name_as_str)`` to improve the error message that is part of ``TimeoutError``.
Set to ``None`` to just use timeout.
:param int timeout:
How many milliseconds to wait before resuming control flow.
:param bool raising:
If :class:`QtBot.SignalTimeoutError <pytestqt.plugin.SignalTimeoutError>`
If :class:`QtBot.TimeoutError <pytestqt.plugin.TimeoutError>`
should be raised if a timeout occurred.
This defaults to ``True`` unless ``qt_wait_signal_raising = false``
is set in the config.
Expand Down Expand Up @@ -390,7 +451,7 @@ def view_updated():
qtbot.waitUntil(lambda: view_model.count() > 10)

Note that this usage only accepts returning actual ``True`` and ``False`` values,
so returning an empty list to express "falseness" raises an ``ValueError``.
so returning an empty list to express "falseness" raises a ``ValueError``.

:param callback: callable that will be called periodically.
:param timeout: timeout value in ms.
Expand Down Expand Up @@ -433,7 +494,7 @@ def timed_out():
wait_until = waitUntil # pep-8 alias

@contextlib.contextmanager
def capture_exceptions(self):
def captureExceptions(self):
"""
.. versionadded:: 2.1

Expand All @@ -447,11 +508,15 @@ def capture_exceptions(self):

# exception is a list of sys.exc_info tuples
assert len(exceptions) == 1

.. note:: This method is also available as ``capture_exceptions`` (pep-8 alias)
"""
from pytestqt.exceptions import capture_exceptions
with capture_exceptions() as exceptions:
yield exceptions

capture_exceptions = captureExceptions

@classmethod
def _inject_qtest_methods(cls):
"""
Expand Down Expand Up @@ -495,8 +560,10 @@ def result(*args, **kwargs):
setattr(cls, method_name, method)


# provide easy access to SignalTimeoutError to qtbot fixtures
# provide easy access to exceptions to qtbot fixtures
QtBot.SignalTimeoutError = SignalTimeoutError
QtBot.SignalEmittedError = SignalEmittedError
QtBot.TimeoutError = TimeoutError


def _add_widget(item, widget):
Expand Down Expand Up @@ -527,3 +594,40 @@ def _iter_widgets(item):
Iterates over widgets registered in the given pytest item.
"""
return iter(getattr(item, 'qt_widgets', []))


class _WaitWidgetContextManager(object):
"""
Context manager implementation used by ``waitActive`` and ``waitExposed`` methods.
"""

def __init__(self, method_name, adjective_name, widget, timeout):
"""
:param str method_name: name ot the ``QtTest`` method to call to check if widget is active/exposed.
:param str adjective_name: "activated" or "exposed".
:param widget:
:param timeout:
"""
self._method_name = method_name
self._adjective_name = adjective_name
self._widget = widget
self._timeout = timeout

def __enter__(self):
__tracebackhide__ = True
if qt_api.pytest_qt_api != 'pyqt5':
raise RuntimeError('Available in PyQt5 only')
return self

def __exit__(self, exc_type, exc_val, exc_tb):
__tracebackhide__ = True
try:
if exc_type is None:
method = getattr(qt_api.QtTest.QTest, self._method_name)
r = method(self._widget, self._timeout)
if not r:
msg = 'widget {} not {} in {} ms.'.format(self._widget, self._adjective_name, self._timeout)
raise TimeoutError(msg)
finally:
self._widget = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this to avoid leaking memory?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!


18 changes: 4 additions & 14 deletions pytestqt/wait_signal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import functools
from collections import namedtuple

from pytestqt.exceptions import TimeoutError
from pytestqt.qt_compat import qt_api


Expand Down Expand Up @@ -48,7 +48,7 @@ def wait(self):
self._timer.start()
self._loop.exec_()
if not self.signal_triggered and self.raising:
raise SignalTimeoutError(self._timeout_message)
raise TimeoutError(self._timeout_message)

def _quit_loop_by_timeout(self):
try:
Expand All @@ -65,7 +65,7 @@ def _cleanup(self):
self._timer = None

def _get_timeout_error_message(self):
"""Subclasses have to implement this, returning an appropriate error message for a SignalTimeoutError."""
"""Subclasses have to implement this, returning an appropriate error message for a TimeoutError."""
raise NotImplementedError # pragma: no cover

def _extract_pyqt_signal_name(self, potential_pyqt_signal):
Expand Down Expand Up @@ -153,7 +153,7 @@ class SignalBlocker(_AbstractSignalBlocker):
this is set to ``None``.

:ivar bool raising:
If :class:`SignalTimeoutError` should be raised if a timeout occurred.
If :class:`TimeoutError` should be raised if a timeout occurred.

.. note:: contrary to the parameter of same name in
:meth:`pytestqt.qtbot.QtBot.waitSignal`, this parameter does not
Expand Down Expand Up @@ -554,16 +554,6 @@ def assert_not_emitted(self):
(self.signal,))


class SignalTimeoutError(Exception):
"""
.. versionadded:: 1.4

The exception thrown by :meth:`pytestqt.qtbot.QtBot.waitSignal` if the
*raising* parameter has been given and there was a timeout.
"""
pass


class SignalEmittedError(Exception):
"""
.. versionadded:: 1.11
Expand Down
Loading