Search code examples
pythontcppyqt5python-asynciocoroutine

How do I add asyncio task to pyqt5 event loop so that it runs and avoids the never awaited error?


I am new to asyncio and I want to leverage it for my pyqt application to handle communication over a tcp connection.

I made this trivial demo to learn how to deal with the QT loop in asyncio context. I have seen other post related to this but they are much more complicated than what I am trying to do at the moment. So I start the server client in a separate window so it listens and I try to send a message through my simple button click event on my widget. As barebones as it gets.... My problem is it does not work.

I am looking to be able to a single exchange of info and a case where the port is left open for a stream. I think these tasks would be straight forward in asyncio but having it play nice with qt seems difficult at this point.

right now I am getting

RuntimeWarning: coroutine 'PushButton.sendmessage' was never awaited
  rslt = self.__app.exec_()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

I am not sure where to start on fixing this.

test.py

from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, pyqtSlot
import sys
import asyncio
from asyncqt import QEventLoop
from async_client import tcp_echo_client


class PushButton(QWidget):

    loop = None

    def __init__(self,app_loop):
        super(PushButton,self).__init__()
        self.initUI()
        self.loop = loop

    def initUI(self):
        self.setWindowTitle("PushButton")
        self.setGeometry(400,400,300,260)
        self.send_bttn = QPushButton(self)
        self.send_bttn.setText("SEnd Message")      
        self.send_bttn.clicked.connect(self.sendmessage)
        self.send_bttn.move(100,100)
        
    @pyqtSlot()    
    async def sendmessage(self):
        run_command = asyncio.create_task(tcp_echo_client("hey",self_loop))
        asyncio.run(run_command)



if __name__ == '__main__':
    app = QApplication(sys.argv)
    loop = QEventLoop(app)
    asyncio.set_event_loop(loop)  # NEW must set the event loop    sys.exit(app.exec_()) 
    
    ex = PushButton(loop)
    ex.show()
    with loop:
        loop.run_forever()

the simple echo client routine

import asyncio


async def tcp_echo_client(message, loop):
    reader, writer = await asyncio.open_connection('127.0.0.1', 8889,
                                                   loop=loop)

    print('Send: %r' % message)
    writer.write(message.encode())

    data = await reader.read(100)
    print('Received: %r' % data.decode())

    print('Close the socket')
    writer.close()

and the responding server


import asyncio

async def handle_echo(reader, writer):
    data = await reader.read(100)
    message = data.decode()
    addr = writer.get_extra_info('peername')
    print("Received %r from %r" % (message, addr))

    print("Send: %r" % message)
    writer.write(data)
    await writer.drain()

    print("Close the client socket")
    writer.close()

loop = asyncio.get_event_loop()
coro = asyncio.start_server(handle_echo, '127.0.0.1', 8889, loop=loop)
server = loop.run_until_complete(coro)

# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()


Solution

  • The problem is that if you invoke a slot that is a coroutine then you must use the asyncSlot decorator, also do not use ayncion.run() but await(In addition to eliminating other typos).

    import sys
    import asyncio
    
    from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
    
    from asyncqt import QEventLoop, asyncSlot
    from async_client import tcp_echo_client
    
    
    class PushButton(QWidget):
    
        loop = None
    
        def __init__(self, loop=None):
            super(PushButton, self).__init__()
            self.initUI()
            self.loop = loop or asyncio.get_event_loop()
    
        def initUI(self):
            self.setWindowTitle("PushButton")
            self.setGeometry(400, 400, 300, 260)
            self.send_bttn = QPushButton(self)
            self.send_bttn.setText("SEnd Message")
            self.send_bttn.clicked.connect(self.sendmessage)
            self.send_bttn.move(100, 100)
    
        @asyncSlot()
        async def sendmessage(self):
            await tcp_echo_client("hey", self.loop)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        loop = QEventLoop(app)
        asyncio.set_event_loop(loop)
    
        ex = PushButton(loop)
        ex.show()
        with loop:
            loop.run_forever()