Description
According to the docs,
"When an exception is not handled at all, the interpreter terminates execution of the program, or returns to its
interactive main loop. In either case, it prints a stack traceback, except when the exception is SystemExit."
I'm guessing that 'interactive main loop' was intended to cover the peculiar way tkinter works. If that is so, any peculiar behaviour of asyncio's main loop is not explicitly defined.
❌ 1 - Threads + Asyncio
In the minimal and reproducible code example below the unhandled exception does not produce a stack trace unless it is explicitly intercepted with a bare except in the try-except handler. It silently returns to the asyncio loop without any indication that an unhandled error has occurred. (This thread + asyncio design is a pattern for interworking tkinter with asyncio and is recommended in Modern Tkinter by Mark Roseman).
The exception is raised in a coroutine function initiated from a different thread using asyncio. run_coroutine_threadsafe(func(*args), loop).
If the exception is intercepted with a bare except statement the stack trace begins at aio_blocker_controller and not at the asyncio main loop.
✅ 2 & 3 - Pure asyncio & pure threading.
This problem is not seen in a pure threaded or a pure asyncio example.
For asyncio code the equivalent call of await func(*args) produces a correct stack trace beginning at asyncio.run(main()).
For threaded code the equivalent call of threading.Thread(target=func, *args)) also produces a correct stack
trace beginning at Thread.run.
Code Example 1 - Threads + Asyncio
import asyncio
import sys
import threading
import time
import traceback
from typing import Callable, Optional
aio_loop: Optional[asyncio.AbstractEventLoop] = None
async def aio_blocker(block: float = 0.0):
await asyncio.sleep(block)
raise ValueError
async def aio_blocker_controller(func: Callable):
try:
await func()
# # Handle unhandled exceptions!?
# except:
# traceback.print_exception(*(sys.exc_info()))
finally:
print('aio_blocker_controller completed normally.')
async def manage_aio_loop(aio_initiate_shutdown: threading.Event):
global aio_loop
aio_loop = asyncio.get_running_loop()
while not aio_initiate_shutdown.is_set():
await asyncio.sleep(0)
def aio_main(aio_initiate_shutdown: threading.Event):
asyncio.run(manage_aio_loop(aio_initiate_shutdown))
def main():
aio_initiate_shutdown = threading.Event()
aio_thread = threading.Thread(target=aio_main, args=(aio_initiate_shutdown,))
aio_thread.start()
while not aio_loop:
time.sleep(0)
asyncio.run_coroutine_threadsafe(aio_blocker_controller(aio_blocker), aio_loop)
time.sleep(0.2)
aio_initiate_shutdown.set()
aio_thread.join()
if __name__ == '__main__':
sys.exit(main())
Code Example 2 - Pure Asyncio
import asyncio
from typing import Callable
async def aio_blocker(block: float = 0.0):
await asyncio.sleep(block)
raise ValueError
async def aio_blocker_controller(func: Callable):
try:
await func()
finally:
print('aio_blocker_controller completed normally.')
async def main():
await aio_blocker_controller(aio_blocker)
if __name__ == '__main__':
asyncio.run(main())
Code Example 3 - Pure Threading
import sys
import threading
import time
from typing import Callable
def blocker(block: float = 0.0):
time.sleep(block)
raise ValueError
def blocker_controller(func: Callable):
try:
func()
finally:
print('blocker_controller completed normally.')
def main():
thread = threading.Thread(target=blocker_controller, args=(blocker, ))
thread.start()
if __name__ == '__main__':
sys.exit(main())
Your environment
Python v3.10.7 on MacOS Monterey 12.6
Metadata
Metadata
Assignees
Projects
Status