pkg.lib.App: Fix event loop for Python 3.14+
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 4m36s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 4m24s
CI / Packaging test (pull_request) Successful in 0s
CI / Packaging - Kali Linux (push) Successful in 4m17s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 4m18s
CI / Packaging test (push) Successful in 0s

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 <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-08-14 14:48:03 +02:00
commit 25fc4d89f7
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61

View file

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