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 <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-08-14 23:59:10 +02:00
commit 845601ff10
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61

View file

@ -4,6 +4,7 @@ import asyncio
import cProfile import cProfile
import os import os
import sys import sys
import warnings
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
from typing import TYPE_CHECKING, Any, cast, override from typing import TYPE_CHECKING, Any, cast, override
@ -31,6 +32,17 @@ if TYPE_CHECKING:
from typing import TypeVar from typing import TypeVar
T = TypeVar('T') 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 class App: # export
def _add_arguments(self, parser: ArgumentParser) -> None: def _add_arguments(self, parser: ArgumentParser) -> None:
@ -341,7 +353,9 @@ class App: # export
return self.__parser return self.__parser
def run(self, argv: list[str] | None = None) -> None: def run(self, argv: list[str] | None = None) -> None:
previous_eloop: asyncio.AbstractEventLoop | None = None
if self.__eloop is None: if self.__eloop is None:
previous_eloop = _get_current_event_loop()
eloop = asyncio.new_event_loop() eloop = asyncio.new_event_loop()
asyncio.set_event_loop(eloop) asyncio.set_event_loop(eloop)
self.__eloop = eloop self.__eloop = eloop
@ -350,6 +364,12 @@ class App: # export
ret = self.eloop.run_until_complete(self.__run(argv)) ret = self.eloop.run_until_complete(self.__run(argv))
finally: finally:
self.close() 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 return ret