question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

What is the intended way of blocking synchronous slots until some async. code is done?

See original GitHub issue

Sometimes it is required to wait for result of asynchronous operation in synchronous Qt slot, is there any proper way to do it?

For example I have QMainWindow subclass that has QWebEngineView as one of it’s child, QWebEngineView runs some JavaScript client code, and my application communicates with it asynchronously using WebSockets. Now I want to request from QWebEngineView some data to store on application exit: in overridden QMainWindow::closeEvent().

I can’t create async. task and let closeEvent() to finish, since the main window will destroy QWebEngineView on close, and most probably I won’t get results in time. I can’t use BaseEventLoop.run_until_complete() with my async. call, since event loop is already running and is not reentrant. I can’t create second thread, start second event loop there, call async. method in the second event loop, and in closeEvent() wait when the thread will finish, since my web socket is already managed by event loop in the main thread.

My current solution is to postpone the main window closing by ignoring first close event, start async. task, and call QMainWindow::close() second time manually from async. task when work is done. This solution works in closeEvent(), since it’s possible to postpone closing, but will not in most of other synchronous slots.

Issue Analytics

  • State:open
  • Created 8 years ago
  • Comments:12 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
gmarullcommented, Sep 23, 2018

Maybe more related to #11, but I’ve found this syntax (asyncSlot()) quite useful:

import sys
import asyncio
import functools
import random

from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout
from quamash import QEventLoop


app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)


def asyncSlot(*args):
    def real_decorator(fn):
        @pyqtSlot(*args)
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            asyncio.ensure_future(fn(*args, **kwargs))
        return wrapper
    return real_decorator


class Window(QWidget):
    def __init__(self):
        super().__init__()
    
        self.setLayout(QHBoxLayout())

        self.btn1 = QPushButton('Ready', parent=self)
        self.btn1.clicked.connect(self.on_btn1_clicked)
        self.layout().addWidget(self.btn1)

        self.btn2 = QPushButton('Test', parent=self)
        self.layout().addWidget(self.btn2)

    @asyncSlot()
    async def on_btn1_clicked(self):
        try:
            self.btn1.setEnabled(False)
            self.btn1.setText('Running...')
            await self.task()
            self.btn1.setText('Done!')
        except Exception:
            self.btn1.setText('Error!')
        finally:
            self.btn1.setEnabled(True)

    async def task(self):
        await asyncio.sleep(1)
        if bool(random.getrandbits(1)):
            raise Exception('Oooops a random exception!')



w = Window()
w.show()


with loop:
    loop.run_forever()
0reactions
danieljfarrellcommented, Sep 5, 2019

Cancelling the task?

@gmarull I really like this solution! Thanks.

Can you think of a way of cancelling the task created by asyncio.ensure_future?

The only way I could think of doing it is below.

When the cancel button is pressed I search all running tasks and match the task’s coroutine’s __qualname__ attribute to that of async def method name on the object. It’s pretty gross.

(In asyncSlot I also added a done callback on the task so that any exceptions can be caught).

import sys
import asyncio
import functools
import random
import inspect
from PyQt5.QtCore import pyqtSlot, pyqtRemoveInputHook
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout
from quamash import QEventLoop


app = QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)


def debug_trace():
    """Set a tracepoint in the Python debugger that works with Qt"""
    pyqtRemoveInputHook()
    import pdb; pdb.set_trace()


def asyncSlot(*args):

    def log_error(future):
        try:
            future.result()
        except asyncio.CancelledError:
            pass
        except Exception:
            # Add better error handling
            traceback.print_exc()

    def helper(coro):
        if not inspect.iscoroutinefunction(coro):
            raise RuntimeError('Must be a coroutine!')

        @pyqtSlot(*args)
        @functools.wraps(coro)
        def wrapper(self, *args, **kwargs):
            loop = asyncio.get_event_loop()
            future = loop.create_task(coro(self, *args, **kwargs))
            future.add_done_callback(log_error)
        return wrapper

    return helper


class Window(QWidget):
    def __init__(self):
        super().__init__()
    
        self.setLayout(QHBoxLayout())

        self.btn1 = QPushButton('Ready', parent=self)
        self.btn1.clicked.connect(self.onRun)
        self.layout().addWidget(self.btn1)

        self.btn2 = QPushButton('Cancel', parent=self)
        self.btn2.clicked.connect(self.onCancel)
        self.layout().addWidget(self.btn2)


    @asyncSlot()
    async def onRun(self):
        try:
            self.btn1.setEnabled(False)
            self.btn1.setText('Running...')
            await self.task()
            self.btn1.setText('Done!')
        except asyncio.CancelledError:
            self.btn1.setText('Cancelled!')
        except Exception:
            self.btn1.setText('Error!')
        finally:
            self.btn1.setEnabled(True)

    @pyqtSlot()
    def onCancel(self):
        print("Cancelling.")
        for t in asyncio.Task.all_tasks():
            if t._coro.__qualname__ == self.onRun.__qualname__:
                t.cancel()
                print("Cancelled.")
                break

    async def task(self):
        await asyncio.sleep(1)
        if bool(random.getrandbits(1)):
            raise Exception('Oooops a random exception!')



w = Window()
w.show()


with loop:
    loop.run_forever()
Read more comments on GitHub >

github_iconTop Results From Across the Web

Getting Started With Async Features in Python
A synchronous program is executed one step at a time. Even with conditional branching, loops and function calls, you can still think about...
Read more >
Async-await behaving synchronously - Stack Overflow
I have implemented a small piece of asynchronous code and I face a weird behavior. Basically what I want to do is run...
Read more >
JavaScript Concurrency: Avoiding the Sequential Trap
As mentioned earlier, the main use case for promises is to defer the execution of code until the requested data is ready to...
Read more >
How to embrace asynchronous communication for remote work
Here is a complete guide to everything you need to know about how to work and communicate asynchronously in a remote work environment....
Read more >
Log4j 2 Lock-free Asynchronous Loggers for Low-Latency ...
The latter elements are intended for mixing async with sync loggers. If you use both mechanisms together you will end up with two...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found