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
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
3.1.0 (unreleased)
------------------

- The ``qt_wait_signal_raising`` option was renamed to ``qt_default_raising``.
The old name continues to work, but is deprecated.
- New ``qtbot.waitCallback()`` method that returns a ``CallbackBlocker``, which
can be used to wait for a callback to be called.
- The docs still referred to ``SignalTimeoutError`` in some places, despite it
being renamed to ``TimeoutError`` in the 2.1 release. This is now corrected.

3.0.3 (unreleased)
------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pytest-qt
logging
signals
wait_until
wait_callback
virtual_methods
modeltester
app_exit
Expand Down
20 changes: 11 additions & 9 deletions docs/signals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ ensuring the results are correct:
app.worker.start()
# Test will block at this point until either the "finished" or the
# "failed" signal is emitted. If 10 seconds passed without a signal,
# SignalTimeoutError will be raised.
# TimeoutError will be raised.

assert_application_results(app)

Expand All @@ -34,7 +34,7 @@ raising parameter
.. versionchanged:: 2.0

You can pass ``raising=False`` to avoid raising a
:class:`qtbot.SignalTimeoutError <SignalTimeoutError>` if the timeout is
:class:`qtbot.TimeoutError <TimeoutError>` if the timeout is
reached before the signal is triggered:

.. code-block:: python
Expand All @@ -46,29 +46,31 @@ reached before the signal is triggered:

assert_application_results(app)

# qtbot.SignalTimeoutError is not raised, but you can still manually
# qtbot.TimeoutError is not raised, but you can still manually
# check whether the signal was triggered:
assert blocker.signal_triggered, "process timed-out"

.. _qt_wait_signal_raising:
.. _qt_default_raising:

qt_wait_signal_raising ini option
---------------------------------
qt_default_raising ini option
-----------------------------

.. versionadded:: 1.11
.. versionchanged:: 2.0
.. versionchanged:: 3.1

The ``qt_wait_signal_raising`` ini option can be used to override the default
The ``qt_default_raising`` ini option can be used to override the default
value of the ``raising`` parameter of the ``qtbot.waitSignal`` and
``qtbot.waitSignals`` functions when omitted:

.. code-block:: ini

[pytest]
qt_wait_signal_raising = false
qt_default_raising = false

Calls which explicitly pass the ``raising`` parameter are not affected.

This option was called ``qt_wait_signal_raising`` before 3.1.0.

check_params_cb parameter
-------------------------
Expand Down Expand Up @@ -158,7 +160,7 @@ the ``raising`` parameter::
w.start()

# this will be reached after all workers emit their "finished"
# signal or a qtbot.SignalTimeoutError will be raised
# signal or a qtbot.TimeoutError will be raised
assert_application_results(app)

check_params_cbs parameter
Expand Down
50 changes: 50 additions & 0 deletions docs/wait_callback.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
waitCallback: Waiting for methods taking a callback
===================================================

.. versionadded:: 3.1

Some methods in Qt (especially ``QtWebEngine``) take a callback as argument,
which gets called by Qt once a given operation is done.

To test such code, you can use :meth:`qtbot.waitCallback <pytestqt.plugin.QtBot.waitCallback>`
which waits until the callback has been called or a timeout is reached.

The ``qtbot.waitCallback()`` method returns a callback which is callable
directly.

For example:

.. code-block:: python

def test_js(qtbot):
page = QWebEnginePage()
with qtbot.waitCallback() as cb:
page.runJavaScript("1 + 1", cb)
# After callback

Anything following the ``with`` block will be run only after the callback has been called.

If the callback doesn't get called during the given timeout,
:class:`qtbot.TimeoutError <TimeoutError>` is raised. If it is called more than once,
:class:`qtbot.CallbackCalledTwiceError <CallbackCalledTwiceError>` is raised.

raising parameter
-----------------

Similarly to ``qtbot.waitSignal``, you can pass a ``raising=False`` parameter
(or set the ``qt_default_raising`` ini option) to avoid raising an exception on
timeouts. See :doc:`signals` for details.

Getting arguments the callback was called with
----------------------------------------------

After the callback is called, the arguments and keyword arguments passed to it
are available via ``.args`` (as a list) and ``.kwargs`` (as a dict),
respectively.

In the example above, we could check the result via:

.. code-block:: python

assert cb.args == [2]
assert cb.kwargs == {}
14 changes: 13 additions & 1 deletion pytestqt/plugin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import warnings

import pytest

from pytestqt.exceptions import (
Expand Down Expand Up @@ -101,9 +103,13 @@ def pytest_addoption(parser):
"qt_api", 'Qt api version to use: "pyside", "pyqt4", "pyqt4v2", "pyqt5"'
)
parser.addini("qt_no_exception_capture", "disable automatic exception capture")
parser.addini(
"qt_default_raising",
"Default value for the raising parameter of qtbot.waitSignal/waitCallback",
)
parser.addini(
"qt_wait_signal_raising",
"Default value for the raising parameter of qtbot.waitSignal",
"Default value for the raising parameter of qtbot.waitSignal (legacy alias)",
)

default_log_fail = QtLoggingPlugin.LOG_FAIL_OPTIONS[0]
Expand Down Expand Up @@ -211,6 +217,12 @@ def pytest_configure(config):

qt_api.set_qt_api(config.getini("qt_api"))

