PyQt5 and asyncio: productivity never ends

I am trying to create a new application based on PyQt5 and asyncio (since python 3.4, I look forward to eventually upgrading to 3.5 using async / await). My goal is to use asyncio so that the graphical interface remains responsive even when the application expects some connected equipment to complete the operation.

When you look at how to combine Qt5 and asyncio event loops, I found a mailing list offering to use quamash . However, when running this example (unmodified),

yield from fut 

Nevers seems to be returning. I see the Timeout output, so the timer callback obviously works, but the future cannot wake the wait method. When manually closing a window, it tells me that there are unfinished futures:

 Yielding until signal... Timeout Traceback (most recent call last): File "pyqt_asyncio_list.py", line 26, in <module> loop.run_until_complete(_go()) File "/usr/local/lib/python3.5/site-packages/quamash/__init__.py", line 291, in run_until_complete raise RuntimeError('Event loop stopped before Future completed.') RuntimeError: Event loop stopped before Future completed. 

I tested this on Ubuntu with python 3.5 and on Windows 3.4, the same behavior on both platforms.

In any case, since this is not what I'm actually trying to achieve, I also checked another code:

 import quamash import asyncio from PyQt5.QtWidgets import * from PyQt5.QtCore import * @asyncio.coroutine def op(): print('op()') @asyncio.coroutine def slow_operation(): print('clicked') yield from op() print('op done') yield from asyncio.sleep(0.1) print('timeout expired') yield from asyncio.sleep(2) print('second timeout expired') def coroCallHelper(coro): asyncio.ensure_future(coro(), loop=loop) class Example(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): def btnCallback(obj): #~ loop.call_soon(coroCallHelper, slow_operation) asyncio.ensure_future(slow_operation(), loop=loop) print('btnCallback returns...') btn = QPushButton('Button', self) btn.resize(btn.sizeHint()) btn.move(50, 50) btn.clicked.connect(btnCallback) self.setGeometry(300, 300, 300, 200) self.setWindowTitle('Async') self.show() with quamash.QEventLoop(app=QApplication([])) as loop: w = Example() loop.run_forever() #~ loop = asyncio.get_event_loop() #~ loop.run_until_complete(slow_operation()) 

It is assumed that the program should display a window with a button in it (what it does), while the button calls slow_operation () without locking the GUI. When I run this example, I can click the button as often as I want, so the GUI is not blocked. But

 yield from asyncio.sleep(0.1) 

never transmitted, and the terminal output looks like this:

 btnCallback returns... clicked op() op done btnCallback returns... clicked op() op done 

No exception occurs when I close the window this time. The slow_operation () function basically works if I directly start an event loop with it:

 #~ with quamash.QEventLoop(app=QApplication([])) as loop: #~ w = Example() #~ loop.run_forever() loop = asyncio.get_event_loop() loop.run_until_complete(slow_operation()) 

Now two questions:

  • Is this a smart way to get the long-running operations of the GUI decoupled? My intention is that the button callback sends the coroutine call to the event loop (with or without an additional nesting level, cf. coroCallHelper ()), where it is then scheduled and executed. I do not need separate threads, since in fact only I / O takes time, not the actual processing.

  • How can I fix this behavior?

Thanks Philipp

+5
source share
1 answer

It's good that one plus SO: Ask a question makes you think about everything. I somehow figured this out:

Again, looking at an example from quamash repo , I found that the event loop for use is slightly different:

 app = QApplication(sys.argv) loop = QEventLoop(app) asyncio.set_event_loop(loop) # NEW must set the event loop # ... with loop: loop.run_until_complete(master()) 

The key seems to be asyncio.set_event_loop() . It is also important to note that the QEventLoop mentioned is the one specified in the quamash package, not Qt5. So now my example is as follows:

 import sys import quamash import asyncio from PyQt5.QtWidgets import * from PyQt5.QtCore import * @asyncio.coroutine def op(): print('op()') @asyncio.coroutine def slow_operation(): print('clicked') yield from op() print('op done') yield from asyncio.sleep(0.1) print('timeout expired') yield from asyncio.sleep(2) print('second timeout expired') loop.stop() def coroCallHelper(coro): asyncio.ensure_future(coro(), loop=loop) class Example(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): def btnCallback(obj): #~ loop.call_soon(coroCallHelper, slow_operation) asyncio.ensure_future(slow_operation(), loop=loop) print('btnCallback returns...') btn = QPushButton('Button', self) btn.resize(btn.sizeHint()) btn.move(50, 50) btn.clicked.connect(btnCallback) self.setGeometry(300, 300, 300, 200) self.setWindowTitle('Async') self.show() app = QApplication(sys.argv) loop = quamash.QEventLoop(app) asyncio.set_event_loop(loop) # NEW must set the event loop with loop: w = Example() w.show() loop.run_forever() print('Coroutine has ended') 

And now it just works:

 btnCallback returns... clicked op() op done timeout expired second timeout expired Coroutine has ended 

Perhaps this helps others. I am pleased with this, at least;) Of course, comments on the general outline are still welcome!

Regards, Philipp

+7
source

All Articles