Skip to content

Commit bad3a3b

Browse files
authored
Merge pull request #141 from MShekow/master
waitSignal and waitSignals() evaluate signal parameters
2 parents 9fa837c + 2e21419 commit bad3a3b

File tree

3 files changed

+391
-32
lines changed

3 files changed

+391
-32
lines changed

pytestqt/qtbot.py

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

235235
stop = stopForInteraction
236236

237-
def waitSignal(self, signal=None, timeout=1000, raising=None):
237+
def waitSignal(self, signal=None, timeout=1000, raising=None, check_params_cb=None):
238238
"""
239239
.. versionadded:: 1.2
240240
@@ -270,6 +270,10 @@ def waitSignal(self, signal=None, timeout=1000, raising=None):
270270
should be raised if a timeout occurred.
271271
This defaults to ``True`` unless ``qt_wait_signal_raising = false``
272272
is set in the config.
273+
:param Callable check_params_cb:
274+
Optional callable(*parameters) that compares the provided signal parameters to some expected parameters.
275+
It has to match the signature of ``signal`` (just like a slot function would) and return ``True`` if
276+
parameters match, ``False`` otherwise.
273277
:returns:
274278
``SignalBlocker`` object. Call ``SignalBlocker.wait()`` to wait.
275279
@@ -285,14 +289,14 @@ def waitSignal(self, signal=None, timeout=1000, raising=None):
285289
raising = True
286290
else:
287291
raising = _parse_ini_boolean(raising_val)
288-
blocker = SignalBlocker(timeout=timeout, raising=raising)
292+
blocker = SignalBlocker(timeout=timeout, raising=raising, check_params_cb=check_params_cb)
289293
if signal is not None:
290294
blocker.connect(signal)
291295
return blocker
292296

293297
wait_signal = waitSignal # pep-8 alias
294298

295-
def waitSignals(self, signals=None, timeout=1000, raising=None):
299+
def waitSignals(self, signals=None, timeout=1000, raising=None, check_params_cbs=None, order="none"):
296300
"""
297301
.. versionadded:: 1.4
298302
@@ -315,15 +319,28 @@ def waitSignals(self, signals=None, timeout=1000, raising=None):
315319
blocker.wait()
316320
317321
:param list signals:
318-
A list of :class:`Signal` objects to wait for. Set to ``None`` to
319-
just use timeout.
322+
A list of :class:`Signal` objects to wait for. Set to ``None`` to just use
323+
timeout.
320324
:param int timeout:
321325
How many milliseconds to wait before resuming control flow.
322326
:param bool raising:
323327
If :class:`QtBot.SignalTimeoutError <pytestqt.plugin.SignalTimeoutError>`
324328
should be raised if a timeout occurred.
325329
This defaults to ``True`` unless ``qt_wait_signal_raising = false``
326330
is set in the config.
331+
:param list check_params_cbs:
332+
optional list of callables that compare the provided signal parameters to some expected parameters.
333+
Each callable has to match the signature of the corresponding signal in ``signals`` (just like a slot
334+
function would) and return ``True`` if parameters match, ``False`` otherwise.
335+
Instead of a specific callable, ``None`` can be provided, to disable parameter checking for the
336+
corresponding signal.
337+
If the number of callbacks doesn't match the number of signals ``ValueError`` will be raised.
338+
:param str order: determines the order in which to expect signals
339+
* ``"none"``: no order is enforced
340+
* ``"strict"``: signals have to be emitted strictly in the provided order
341+
(e.g. fails when expecting signals [a, b] and [a, a, b] is emitted)
342+
* ``"simple"``: like "strict", but signals may be emitted in-between the provided ones, e.g. expected
343+
``signals`` == [a, b, c] and actually emitted signals = [a, a, b, a, c] works (would fail with "strict")
327344
:returns:
328345
``MultiSignalBlocker`` object. Call ``MultiSignalBlocker.wait()``
329346
to wait.
@@ -334,12 +351,19 @@ def waitSignals(self, signals=None, timeout=1000, raising=None):
334351
335352
.. note:: This method is also available as ``wait_signals`` (pep-8 alias)
336353
"""
354+
if order not in ["none", "simple", "strict"]:
355+
raise ValueError("order has to be set to 'none', 'simple' or 'strict'")
356+
337357
if raising is None:
338358
raising = self._request.config.getini('qt_wait_signal_raising')
339-
blocker = MultiSignalBlocker(timeout=timeout, raising=raising)
359+
360+
if check_params_cbs:
361+
if len(check_params_cbs) != len(signals):
362+
raise ValueError("Number of callbacks ({}) does not "
363+
"match number of signals ({})!".format(len(check_params_cbs), len(signals)))
364+
blocker = MultiSignalBlocker(timeout=timeout, raising=raising, order=order, check_params_cbs=check_params_cbs)
340365
if signals is not None:
341-
for signal in signals:
342-
blocker._add_signal(signal)
366+
blocker.add_signals(signals)
343367
return blocker
344368

345369
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)