if config.getini("qt_wait_signal_raising"):
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we check if config.getini("qt_wait_signal_raising") returns True or False to raise the warning? Because if users configure it as False, the if block won't execute.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I think it's a string (because there's _parse_ini_boolean in qtbot.py), but I wonder why we don't pass type="bool" in the parser.addini calls in pytest_addoption to let pytest do the ini parsing?

Copy link
Member

Choose a reason for hiding this comment

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

No idea... 🤔

Copy link
Member

Choose a reason for hiding this comment

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

But if you see the warning if qt_wait_signal_raising is set to False or True, I'm happy!

warnings.warn(
"qt_wait_signal_raising is deprecated, use qt_default_raising instead.",
DeprecationWarning,
)

from .qtbot import QtBot

QtBot._inject_qtest_methods()
Expand Down
73 changes: 63 additions & 10 deletions pytestqt/qtbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
MultiSignalBlocker,
SignalEmittedSpy,
SignalEmittedError,
CallbackBlocker,
CallbackCalledTwiceError,
)


Expand Down Expand Up @@ -137,6 +139,19 @@ class QtBot(object):
def __init__(self, request):
self._request = request

def _should_raise(self, raising_arg):
ini_val = self._request.config.getini("qt_default_raising")
legacy_ini_val = self._request.config.getini("qt_wait_signal_raising")

if raising_arg is not None:
return raising_arg
elif legacy_ini_val:
return _parse_ini_boolean(legacy_ini_val)
elif ini_val:
return _parse_ini_boolean(ini_val)
else:
return True

def addWidget(self, widget):
"""
Adds a widget to be tracked by this bot. This is not required, but will ensure that the
Expand Down Expand Up @@ -298,7 +313,7 @@ def waitSignal(self, signal=None, timeout=1000, raising=None, check_params_cb=No
:param bool raising:
If :class:`QtBot.TimeoutError <pytestqt.plugin.TimeoutError>`
should be raised if a timeout occurred.
This defaults to ``True`` unless ``qt_wait_signal_raising = false``
This defaults to ``True`` unless ``qt_default_raising = false``
is set in the config.
:param Callable check_params_cb:
Optional ``callable`` that compares the provided signal parameters to some expected parameters.
Expand All @@ -314,12 +329,7 @@ def waitSignal(self, signal=None, timeout=1000, raising=None, check_params_cb=No
.. note::
This method is also available as ``wait_signal`` (pep-8 alias)
"""
if raising is None:
raising_val = self._request.config.getini("qt_wait_signal_raising")
if not raising_val:
raising = True
else:
raising = _parse_ini_boolean(raising_val)
raising = self._should_raise(raising)
blocker = SignalBlocker(
timeout=timeout, raising=raising, check_params_cb=check_params_cb
)
Expand Down Expand Up @@ -367,7 +377,7 @@ def waitSignals(
:param bool raising:
If :class:`QtBot.TimeoutError <pytestqt.plugin.TimeoutError>`
should be raised if a timeout occurred.
This defaults to ``True`` unless ``qt_wait_signal_raising = false``
This defaults to ``True`` unless ``qt_default_raising = false``
is set in the config.
:param list check_params_cbs:
optional list of callables that compare the provided signal parameters to some expected parameters.
Expand Down Expand Up @@ -399,8 +409,7 @@ def waitSignals(
if order not in ["none", "simple", "strict"]:
raise ValueError("order has to be set to 'none', 'simple' or 'strict'")

if raising is None:
raising = self._request.config.getini("qt_wait_signal_raising")
raising = self._should_raise(raising)
Copy link
Member Author

Choose a reason for hiding this comment

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

Just noticed that I actually fixed another bug here 😆 qtbot.waitSignals() didn't call _parse_ini_boolean like in other places, so raising was "" (so false) when there is no entry for it in the .ini file. This actually ended up hiding a failing test in qutebrowser!


if check_params_cbs:
if len(check_params_cbs) != len(signals):
Expand Down Expand Up @@ -529,6 +538,49 @@ def timed_out():

wait_until = waitUntil # pep-8 alias

def waitCallback(self, timeout=1000, raising=None):
"""
.. versionadded:: 3.1

Stops current test until a callback is called.

Used to stop the control flow of a test until the returned callback is
called, or a number of milliseconds, specified by ``timeout``, has
elapsed.

Best used as a context manager::

with qtbot.waitCallback() as callback:
function_taking_a_callback(callback)
assert callback.args == [True]

Also, you can use the :class:`CallbackBlocker` directly if the
context manager form is not convenient::

blocker = qtbot.waitCallback(timeout=1000)
function_calling_a_callback(blocker)
blocker.wait()


:param int timeout:
How many milliseconds to wait before resuming control flow.
:param bool raising:
If :class:`QtBot.TimeoutError <pytestqt.plugin.TimeoutError>`
should be raised if a timeout occurred.
This defaults to ``True`` unless ``qt_default_raising = false``
is set in the config.
:returns:
A ``CallbackBlocker`` object which can be used directly as a
callback as it implements ``__call__``.

.. note:: This method is also available as ``wait_callback`` (pep-8 alias)
"""
raising = self._should_raise(raising)
blocker = CallbackBlocker(timeout=timeout, raising=raising)
return blocker

wait_callback = waitCallback # pep-8 alias

@contextlib.contextmanager
def captureExceptions(self):
"""
Expand Down Expand Up @@ -599,6 +651,7 @@ def result(*args, **kwargs):
QtBot.SignalTimeoutError = SignalTimeoutError
QtBot.SignalEmittedError = SignalEmittedError
QtBot.TimeoutError = TimeoutError
QtBot.CallbackCalledTwiceError = CallbackCalledTwiceError


def _add_widget(item, widget):
Expand Down
Loading