I'm trying to create a new application based on PyQt5 and asyncio (with python 3.4, looking forward to eventually upgrade to 3.5 with async/await). My goal is to use asyncio so that the GUI stays responsive even when the application is waiting for some connected hardware to finish an operation.
When looking how to merge the event loops of Qt5 and asyncio, I found a mailing list posting, suggesting to use quamash. However, when running this example (unmodified), the
yield from fut
nevers seems to return. I see the output 'Timeout', so the timer callback obviously fires, but the Future fails to wake up the waiting method. When manually closing the window, it tells me that there are uncompleted 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 with 3.4, same behaviour on both platforms.
Anyway, since this is not what I actually try to achieve, I tested some other code as well:
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())
The program is supposed to display a window with a button in it (which it does), with the button invoking slow_operation() without blocking the GUI. When running this example, I can click the button as often as I want, so the GUI is not blocked. But the
yield from asyncio.sleep(0.1)
is never passed and the terminal output looks like this:
btnCallback returns...
clicked
op()
op done
btnCallback returns...
clicked
op()
op done
There is no exception thrown when I close the window this time. The slow_operation() function basically works if I directly run the 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 sensible way to achieve decoupling of lengthy operations from the GUI, generally? My intention is that the button callback posts the coroutine call to the event loop (with or without an additional level of nesting, cf. coroCallHelper()), where it is then scheduled and executed. I don't need separate threads, as it is really only I/O that takes time, no actual processing.
How can I fix this behaviour?
Thanks, Philipp