diff --git a/newsfragments/134.bugfix.rst b/newsfragments/134.bugfix.rst new file mode 100644 index 00000000..b2508031 --- /dev/null +++ b/newsfragments/134.bugfix.rst @@ -0,0 +1,4 @@ +Fix an issue where a call to ``TrioEventLoop.call_exception_handler()`` after +the loop was closed would attempt to call a method on ``None``. This pattern +can be encountered if an ``aiohttp`` session is garbage-collected without being +properly closed, for example. diff --git a/tests/test_trio_asyncio.py b/tests/test_trio_asyncio.py index a86643be..18a9b5ff 100644 --- a/tests/test_trio_asyncio.py +++ b/tests/test_trio_asyncio.py @@ -45,6 +45,15 @@ async def test_get_running_loop(): assert asyncio.get_running_loop() == loop +@pytest.mark.trio +async def test_exception_after_closed(caplog): + async with trio_asyncio.open_loop() as loop: + pass + loop.call_exception_handler({"message": "Test exception after loop closed"}) + assert len(caplog.records) == 1 + assert caplog.records[0].message == "Test exception after loop closed" + + @pytest.mark.trio async def test_tasks_get_cancelled(): record = [] diff --git a/trio_asyncio/_async.py b/trio_asyncio/_async.py index e149c05f..c5e6f0ed 100644 --- a/trio_asyncio/_async.py +++ b/trio_asyncio/_async.py @@ -36,6 +36,13 @@ def default_exception_handler(self, context): # Call the original default handler so we get the full info in the log super().default_exception_handler(context) + if self._nursery is None: + # Event loop is already closed; don't do anything further. + # Some asyncio libraries call the asyncio exception handler + # from their __del__ methods, e.g., aiohttp for "Unclosed + # client session". + return + # Also raise an exception so it can't go unnoticed exception = context.get("exception") if exception is None: