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 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