2

I've got a situation where I would like to connect an async function that uses awaits to the clicked signal of a QPushButton.

The motivation is that I need to manage some io-bound stuff and using awaits seems to be the most cogent way to do that. The problem is that when one uses awaits, the invoking function needs to be marked as async and so do all functions that invoke THAT function. This "bubbles up to the top" until eventually, there's a button clicked signal that needs to be connected to an async slot function. I am not sure how to do that and there's not much advice. Maybe I got the wrong idea and pyQt6 isn't supposed to mix with asyncio?

Here's an example. It's just a QMainWindow with a button, and I am trying to connect the button's clicked signal to a slot function that happens to be async. It just prints numbers and uses asyncio.sleep(1.0) to wait for 1 second after printing each number.


import sys
import asyncio
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        button = QPushButton("Press This")
        self.setCentralWidget(button)
        button.clicked.connect(self.my_async_func)


    async def my_async_func(self):
        for x in range(1,10):
            print(x)
            await asyncio.sleep(1.0)
        print("all done")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()

When I click the button I get the following, and of course, nothing prints.

RuntimeWarning: coroutine 'MainWindow.my_async_func' was never awaited
  app.exec()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

How do I tell connect(...) that I want to use an async function?

My previous experience was coming from .net WPF and I was hoping that it would not be a big deal to tie an async function to a UI event.

Angelo
  • 2,936
  • 5
  • 29
  • 44

1 Answers1

0

OK, I found something that works without too much fussing.

I am not sure if it's correct. Please critique and, if possible, explain (I don't know enough about this).

The trick, for pyqt6, is to use qasync to create and start an event loop that async functions seem to be able to take as an argument. The async function also needs to be decorated with @asyncSlot().

import sys
import asyncio
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
from qasync import QEventLoop, asyncSlot


class MainWindow(QMainWindow):
    def __init__(self, loop=None):
        super().__init__()
        button = QPushButton("Press This")
        self.setCentralWidget(button)
        button.clicked.connect(self.my_async_func)
        self.loop = loop or asyncio.get_event_loop()

    @asyncSlot()
    async def my_async_func(self):
        for x in range(1,10):
            print(x)
            await asyncio.sleep(1.0,self.loop)
        print("all done")


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

    window = MainWindow(loop)
    window.show()
    
    with loop:
        loop.run_forever()



if __name__ == '__main__':
    main()


Angelo
  • 2,936
  • 5
  • 29
  • 44
  • I keep getting this: "AttributeError: 'QApplication' object has no attribute 'exec_'", does it ring a bell? My MainWindow inherits from both QMainWindow and my generated ui window class. – Gauthier Apr 13 '23 at 15:28