-
-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, check_params_cb=None): | ||
super(SignalBlocker, self).__init__(timeout, raising=raising) | ||
self._signals = [] | ||
self.args = None | ||
self.check_params_callback = check_params_cb | ||
|
||
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.check_params_callback: | ||
if not self.check_params_callback(*args): | ||
return # parameter check did not pass | ||
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, raising=True, check_params_cbs=None, order="none"): | ||
super(MultiSignalBlocker, self).__init__(timeout, raising=raising) | ||
self._signals = {} | ||
self._slots = {} | ||
|
||
def _add_signal(self, signal): | ||
self.order = order | ||
self.check_params_callbacks = check_params_cbs | ||
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 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.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.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.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.check_params_callbacks: | ||
callback_func = self.check_params_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._slots.clear() | ||
del self._signals_emitted[:] | ||
self._signals_map.clear() | ||
del self._slots[:] | ||
|
||
|
||
class SignalEmittedSpy(object): | ||
|
||
""" | ||
.. versionadded:: 1.11 | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 wonder if we should store the received args, so we can provide a better error message?
Consider:
conn.text_received
is emitted with['WAIT_CLIENTS', 'PROCESSING']
and eventually times out.In this situation all the user will see is a
TimeoutError
and won't have any hints at what might have gone wrong. I suppose the user would then have to addprint
statements atis_disconnected
to try to figure out the problem.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
raising
isFalse
, probably as a list of*args
:What do you guys think?
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.
Sounds good.
Another question is: should this also be implemented for
MultiSignalBlocker
? It would generally be interesting to get a string-dump (in human readable form) of all recorded signals and their parameters, including the points of time where things went wrong (e.g. in case of strict order when the order was violated). Similarly, ifraising == False
,blocker.recorded_signals
(or something similar) could contain a list of recorded signals together with their parameters.