From 845601ff10937c79096623f4db160c828f132ce5 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Fri, 14 Aug 2026 23:59:10 +0200 Subject: [PATCH] lib.App: Restore previous event loop in run() When run() creates an event loop, it installs it with set_event_loop() but never restores the thread's previous loop, so after run() returns, the thread is left with the now-closed loop created by run(). Any code that calls get_event_loop() afterwards gets a closed loop, and on Python 3.13+ a thread that had no loop at all starts emitting or raising deprecation errors that run() caused. Capture the thread's current loop with _get_current_event_loop() before installing a new one, and restore it in the finally block. If there was no previous loop, unset the loop with set_event_loop(None) so that the thread is left without a loop instead of with the closed one. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/App.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/python/jw/pkg/lib/App.py b/src/python/jw/pkg/lib/App.py index 6eb45581..263938c2 100644 --- a/src/python/jw/pkg/lib/App.py +++ b/src/python/jw/pkg/lib/App.py @@ -4,6 +4,7 @@ import asyncio import cProfile import os import sys +import warnings from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace from typing import TYPE_CHECKING, Any, cast, override @@ -31,6 +32,17 @@ if TYPE_CHECKING: from typing import TypeVar T = TypeVar('T') +def _get_current_event_loop() -> asyncio.AbstractEventLoop | None: + """Return the current event loop of this thread, or None if there is + none, without creating one implicitly or emitting a deprecation + warning.""" + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + try: + return asyncio.get_event_loop() + except (RuntimeError, DeprecationWarning): + return None + class App: # export def _add_arguments(self, parser: ArgumentParser) -> None: @@ -341,7 +353,9 @@ class App: # export return self.__parser def run(self, argv: list[str] | None = None) -> None: + previous_eloop: asyncio.AbstractEventLoop | None = None if self.__eloop is None: + previous_eloop = _get_current_event_loop() eloop = asyncio.new_event_loop() asyncio.set_event_loop(eloop) self.__eloop = eloop @@ -350,6 +364,12 @@ class App: # export ret = self.eloop.run_until_complete(self.__run(argv)) finally: self.close() + # -- Restore the event loop the thread had before run(), or + # unset the loop if there was none. + if previous_eloop is not None: + asyncio.set_event_loop(previous_eloop) + else: + asyncio.set_event_loop(None) return ret