-
-
Notifications
You must be signed in to change notification settings - Fork 69
waitSignal and waitSignals() evaluate signal parameters #141
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -234,7 +234,7 @@ def stopForInteraction(self): | |
|
||
stop = stopForInteraction | ||
|
||
def waitSignal(self, signal=None, timeout=1000, raising=None): | ||
def waitSignal(self, signal=None, timeout=1000, callback=None, raising=None): | ||
""" | ||
.. versionadded:: 1.2 | ||
|
||
|
@@ -265,6 +265,8 @@ def waitSignal(self, signal=None, timeout=1000, raising=None): | |
A signal to wait for. Set to ``None`` to just use timeout. | ||
:param int timeout: | ||
How many milliseconds to wait before resuming control flow. | ||
:param Callable callback: | ||
Optional callable(*args) that returns True if the parameters are as expected, or False otherwise. | ||
:param bool raising: | ||
If :class:`QtBot.SignalTimeoutError <pytestqt.plugin.SignalTimeoutError>` | ||
should be raised if a timeout occurred. | ||
|
@@ -285,14 +287,14 @@ def waitSignal(self, signal=None, timeout=1000, raising=None): | |
raising = True | ||
else: | ||
raising = _parse_ini_boolean(raising_val) | ||
blocker = SignalBlocker(timeout=timeout, raising=raising) | ||
blocker = SignalBlocker(timeout=timeout, raising=raising, callback=callback) | ||
if signal is not None: | ||
blocker.connect(signal) | ||
return blocker | ||
|
||
wait_signal = waitSignal # pep-8 alias | ||
|
||
def waitSignals(self, signals=None, timeout=1000, raising=None): | ||
def waitSignals(self, signals=None, timeout=1000, force_order = "none", callbacks = None, raising=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: No spaces around default values There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also I think I'd rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm OK with renaming the parameter to |
||
""" | ||
.. versionadded:: 1.4 | ||
|
||
|
@@ -315,10 +317,19 @@ def waitSignals(self, signals=None, timeout=1000, raising=None): | |
blocker.wait() | ||
|
||
:param list signals: | ||
A list of :class:`Signal` objects to wait for. Set to ``None`` to | ||
just use timeout. | ||
A list of :class:`Signal`s objects to wait for. Set to ``None`` to just use | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That added "s" is wrong 😉 |
||
timeout. | ||
:param int timeout: | ||
How many milliseconds to wait before resuming control flow. | ||
:param str force_order: | ||
"none": no order is enforced | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
"strict": the signals have to be emitted in the provided order (and no intermediate signals may be emitted) | ||
"simple": like "strict", but signals may be emitted inbetween the provided ones, e.g. suppose | ||
signals=[a, b, c] and the tested object emits the signals a, a, b, a, c, the test will pass with "simple" | ||
but not with "strict" | ||
:param list callbacks: | ||
optional list of Callable functions that evaluate the parameters of the signal. Each element of the list | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Callable functions" sounds weird - either "list of callables" or "list of functions" IMHO. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like "list of callables", as "callable" is a Python term. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not very clear from the docs here what the callbacks are supposed to do with the signals they receive. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, here is a suggestion:
|
||
can be a callable or None. Make sure that the number of callbacks matches the number of signals. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I suggest make it explicit about the
|
||
:param bool raising: | ||
If :class:`QtBot.SignalTimeoutError <pytestqt.plugin.SignalTimeoutError>` | ||
should be raised if a timeout occurred. | ||
|
@@ -334,12 +345,18 @@ def waitSignals(self, signals=None, timeout=1000, raising=None): | |
|
||
.. note:: This method is also available as ``wait_signals`` (pep-8 alias) | ||
""" | ||
assert force_order in ["none", "simple", "strict"], "force_order has to be set to 'none', 'simple' or 'strict'" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a |
||
|
||
if raising is None: | ||
raising = self._request.config.getini('qt_wait_signal_raising') | ||
blocker = MultiSignalBlocker(timeout=timeout, raising=raising) | ||
|
||
if callbacks: | ||
if len(callbacks) != len(signals): | ||
raise ValueError("Number of callbacks ({}) does not " | ||
"match number of signals ({})!".format(len(callbacks), len(signals))) | ||
blocker = MultiSignalBlocker(timeout=timeout, raising=raising, force_order=force_order, callbacks=callbacks) | ||
if signals is not None: | ||
for signal in signals: | ||
blocker._add_signal(signal) | ||
blocker.add_signals(signals) | ||
return blocker | ||
|
||
wait_signals = waitSignals # pep-8 alias | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,6 @@ | |
|
||
|
||
class _AbstractSignalBlocker(object): | ||
|
||
""" | ||
Base class for :class:`SignalBlocker` and :class:`MultiSignalBlocker`. | ||
|
||
|
@@ -71,7 +70,6 @@ def __exit__(self, type, value, traceback): | |
|
||
|
||
class SignalBlocker(_AbstractSignalBlocker): | ||
|
||
""" | ||
Returned by :meth:`pytestqt.qtbot.QtBot.waitSignal` method. | ||
|
||
|
@@ -101,10 +99,11 @@ class SignalBlocker(_AbstractSignalBlocker): | |
.. automethod:: connect | ||
""" | ||
|
||
def __init__(self, timeout=1000, raising=True): | ||
def __init__(self, timeout=1000, raising=True, callback=None): | ||
super(SignalBlocker, self).__init__(timeout, raising=raising) | ||
self._signals = [] | ||
self.args = None | ||
self.callback = callback | ||
|
||
def connect(self, signal): | ||
""" | ||
|
@@ -123,6 +122,9 @@ def _quit_loop_by_signal(self, *args): | |
""" | ||
quits the event loop and marks that we finished because of a signal. | ||
""" | ||
if self.callback: | ||
if not self.callback(*args): | ||
return # parameter check did not pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should store the received args, so we can provide a better error message? Consider: def is_disconnected(text):
return text == 'END'
with qtbot.waitSignal(conn.text_received, callback=is_disconnected):
conn.request_disconnect()
In this situation all the user will see is a I think if we store the received arguments when there is a callback, we can reuse that later in the exception message:
That idea also implies that the same arguments should be available for inspection later in case blocker.all_args == ['WAIT_CLIENTS', 'PROCESSING'] What do you guys think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. |
||
try: | ||
self.signal_triggered = True | ||
self.args = list(args) | ||
|
@@ -138,7 +140,6 @@ def _cleanup(self): | |
|
||
|
||
class MultiSignalBlocker(_AbstractSignalBlocker): | ||
|
||
""" | ||
Returned by :meth:`pytestqt.qtbot.QtBot.waitSignals` method, blocks until | ||
all signals connected to it are triggered or the timeout is reached. | ||
|
@@ -151,48 +152,121 @@ class MultiSignalBlocker(_AbstractSignalBlocker): | |
.. automethod:: wait | ||
""" | ||
|
||
def __init__(self, timeout=1000, raising=True): | ||
def __init__(self, timeout=1000, force_order="none", callbacks=None, raising=True): | ||
super(MultiSignalBlocker, self).__init__(timeout, raising=raising) | ||
self._signals = {} | ||
self._slots = {} | ||
|
||
def _add_signal(self, signal): | ||
self.force_order = force_order | ||
self.callbacks = callbacks | ||
self._signals_emitted = [] # list of booleans, indicates whether the signal was already emitted | ||
self._signals_map = {} # maps from a unique Signal to a list of indices where to expect signal instance emits | ||
self._signals = [] # list of all Signals (for compatibility with _AbstractSignalBlocker) | ||
self._slots = [] # list of slot functions | ||
self._signal_expected_index = 0 # only used when forcing order | ||
self._strict_order_violated = False | ||
|
||
def add_signals(self, signals): | ||
""" | ||
Adds the given signal to the list of signals which :meth:`wait()` waits | ||
for. | ||
|
||
:param signal: QtCore.Signal | ||
:param list signals: list of QtCore.Signal`s | ||
""" | ||
self._signals[signal] = False | ||
slot = functools.partial(self._signal_emitted, signal) | ||
self._slots[signal] = slot | ||
signal.connect(slot) | ||
# determine uniqueness of signals, creating a map that maps from a unique signal to a list of indices | ||
# (positions) where this signal is expected (in case force_order matters) | ||
signals_as_str = [str(signal) for signal in signals] | ||
signal_str_to_signal = {} # maps from a signal-string to one of the signal instances (the first one found) | ||
for index, signal_str in enumerate(signals_as_str): | ||
signal = signals[index] | ||
if signal_str not in signal_str_to_signal: | ||
signal_str_to_signal[signal_str] = signal | ||
self._signals_map[signal] = [index] # create a new list | ||
else: | ||
# append to existing list | ||
first_signal_that_occurred = signal_str_to_signal[signal_str] | ||
self._signals_map[first_signal_that_occurred].append(index) | ||
|
||
def _signal_emitted(self, signal): | ||
for signal in signals: | ||
self._signals_emitted.append(False) | ||
|
||
for unique_signal in self._signals_map: | ||
slot = functools.partial(self._signal_emitted, unique_signal) | ||
self._slots.append(slot) | ||
unique_signal.connect(slot) | ||
self._signals.append(unique_signal) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated whitespace change |
||
def _signal_emitted(self, signal, *args): | ||
""" | ||
Called when a given signal is emitted. | ||
|
||
If all expected signals have been emitted, quits the event loop and | ||
marks that we finished because signals. | ||
""" | ||
self._signals[signal] = True | ||
if all(self._signals.values()): | ||
if self.force_order == "none": | ||
# perform the test for every matching index (stop after the first one that matches) | ||
successfully_emitted = False | ||
successful_index = -1 | ||
potential_indices = self._get_unemitted_signal_indices(signal) | ||
for potential_index in potential_indices: | ||
if self._check_callback(potential_index, *args): | ||
successful_index = potential_index | ||
successfully_emitted = True | ||
break | ||
|
||
if successfully_emitted: | ||
self._signals_emitted[successful_index] = True | ||
elif self.force_order == "simple": | ||
potential_indices = self._get_unemitted_signal_indices(signal) | ||
if potential_indices: | ||
if self._signal_expected_index == potential_indices[0]: | ||
if self._check_callback(self._signal_expected_index, *args): | ||
self._signals_emitted[self._signal_expected_index] = True | ||
self._signal_expected_index += 1 | ||
else: # self.force_order == "strict" | ||
if not self._strict_order_violated: | ||
# only do the check if the strict order has not been violated yet | ||
self._strict_order_violated = True # assume the order has been violated this time | ||
potential_indices = self._get_unemitted_signal_indices(signal) | ||
if potential_indices: | ||
if self._signal_expected_index == potential_indices[0]: | ||
if self._check_callback(self._signal_expected_index, *args): | ||
self._signals_emitted[self._signal_expected_index] = True | ||
self._signal_expected_index += 1 | ||
self._strict_order_violated = False # order has not been violated after all! | ||
|
||
if not self._strict_order_violated and all(self._signals_emitted): | ||
try: | ||
self.signal_triggered = True | ||
self._cleanup() | ||
finally: | ||
self._loop.quit() | ||
|
||
def _check_callback(self, index, *args): | ||
""" | ||
Checks if there's a callback that evaluates the validity of the parameters. Returns False if there is one | ||
and its evaluation revealed that the parameters were invalid. Returns True otherwise. | ||
""" | ||
if self.callbacks: | ||
callback_func = self.callbacks[index] | ||
if callback_func: | ||
if not callback_func(*args): | ||
return False | ||
return True | ||
|
||
def _get_unemitted_signal_indices(self, signal): | ||
"""Returns the indices for the provided signal for which NO signal instance has been emitted yet.""" | ||
return [index for index in self._signals_map[signal] if self._signals_emitted[index] == False] | ||
|
||
def _cleanup(self): | ||
super(MultiSignalBlocker, self)._cleanup() | ||
for signal, slot in self._slots.items(): | ||
for i in range(len(self._signals)): | ||
signal = self._signals[i] | ||
slot = self._slots[i] | ||
_silent_disconnect(signal, slot) | ||
self._signals.clear() | ||
self._signals_emitted.clear() | ||
self._signals_map.clear() | ||
self._slots.clear() | ||
|
||
|
||
class SignalEmittedSpy(object): | ||
|
||
""" | ||
.. versionadded:: 1.11 | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this parameter should appear at the end, so code doing
waitSignal(signal, 1000, True)
won't break.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I feel
callback
is to generic... perhapscheck_params_callback
would convey the intent a little better?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind this rename.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check_params_cb
maybe? I think_cb
is a common suffix, andcheck_params_callback
seems so long 😉