From 0cbbda24fb2919c21e803580c4c5fe447bab3fed Mon Sep 17 00:00:00 2001 From: Gyanu Date: Sun, 30 Aug 2026 11:40:36 +0530 Subject: [PATCH 1/3] Detect asyncio when a loop is running but there is no current Task. Twisted's asyncioreactor (and call_soon callbacks) run on the asyncio loop without an asyncio.Task, so current_task() is None and sniffio raised AsyncLibraryNotFoundError. --- sniffio/_impl.py | 9 +++++++++ sniffio/_tests/test_sniffio.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/sniffio/_impl.py b/sniffio/_impl.py index c1a7bbf..a1eb30f 100644 --- a/sniffio/_impl.py +++ b/sniffio/_impl.py @@ -83,6 +83,15 @@ async def generic_sleep(seconds): return "asyncio" except RuntimeError: pass + else: + # Running loop, but not inside a Task (Twisted asyncioreactor, + # call_soon callbacks, etc.) + try: + asyncio.get_running_loop() + except RuntimeError: + pass + else: + return "asyncio" # Sniff for curio (for now) if 'curio' in sys.modules: diff --git a/sniffio/_tests/test_sniffio.py b/sniffio/_tests/test_sniffio.py index 02945a9..b11da3c 100644 --- a/sniffio/_tests/test_sniffio.py +++ b/sniffio/_tests/test_sniffio.py @@ -58,6 +58,25 @@ async def this_is_asyncio(): current_async_library() +def test_asyncio_running_loop_without_task(): + import asyncio + + result = [] + + def sync_cb(): + result.append(current_async_library()) + loop.stop() + + loop = asyncio.new_event_loop() + try: + loop.call_soon(sync_cb) + loop.run_forever() + finally: + loop.close() + + assert result == ["asyncio"] + + @pytest.mark.skipif( sys.version_info >= (3, 12), reason= From 98a876b4f8a28a168a61172b5fd81c65e7b1f114 Mon Sep 17 00:00:00 2001 From: Gyanu Date: Tue, 1 Sep 2026 11:10:50 +0530 Subject: [PATCH 2/3] Stop the loop if the asyncio fallback test fails. --- newsfragments/51.bugfix.rst | 3 +++ sniffio/_tests/test_sniffio.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 newsfragments/51.bugfix.rst diff --git a/newsfragments/51.bugfix.rst b/newsfragments/51.bugfix.rst new file mode 100644 index 0000000..8bd9665 --- /dev/null +++ b/newsfragments/51.bugfix.rst @@ -0,0 +1,3 @@ +``current_async_library`` now reports ``asyncio`` when an event loop is +running but there is no current Task, such as in ``call_soon`` callbacks +and Twisted's asyncioreactor. diff --git a/sniffio/_tests/test_sniffio.py b/sniffio/_tests/test_sniffio.py index b11da3c..a99f5e8 100644 --- a/sniffio/_tests/test_sniffio.py +++ b/sniffio/_tests/test_sniffio.py @@ -64,8 +64,10 @@ def test_asyncio_running_loop_without_task(): result = [] def sync_cb(): - result.append(current_async_library()) - loop.stop() + try: + result.append(current_async_library()) + finally: + loop.stop() loop = asyncio.new_event_loop() try: From 84c51c9a073bae30faa1387741708d94a9637e5a Mon Sep 17 00:00:00 2001 From: Gyanu Mayank Date: Tue, 1 Sep 2026 22:24:55 +0530 Subject: [PATCH 3/3] Detect asyncio from get_running_loop only. current_task() already raises outside a loop, so probing it first is strictly slower and still needs the get_running_loop fallback for call_soon callbacks. --- newsfragments/51.bugfix.rst | 3 ++- sniffio/_impl.py | 16 ++-------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/newsfragments/51.bugfix.rst b/newsfragments/51.bugfix.rst index 8bd9665..8677649 100644 --- a/newsfragments/51.bugfix.rst +++ b/newsfragments/51.bugfix.rst @@ -1,3 +1,4 @@ ``current_async_library`` now reports ``asyncio`` when an event loop is running but there is no current Task, such as in ``call_soon`` callbacks -and Twisted's asyncioreactor. +and Twisted's asyncioreactor. Detection uses ``asyncio.get_running_loop()`` +only; ``current_task()`` is a slower subset of the same check. diff --git a/sniffio/_impl.py b/sniffio/_impl.py index a1eb30f..64d311e 100644 --- a/sniffio/_impl.py +++ b/sniffio/_impl.py @@ -75,23 +75,11 @@ async def generic_sleep(seconds): if "asyncio" in sys.modules: import asyncio try: - current_task = asyncio.current_task # type: ignore[attr-defined] - except AttributeError: - current_task = asyncio.Task.current_task # type: ignore[attr-defined] - try: - if current_task() is not None: - return "asyncio" + asyncio.get_running_loop() except RuntimeError: pass else: - # Running loop, but not inside a Task (Twisted asyncioreactor, - # call_soon callbacks, etc.) - try: - asyncio.get_running_loop() - except RuntimeError: - pass - else: - return "asyncio" + return "asyncio" # Sniff for curio (for now) if 'curio' in sys.modules: