Using aiohttp with QtAsyncio
-
Hi,
I'd like to use
QtAsyncio
in my application. I want to make web requests from it, usingaiohttp
, and therefore want to have an application-wideaiohttp.ClientSession
.
I have come this far:import asyncio import sys import PySide6.QtAsyncio as QtAsyncio import aiohttp from PySide6.QtCore import Qt from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget) class AioSessionHolder: def __init__(self): loop = asyncio.get_event_loop() self.session = loop.run_until_complete(self.create_session()) @staticmethod async def create_session(): return aiohttp.ClientSession() def close(self): if self.session: loop = asyncio.new_event_loop() loop.run_until_complete(self.session.close()) self.session = None async def async_close(self): if self.session: await self.session.close() self.session = None class MainWindow(QMainWindow): def __init__(self, aio_session: AioSessionHolder): super().__init__() self.aio_session = aio_session.session widget = QWidget() self.setCentralWidget(widget) layout = QVBoxLayout(widget) self.text = QLabel("The answer is 42.") layout.addWidget(self.text, alignment=Qt.AlignmentFlag.AlignCenter) async_trigger = QPushButton(text="What is the question?") async_trigger.clicked.connect(lambda: asyncio.ensure_future(self.set_text())) layout.addWidget(async_trigger, alignment=Qt.AlignmentFlag.AlignCenter) async def set_text(self): resp = await self.aio_session.get("https://httpbin.org/get") self.text.setText(resp.status_code) if __name__ == "__main__": aio_session = AioSessionHolder() app = QApplication(sys.argv) main_window = MainWindow(aio_session) main_window.show() QtAsyncio.run()
However this has at least two problems:
- The request part does not work, I get
Timeout context manager should be used inside a task
as an error message - I was unable to integrate any session closing, without error messages (see my attempts above)
Can anybody give a minimal working example like the one above?
I would really like to use async in my application 🙂
- The request part does not work, I get
-
Hi and welcome to devnet,
Not that I don't want to help you with that module (though I haven't used it yet) but why not make use QNetworkAccessManager as it is already asynchronous and does not require an external library ?
-
Thank you for your suggestion.
First of all it didn't occur to me, that this exists.
aiohttp
seems fairly standard, that's why I found while googling for solutions to my problems.Having looked at it, I still think, that I prefer using
aiohttp
since it uses the python-native asynchronicity, which means I can useawait
instead ofSlot
s. Unless I'm mistaken, and didn't understand howQNetworkAccessManager
works?
Usingawait
makes for cleaner code imo.As for it being external: I actually think of this as a feature, as this will allow for greater separation between functionality and presentation.
-
@SGaist I am also experiencing a similar problem using the Python HTTPX package. The code simply makes an async request to an HTTP API that returns a JSON data. The code works perfectly everywhere else, but fails with QtAsyncio of PySide6.
The reason we prefer HTTPX to QNetworkAccessManager is because we come from a Python background and are more familiar with packages like HTTPX, aiohttp etc, which have much bigger community with more examples as well.
Returning to the topic. After debugging for a while, I believe our problem is the QAsyncioEventLoop class in QtAsyncio doesn't have the relavent network async events implemented, e.g. DNS, Sockets etc. In other words, the current even loop is not complete. An async HTTP request requires the even loop (QAsyncioEventLoop) to be able to handle DNS event (and more). If not, the event lool will raise a NotImplementedError exception that is not properly captured and the app just fails silently.
-
@Winton thanks for the hints. Based on the QtAsyncio TP announcement, you are likely correct about the network side of things (See the Level 1 and 2 implementations).
Which version of PySide6 are you using ?
-
@Faerbit This seems to be a known problem due to the fact, and there seems to be a library to address this problem:qasync. The following is a minimal sample I have assembled to use async/await aiohttp in a PySide6 app. It worked on my computer.
import asyncio import aiohttp import qasync from PySide6.QtCore import Qt from PySide6.QtWidgets import QApplication, QPushButton, QMainWindow, QVBoxLayout, QWidget class MainWindow(QMainWindow): def __init__(self): super().__init__() self.resize(600, 400) self.central_widget = QWidget() self.layout = QVBoxLayout() self.button = QPushButton("Fetch Data") self.button.clicked.connect(lambda : asyncio.create_task(self.fetch_data())) self.layout.addWidget(self.button, alignment=Qt.AlignCenter) self.central_widget.setLayout(self.layout) self.setCentralWidget(self.central_widget) async def fetch_data(self): self.setWindowTitle("Loading...") async with aiohttp.ClientSession() as session: async with session.get('https://reqres.in/api/users?delay=5') as response: if response.status == 200: text = await response.text() print(text) self.setWindowTitle("Got") app = QApplication([]) window = MainWindow() window.show() loop = qasync.QEventLoop(app) with loop: loop.run_forever()
-
An alternative approach is to switch policy to the default, this can be problematic if there are other events but in my case it works, nest_asyncio is required.
import nest_asyncio nest_asyncio.apply() class CustomAsyncEventPolicy: def __init__(self, policy: asyncio.AbstractEventLoopPolicy | None): self._policy = policy self._qt_policy = asyncio.get_event_loop_policy() def __enter__(self) -> None: asyncio.set_event_loop_policy(self._policy) def __exit__(self, exc_type, exc_val, exc_tb) -> None: asyncio.set_event_loop_policy(self._qt_policy) class Main(QtWidgets.QMainWindow): def __init__(self): super(Main, self).__init__() def do_something_with_native_policy(self) -> None: with CustomAsyncEventPolicy(None): self.default_async_policy_loop_call() def default_async_policy_loop_call(self): try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete( self.your_aiohttp_async_fn() ) except Exception: raise finally: if not loop.is_closed(): loop.close() def your_aiohttp_async_fn(self): ... if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) main = Main() main.show() QtAsyncio.run(keep_running=True, quit_qapp=True)
-
For the record, there is https://bugreports.qt.io/browse/PYSIDE-2713 . Contributions for this would be very welcome.