Skip to content

Commit e160714

Browse files
committed
Merge remote-tracking branch 'origin/master' into qt-api-ini
2 parents d9c3d8f + bad3a3b commit e160714

File tree

4 files changed

+392
-33
lines changed

4 files changed

+392
-33
lines changed

docs/signals.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ the ``raising`` parameter::
102102

103103
def test_workers(qtbot):
104104
workers = spawn_workers()
105-
with qtbot.waitSignal([w.finished for w in workers]):
105+
with qtbot.waitSignals([w.finished for w in workers]):
106106
for w in workers:
107107
w.start()
108108

pytestqt/qtbot.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def stopForInteraction(self):
188188

189189
stop = stopForInteraction
190190

191-
def waitSignal(self, signal=None, timeout=1000, raising=None):
191+
def waitSignal(self, signal=None, timeout=1000, raising=None, check_params_cb=None):
192192
"""
193193
.. versionadded:: 1.2
194194
@@ -224,6 +224,10 @@ def waitSignal(self, signal=None, timeout=1000, raising=None):
224224
should be raised if a timeout occurred.
225225
This defaults to ``True`` unless ``qt_wait_signal_raising = false``
226226
is set in the config.
227+
:param Callable check_params_cb:
228+
Optional callable(*parameters) that compares the provided signal parameters to some expected parameters.
229+
It has to match the signature of ``signal`` (just like a slot function would) and return ``True`` if
230+
parameters match, ``False`` otherwise.
227231
:returns:
228232
``SignalBlocker`` object. Call ``SignalBlocker.wait()`` to wait.
229233
@@ -239,14 +243,14 @@ def waitSignal(self, signal=None, timeout=1000, raising=None):
239243
raising = True
240244
else:
241245
raising = _parse_ini_boolean(raising_val)
242-
blocker = SignalBlocker(timeout=timeout, raising=raising)
246+
blocker = SignalBlocker(timeout=timeout, raising=raising, check_params_cb=check_params_cb)
243247
if signal is not None:
244248
blocker.connect(signal)
245249
return blocker
246250

247251
wait_signal = waitSignal # pep-8 alias
248252

249-
def waitSignals(self, signals=None, timeout=1000, raising=None):
253+
def waitSignals(self, signals=None, timeout=1000, raising=None, check_params_cbs=None, order="none"):
250254
"""
251255
.. versionadded:: 1.4
252256
@@ -269,15 +273,28 @@ def waitSignals(self, signals=None, timeout=1000, raising=None):
269273
blocker.wait()
270274
271275
:param list signals:
272-
A list of :class:`Signal` objects to wait for. Set to ``None`` to
273-
just use timeout.
276+
A list of :class:`Signal` objects to wait for. Set to ``None`` to just use
277+
timeout.
274278
:param int timeout:
275279
How many milliseconds to wait before resuming control flow.
276280
:param bool raising:
277281
If :class:`QtBot.SignalTimeoutError <pytestqt.plugin.SignalTimeoutError>`
278282
should be raised if a timeout occurred.
279283
This defaults to ``True`` unless ``qt_wait_signal_raising = false``
280284
is set in the config.
285+
:param list check_params_cbs:
286+
optional list of callables that compare the provided signal parameters to some expected parameters.
287+
Each callable has to match the signature of the corresponding signal in ``signals`` (just like a slot
288+
function would) and return ``True`` if parameters match, ``False`` otherwise.
289+
Instead of a specific callable, ``None`` can be provided, to disable parameter checking for the
290+
corresponding signal.
291+
If the number of callbacks doesn't match the number of signals ``ValueError`` will be raised.
292+
:param str order: determines the order in which to expect signals
293+
* ``"none"``: no order is enforced
294+
* ``"strict"``: signals have to be emitted strictly in the provided order
295+
(e.g. fails when expecting signals [a, b] and [a, a, b] is emitted)
296+
* ``"simple"``: like "strict", but signals may be emitted in-between the provided ones, e.g. expected
297+
``signals`` == [a, b, c] and actually emitted signals = [a, a, b, a, c] works (would fail with "strict")
281298
:returns:
282299
``MultiSignalBlocker`` object. Call ``MultiSignalBlocker.wait()``
283300
to wait.
@@ -288,12 +305,19 @@ def waitSignals(self, signals=None, timeout=1000, raising=None):
288305
289306
.. note:: This method is also available as ``wait_signals`` (pep-8 alias)
290307
"""
308+
if order not in ["none", "simple", "strict"]:
309+
raise ValueError("order has to be set to 'none', 'simple' or 'strict'")
310+
291311
if raising is None:
292312
raising = self._request.config.getini('qt_wait_signal_raising')
293-
blocker = MultiSignalBlocker(timeout=timeout, raising=raising)
313+
314+
if check_params_cbs:
315+
if len(check_params_cbs) != len(signals):
316+
raise ValueError("Number of callbacks ({}) does not "
317+
"match number of signals ({})!".format(len(check_params_cbs), len(signals)))
318+
blocker = MultiSignalBlocker(timeout=timeout, raising=raising, order=order, check_params_cbs=check_params_cbs)
294319
if signals is not None:
295-
for signal in signals:
296-
blocker._add_signal(signal)
320+
blocker.add_signals(signals)
297321
return blocker
298322

299323
wait_signals = waitSignals # pep-8 alias

pytestqt/wait_signal.py

Lines changed: 95 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44

55
class _AbstractSignalBlocker(object):
6-
76
"""
87
Base class for :class:`SignalBlocker` and :class:`MultiSignalBlocker`.
98
@@ -71,7 +70,6 @@ def __exit__(self, type, value, traceback):
7170

