-
Notifications
You must be signed in to change notification settings - Fork 46
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
Support awaiting on qt signals #90
Comments
I think it would be beneficial to get some convenience functionality for bringing Qt signals to the async domain. IMHO this isn't as trivial as one might expect, since a there are two pitfalls in this:
If those two points are adressed, we can use Example usage: # Await signal (forever)
await future_from_signal(bn.clicked)
# Await signal for 1s, automatically raise/cancel/cleanup on timeout
await asyncio.wait_for(future_from_signal(bn.clicked), timeout=1.0)
# Await the first of two signals
sig1, sig2 = future_from_signal(bn1.clicked), future_from_signal(bn2.clicked)
await asyncio.wait({sig1, sig2}, return_when=asyncio.FIRST_COMPLETED)
# ... check which one fired, cancel() the other ...
# Await signal from potentially volatile object
try:
await future_from_signal(obj.signal, obj)
except SenderDestroyed:
# ... sender destroyed before signal was emitted ... I think this should work (corrections/suggestions?): class SenderDestroyed(Exception):
"""
Exception raised on signal source destruction.
"""
def __init__(self):
super(SenderDestroyed, self).__init__("Sender object was destroyed")
class _SignalFutureAdapter:
__slots__ = ("_signal", "_future", "_sender", "__weakref__")
def __init__(self,
signal: QtCore.pyqtBoundSignal,
sender: QtCore.QObject):
self._signal = signal
self._future = asyncio.get_event_loop().create_future()
self._sender = sender
signal.connect(self._on_signal)
sender.destroyed.connect(self._on_destroyed)
self._future.add_done_callback(self._done_callback)
def _on_signal(self, *args):
if not self._future.done():
self._future.set_result(None if len(args) is 0 else args[0] if len(args) is 1 else args)
def _on_destroyed(self):
self._future.remove_done_callback(self._done_callback)
self._future.set_exception(SenderDestroyed())
def _done_callback(self, _):
self._signal.disconnect(self._on_signal)
self._sender.destroyed.disconnect(self._on_destroyed)
def future(self) -> asyncio.Future:
return self._future
def _on_signal(future: asyncio.Future, *args):
if not future.done():
future.set_result(None if len(args) is 0 else args[0] if len(args) is 1 else args)
def future_from_signal(signal: QtCore.pyqtBoundSignal,
sender: QtCore.QObject = None) -> asyncio.Future:
"""
Create Future for awaiting a Qt signal.
Providing a sender object as second argument guards the Future
against premature destruction of sender, in which case a
`SenderDestroyed` exception is raised.
:param signal: Qt signal.
:param sender: Sender object for observing destroyed signal (optional).
:return: Future for awaiting signal.
"""
if sender is None:
future = asyncio.get_event_loop().create_future()
on_signal = functools.partial(_on_signal, future)
signal.connect(on_signal)
future.add_done_callback(lambda f: signal.disconnect(on_signal))
return future
adapter = _SignalFutureAdapter(signal, sender)
return adapter.future() If there is interest in having this in |
I did some experimenting with quamash, and I think it's fairly easy to make an awaitable wrapper for Qt signals. While it's not part of a regular asyncio event loop, it would make working with pure PyQt code (like QNetworkReply or QDialog) a bit easier.
I made an experimental branch at https://github.com/Wesmania/quamash/tree/non-compliant-stuff that cuts quamash down to a small, non-compliant event loop with support for awaiting on signals. There's still some stuff to be worked out (cleaning up references to awaited-on signals at cancel, wrapping slots in call_soon to avoid growing the stack too much, custom future with "done" signal), but it can serve as a proof-of-concept.
The text was updated successfully, but these errors were encountered: