Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Make AbstractSignalBlocker a QObject and fix signals in threads #594

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
17 changes: 14 additions & 3 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
UNRELEASED
----------

* Added official support for Python 3.13.
* Dropped support for EOL Python 3.8.
* Dropped support for EOL PySide 2.
- Added official support for Python 3.13.
- Dropped support for EOL Python 3.8.
- Dropped support for EOL PySide 2.
- Fixed PySide6 exceptions / warnings about being unable to disconnect signals
with ``qtbot.waitSignal`` (`#552`_, `#558`_).
- Reduced the likelyhood of trouble when using ``qtbot.waitSignal(s)`` and
``qtbot.waitCallback`` where the signal/callback is emitted from a non-main
thread. In theory, more problems remain and this isn't a proper fix yet. In
practice, it seems impossible to provoke any problems in pytest-qt's testsuite.
(`#586`_)

.. _#552: https://github.com/pytest-dev/pytest-qt/issues/552
.. _#558: https://github.com/pytest-dev/pytest-qt/issues/558
.. _#586: https://github.com/pytest-dev/pytest-qt/issues/586

4.4.0 (2024-02-07)
------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/wait_callback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Anything following the ``with`` block will be run only after the callback has be

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

raising parameter
-----------------
Expand Down
3 changes: 1 addition & 2 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
testpaths = tests
addopts = --strict-markers --strict-config
xfail_strict = true
markers =
filterwarnings: pytest's filterwarnings marker
filterwarnings = error
22 changes: 22 additions & 0 deletions src/pytestqt/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,25 @@ class ScreenshotError(Exception):

Access via ``qtbot.ScreenshotError``.
"""


class SignalEmittedError(Exception):
"""
.. versionadded:: 1.11

The exception thrown by :meth:`pytestqt.qtbot.QtBot.assertNotEmitted` if a
signal was emitted unexpectedly.

Access via ``qtbot.SignalEmittedError``.
"""


class CallbackCalledTwiceError(Exception):
"""
.. versionadded:: 3.1

The exception thrown by :meth:`pytestqt.qtbot.QtBot.waitCallback` if a
callback was called twice.

Access via ``qtbot.CallbackCalledTwiceError``.
"""
22 changes: 10 additions & 12 deletions src/pytestqt/qtbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
import weakref
import warnings

from pytestqt.exceptions import TimeoutError, ScreenshotError
from pytestqt.qt_compat import qt_api
from pytestqt.wait_signal import (
SignalBlocker,
MultiSignalBlocker,
SignalEmittedSpy,
from pytestqt.exceptions import (
TimeoutError,
ScreenshotError,
SignalEmittedError,
CallbackBlocker,
CallbackCalledTwiceError,
)
from pytestqt.qt_compat import qt_api
from pytestqt import wait_signal


def _parse_ini_boolean(value):
Expand Down Expand Up @@ -350,7 +348,7 @@ def waitSignal(self, signal, *, timeout=5000, raising=None, check_params_cb=None
f"Passing None as signal isn't supported anymore, use qtbot.wait({timeout}) instead."
)
raising = self._should_raise(raising)
blocker = SignalBlocker(
blocker = wait_signal.SignalBlocker(
timeout=timeout, raising=raising, check_params_cb=check_params_cb
)
blocker.connect(signal)
Expand Down Expand Up @@ -437,7 +435,7 @@ def waitSignals(
len(check_params_cbs), len(signals)
)
)
blocker = MultiSignalBlocker(
blocker = wait_signal.MultiSignalBlocker(
timeout=timeout,
raising=raising,
order=order,
Expand All @@ -455,7 +453,7 @@ def wait(self, ms):
While waiting, events will be processed and your test will stay
responsive to user interface events or network communication.
"""
blocker = MultiSignalBlocker(timeout=ms, raising=False)
blocker = wait_signal.MultiSignalBlocker(timeout=ms, raising=False)
blocker.wait()

@contextlib.contextmanager
Expand All @@ -475,7 +473,7 @@ def assertNotEmitted(self, signal, *, wait=0):
.. note:: This method is also available as ``assert_not_emitted``
(pep-8 alias)
"""
spy = SignalEmittedSpy(signal)
spy = wait_signal.SignalEmittedSpy(signal)
with spy, self.waitSignal(signal, timeout=wait, raising=False):
yield
spy.assert_not_emitted()
Expand Down Expand Up @@ -589,7 +587,7 @@ def waitCallback(self, *, timeout=5000, raising=None):
.. note:: This method is also available as ``wait_callback`` (pep-8 alias)
"""
raising = self._should_raise(raising)
blocker = CallbackBlocker(timeout=timeout, raising=raising)
blocker = wait_signal.CallbackBlocker(timeout=timeout, raising=raising)
return blocker

@contextlib.contextmanager
Expand Down
20 changes: 20 additions & 0 deletions src/pytestqt/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import dataclasses
from typing import Any


def get_marker(item, name):
"""Get a marker from a pytest item.

Expand All @@ -9,3 +13,19 @@ def get_marker(item, name):
except AttributeError:
# pytest < 3.6
return item.get_marker(name)


@dataclasses.dataclass
class SignalAndArgs:
signal_name: str
args: list[Any]

def __str__(self) -> str:
args = repr(self.args) if self.args else ""

# remove signal parameter signature, e.g. turn "some_signal(str,int)" to "some_signal", because we're adding
# the actual parameters anyways
signal_name = self.signal_name
signal_name = signal_name.partition("(")[0]

return signal_name + args
Loading
Loading