We're seeing an issue where the close method of a QSelectorEventLoop doesn't completely clean up resources associated to that event loop.
Reproducer
The following code should reproduce:
import gc
import PySide6.QtGui
import qasync
async def app_main():
# Dummy application.
pass
def run_app(qt_app):
event_loop = qasync.QEventLoop(qt_app)
event_loop.run_until_complete(app_main())
event_loop.close()
qt_app = PySide6.QtGui.QGuiApplication()
for _ in range(10):
run_app(qt_app)
gc.collect()
loops = [
obj for obj in gc.get_objects()
if type(obj).__name__ == "QSelectorEventLoop"
]
print("number of event loop objects: ", len(loops))
del loops
When I run the above on my machine, I get:
(qasync) mdickinson@mirzakhani Desktop % python ~/Desktop/qasync_leak.py
number of event loop objects: 1
number of event loop objects: 2
number of event loop objects: 3
number of event loop objects: 4
number of event loop objects: 5
number of event loop objects: 6
number of event loop objects: 7
number of event loop objects: 8
number of event loop objects: 9
number of event loop objects: 10
The expectation / hope was that the number of event loops would be stable.
Diagnosis
Here's a graph showing all the ancestors of one of the leaked QSelectorEventLoop objects.

Those two lambda objects at the top have no referrers (in the sense of gc.get_referrers); they're apparently being kept alive by PySide6. Those lambdas correspond to these two lines:
|
signaller.signal.connect(lambda callback, args: self.call_soon(callback, *args)) |
|
notifier.activated["int"].connect( |
|
lambda: self.__on_notifier_ready( |
|
self._read_notifiers, notifier, fd, callback, args |
|
) # noqa: C812 |
|
) |
I believe that if we add the corresponding signal disconnects at event loop close time, then this will fix the above issue.
System details
- Python 3.10 venv
- qasync 0.23.0 installed from PyPI (via
pip)
- PySide6 6.4.2 installed from PyPI (via
pip)
- macOS 12.6.2 on an Intel MacBook Pro
Context
In case you're wondering why anyone would be creating QEventLoops repeatedly, the answer is unit testing. We're using qasync to provide an asyncio event loop for an embedded IPython kernel to use within a Qt-based application. In our test suite we have many tests that create and then tear down that asyncio event loop, and we were observing test interactions as a result of not being able to cleanly clean up all the resources associated to the event loop. (The Qt application itself is a singleton, of course, so we don't try to clean that up between tests.)
We're seeing an issue where the
closemethod of aQSelectorEventLoopdoesn't completely clean up resources associated to that event loop.Reproducer
The following code should reproduce:
When I run the above on my machine, I get:
The expectation / hope was that the number of event loops would be stable.
Diagnosis
Here's a graph showing all the ancestors of one of the leaked

QSelectorEventLoopobjects.Those two
lambdaobjects at the top have no referrers (in the sense ofgc.get_referrers); they're apparently being kept alive by PySide6. Thoselambdas correspond to these two lines:qasync/qasync/__init__.py
Line 347 in 88b64c7
qasync/qasync/__init__.py
Lines 508 to 512 in 88b64c7
I believe that if we add the corresponding signal
disconnects at event loop close time, then this will fix the above issue.System details
pip)pip)Context
In case you're wondering why anyone would be creating
QEventLoops repeatedly, the answer is unit testing. We're usingqasyncto provide an asyncio event loop for an embedded IPython kernel to use within a Qt-based application. In our test suite we have many tests that create and then tear down that asyncio event loop, and we were observing test interactions as a result of not being able to cleanly clean up all the resources associated to the event loop. (The Qt application itself is a singleton, of course, so we don't try to clean that up between tests.)