From 25fc4d89f7ced4b84836ec342c6f440ff6839330 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Fri, 14 Aug 2026 14:48:03 +0200 Subject: [PATCH] pkg.lib.App: Fix event loop for Python 3.14+ asyncio.get_event_loop() is removed in Python 3.14 when called from outside an async context. The current code calls it in __init__(), which crashes on 3.14+. To fix this, drop the eager loop creation from __init__(). Instead, lazily create a loop in run() via asyncio.new_event_loop() when no external loop was provided, and close it in the finally block. This makes the lifecycle symmetric: run() owns the full create-use-close cycle and supports re-entrant calls. Replace __del__() with an explicit close() method, guarding against double-close via is_closed(). close() always clears __eloop to None so a closed loop never lingers. Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/App.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/python/jw/pkg/lib/App.py b/src/python/jw/pkg/lib/App.py index e0891f01..de0289a6 100644 --- a/src/python/jw/pkg/lib/App.py +++ b/src/python/jw/pkg/lib/App.py @@ -84,7 +84,7 @@ class App: # export description: str = '', name_filter: str = '^Cmd.*', modules: list[str] | None = None, - eloop: None = None, + eloop: asyncio.AbstractEventLoop | None = None, ) -> None: def add_cmd_to_parser(cmd: AbstractCmd, parsers: Any) -> ArgumentParser: @@ -171,12 +171,9 @@ class App: # export set_log_flags(self.__default_log_flags) set_log_level(self.__default_log_level) + self.__async_runner: AsyncRunner | None = None self.__eloop = eloop self.__own_eloop = False - if eloop is None: - self.__eloop = asyncio.get_event_loop() - self.__own_eloop = True - self.__async_runner: AsyncRunner | None = None self.__parser = ArgumentParser( formatter_class = ArgumentDefaultsHelpFormatter, @@ -212,12 +209,12 @@ class App: # export '-h', '--help', action = 'help', help = 'Show this help message and exit' ) - def __del__(self) -> None: + def close(self) -> None: if self.__own_eloop: if self.__eloop is not None: - self.__eloop.close() + if not self.__eloop.is_closed(): + self.__eloop.close() self.__eloop = None - self.__own_eloop = False async def __aenter__(self) -> None: pass @@ -333,12 +330,18 @@ class App: # export return self.__parser def run(self, argv: list[str] | None = None) -> None: + if self.__eloop is None: + eloop = asyncio.new_event_loop() + asyncio.set_event_loop(eloop) + self.__eloop = eloop + self.__own_eloop = True try: ret = self.eloop.run_until_complete(self.__run(argv)) finally: if self.__async_runner: self.__async_runner.close() self.__async_runner = None + self.close() return ret -- 2.55.0