7271

7372
class SignalBlocker(_AbstractSignalBlocker):
74-
7573
"""
7674
Returned by :meth:`pytestqt.qtbot.QtBot.waitSignal` method.
7775
@@ -101,10 +99,11 @@ class SignalBlocker(_AbstractSignalBlocker):
10199
.. automethod:: connect
102100
"""
103101

104-
def __init__(self, timeout=1000, raising=True):
102+
def __init__(self, timeout=1000, raising=True, check_params_cb=None):
105103
super(SignalBlocker, self).__init__(timeout, raising=raising)
106104
self._signals = []
107105
self.args = None
106+
self.check_params_callback = check_params_cb
108107

109108
def connect(self, signal):
110109
"""
@@ -123,6 +122,9 @@ def _quit_loop_by_signal(self, *args):
123122
"""
124123
quits the event loop and marks that we finished because of a signal.
125124
"""
125+
if self.check_params_callback:
126+
if not self.check_params_callback(*args):
127+
return # parameter check did not pass
126128
try:
127129
self.signal_triggered = True
128130
self.args = list(args)
@@ -138,7 +140,6 @@ def _cleanup(self):
138140

139141

140142
class MultiSignalBlocker(_AbstractSignalBlocker):
141-
142143
"""
143144
Returned by :meth:`pytestqt.qtbot.QtBot.waitSignals` method, blocks until
144145
all signals connected to it are triggered or the timeout is reached.
@@ -151,48 +152,121 @@ class MultiSignalBlocker(_AbstractSignalBlocker):
151152
.. automethod:: wait
152153
"""
153154

154-
def __init__(self, timeout=1000, raising=True):
155+
def __init__(self, timeout=1000, raising=True, check_params_cbs=None, order="none"):
155156
super(MultiSignalBlocker, self).__init__(timeout, raising=raising)
156-
self._signals = {}
157-
self._slots = {}
158-
159-
def _add_signal(self, signal):
157+
self.order = order
158+
self.check_params_callbacks = check_params_cbs
159+
self._signals_emitted = [] # list of booleans, indicates whether the signal was already emitted
160+
self._signals_map = {} # maps from a unique Signal to a list of indices where to expect signal instance emits
161+
self._signals = [] # list of all Signals (for compatibility with _AbstractSignalBlocker)
162+
self._slots = [] # list of slot functions
163+
self._signal_expected_index = 0 # only used when forcing order
164+
self._strict_order_violated = False
165+
166+
def add_signals(self, signals):
160167
"""
161168
Adds the given signal to the list of signals which :meth:`wait()` waits
162169
for.
163170
164-
:param signal: QtCore.Signal
171+
:param list signals: list of QtCore.Signal`s
165172
"""
166-
self._signals[signal] = False
167-
slot = functools.partial(self._signal_emitted, signal)
168-
self._slots[signal] = slot
169-
signal.connect(slot)
173+
# determine uniqueness of signals, creating a map that maps from a unique signal to a list of indices
174+
# (positions) where this signal is expected (in case order matters)
175+
signals_as_str = [str(signal) for signal in signals]
176+
signal_str_to_signal = {} # maps from a signal-string to one of the signal instances (the first one found)
177+
for index, signal_str in enumerate(signals_as_str):
178+
signal = signals[index]
179+
if signal_str not in signal_str_to_signal:
180+
signal_str_to_signal[signal_str] = signal
181+
self._signals_map[signal] = [index] # create a new list
182+
else:
183+
# append to existing list
184+
first_signal_that_occurred = signal_str_to_signal[signal_str]
185+
self._signals_map[first_signal_that_occurred].append(index)
170186

171-
def _signal_emitted(self, signal):
187+
for signal in signals:
188+
self._signals_emitted.append(False)
189+
190+
for unique_signal in self._signals_map:
191+
slot = functools.partial(self._signal_emitted, unique_signal)
192+
self._slots.append(slot)
193+
unique_signal.connect(slot)
194+
self._signals.append(unique_signal)
195+
196+
def _signal_emitted(self, signal, *args):
172197
"""
173198
Called when a given signal is emitted.
174199
175200
If all expected signals have been emitted, quits the event loop and
176201
marks that we finished because signals.
177202
"""
178-
self._signals[signal] = True
179-
if all(self._signals.values()):
203+
if self.order == "none":
204+
# perform the test for every matching index (stop after the first one that matches)
205+
successfully_emitted = False
206+
successful_index = -1
207+
potential_indices = self._get_unemitted_signal_indices(signal)
208+
for potential_index in potential_indices:
209+
if self._check_callback(potential_index, *args):
210+
successful_index = potential_index
211+
successfully_emitted = True
212+
break
213+
214+
if successfully_emitted:
215+
self._signals_emitted[successful_index] = True
216+
elif self.order == "simple":
217+
potential_indices = self._get_unemitted_signal_indices(signal)
218+
if potential_indices:
219+
if self._signal_expected_index == potential_indices[0]:
220+
if self._check_callback(self._signal_expected_index, *args):
221+
self._signals_emitted[self._signal_expected_index] = True
222+
self._signal_expected_index += 1
223+
else: # self.order == "strict"
224+
if not self._strict_order_violated:
225+
# only do the check if the strict order has not been violated yet
226+
self._strict_order_violated = True # assume the order has been violated this time
227+
potential_indices = self._get_unemitted_signal_indices(signal)
228+
if potential_indices:
229+
if self._signal_expected_index == potential_indices[0]:
230+
if self._check_callback(self._signal_expected_index, *args):
231+
self._signals_emitted[self._signal_expected_index] = True
232+
self._signal_expected_index += 1
233+
self._strict_order_violated = False # order has not been violated after all!
234+
235+
if not self._strict_order_violated and all(self._signals_emitted):
180236
try:
181237
self.signal_triggered = True
182238
self._cleanup()
183239
finally:
184240
self._loop.quit()
185241

242+
def _check_callback(self, index, *args):
243+
"""
244+
Checks if there's a callback that evaluates the validity of the parameters. Returns False if there is one
245+
and its evaluation revealed that the parameters were invalid. Returns True otherwise.
246+
"""
247+
if self.check_params_callbacks:
248+
callback_func = self.check_params_callbacks[index]
249+
if callback_func:
250+
if not callback_func(*args):
251+
return False
252+
return True
253+
254+
def _get_unemitted_signal_indices(self, signal):
255+
"""Returns the indices for the provided signal for which NO signal instance has been emitted yet."""
256+
return [index for index in self._signals_map[signal] if self._signals_emitted[index] == False]
257+
186258
def _cleanup(self):
187259
super(MultiSignalBlocker, self)._cleanup()
188-
for signal, slot in self._slots.items():
260+
for i in range(len(self._signals)):
261+
signal = self._signals[i]
262+
slot = self._slots[i]
189263
_silent_disconnect(signal, slot)
190-
self._signals.clear()
191-
self._slots.clear()
264+
del self._signals_emitted[:]
265+
self._signals_map.clear()
266+
del self._slots[:]
192267

193268

194269
class SignalEmittedSpy(object):
195-
196270
"""
197271
.. versionadded:: 1.11
198272

0 commit comments

Comments
 (0)