Skip to content

Commit b63f7f0

Browse files
author
John David Reaver
committed
Added signal blocker, as discussed in #12
1 parent f0e93a3 commit b63f7f0

File tree

2 files changed

+110
-2
lines changed

2 files changed

+110
-2
lines changed

pytestqt/_tests/test_wait_signal.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
import time
3+
4+
from pytestqt.qt_compat import QtCore, Signal
5+
6+
7+
def test_signal_blocker_exception(qtbot):
8+
with pytest.raises(ValueError):
9+
qtbot.waitSignal(None, None).wait()
10+
11+
12+
class Signaller(QtCore.QObject):
13+
14+
signal = Signal()
15+
16+
17+
def test_wait_signal_context_manager(qtbot, monkeypatch):
18+
signaller = Signaller()
19+
20+
# Emit a signal after half a second, and block the signal with a timeout
21+
# of 2 seconds.
22+
QtCore.QTimer.singleShot(500, signaller.signal.emit)
23+
with qtbot.waitSignal(signaller.signal, 2000) as blocker:
24+
saved_loop = blocker.loop
25+
start_time = time.time()
26+
27+
# Check that event loop exited.
28+
assert not saved_loop.isRunning()
29+
# Check that it didn't exit by a timeout.
30+
assert time.time() - start_time < 2 # Less than 2 seconds elapsed
31+
32+
33+
def test_wait_signal_function(qtbot, monkeypatch):
34+
signaller = Signaller()
35+
36+
# Emit a signal after half a second, and block the signal with a timeout
37+
# of 2 seconds.
38+
QtCore.QTimer.singleShot(500, signaller.signal.emit)
39+
blocker = qtbot.waitSignal(signaller.signal, 2000)
40+
start_time = time.time()
41+
blocker.wait()
42+
43+
# Check that event loop exited.
44+
assert not blocker.loop.isRunning()
45+
# Check that it didn't exit by a timeout.
46+
assert time.time() - start_time < 2 # Less than 2 seconds elapsed

pytestqt/plugin.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import pytest
66

7-
from pytestqt.qt_compat import QtGui
8-
from pytestqt.qt_compat import QtTest
7+
from pytestqt.qt_compat import QtCore, QtGui, QtTest
98

109

1110
def _inject_qtest_methods(cls):
@@ -61,6 +60,7 @@ class QtBot(object):
6160
.. automethod:: addWidget
6261
.. automethod:: waitForWindowShown
6362
.. automethod:: stopForInteraction
63+
.. automethod:: waitSignal
6464
6565
**Raw QTest API**
6666
@@ -212,6 +212,68 @@ def stopForInteraction(self):
212212

213213
stop = stopForInteraction
214214

215+
def waitSignal(self, signal=None, timeout=1000):
216+
"""
217+
Stops current test until a signal is triggered.
218+
219+
Used to stop the control flow of a test until a signal is emitted, or
220+
a number of milliseconds, specified by ``timeout``, has elapsed.
221+
222+
Best used as a context manager::
223+
224+
with qtbot.waitSignal(signal, timeout=1000):
225+
long_function_that_calls_signal()
226+
227+
Can also be used to return blocker object::
228+
229+
blocker = qtbot.waitSignal(signal, timeout=1000)
230+
blocker.connect(other_signal)
231+
long_function_that_calls_signal()
232+
blocker.wait()
233+
234+
:param Signal signal:
235+
A signal to wait for. Set to ``None`` to just use timeout.
236+
:param int timeout:
237+
How many milliseconds to wait before resuming control flow.
238+
:returns:
239+
``SignalBlocker`` object. Call ``SignalBlocker.wait()`` to wait.
240+
241+
.. note::
242+
Cannot have both ``signals`` and ``timeout`` equal ``None``, or
243+
else you will block indefinitely. We throw an error if this occurs.
244+
245+
"""
246+
blocker = SignalBlocker(timeout=timeout)
247+
if signal is not None:
248+
blocker.connect(signal)
249+
return blocker
250+
251+
252+
class SignalBlocker:
253+
254+
def __init__(self, timeout=1000):
255+
self.loop = QtCore.QEventLoop()
256+
self._signals = []
257+
self.timeout = timeout
258+
259+
def wait(self):
260+
if self.timeout is None and len(self._signals) == 0:
261+
raise ValueError("No signals or timeout specified.")
262+
if self.timeout is not None:
263+
QtCore.QTimer.singleShot(self.timeout, self.loop.quit)
264+
self.loop.exec_()
265+
266+
def connect(self, signal):
267+
signal.connect(self.loop.quit)
268+
self._signals.append(signal)
269+
270+
def __enter__(self):
271+
# Return self for testing purposes. Generally not needed.
272+
return self
273+
274+
def __exit__(self, type, value, traceback):
275+
self.wait()
276+
215277

216278
def pytest_configure(config):
217279
"""

0 commit comments

Comments
 (0)