1515
1616from asgiref .sync import (
1717 AsyncSingleThreadContext ,
18+ AsyncToSync ,
1819 ThreadSensitiveContext ,
1920 async_to_sync ,
2021 iscoroutinefunction ,
@@ -549,21 +550,24 @@ def inner(result):
549550def test_async_single_thread_context_matches ():
550551 """
551552 Tests that functions wrapped with async_to_sync and executed within an
552- AsyncSingleThreadContext run on the same thread, even without a main_event_loop.
553+ AsyncSingleThreadContext run on the same thread/loop, even without a
554+ main_event_loop.
553555 """
554556 result_1 = {}
555557 result_2 = {}
556558
557- async def store_thread_async (result ):
559+ async def store_thread_info_async (result ):
558560 result ["thread" ] = threading .current_thread ()
561+ result ["loop" ] = asyncio .get_running_loop ()
559562
560563 with AsyncSingleThreadContext ():
561- async_to_sync (store_thread_async )(result_1 )
562- async_to_sync (store_thread_async )(result_2 )
564+ async_to_sync (store_thread_info_async )(result_1 )
565+ async_to_sync (store_thread_info_async )(result_2 )
563566
564567 # They should not have run in the main thread, and on the same threads
565568 assert result_1 ["thread" ] != threading .current_thread ()
566569 assert result_1 ["thread" ] == result_2 ["thread" ]
570+ assert result_1 ["loop" ] == result_2 ["loop" ]
567571
568572
569573def test_async_single_thread_nested_context ():
@@ -572,20 +576,28 @@ def test_async_single_thread_nested_context():
572576 """
573577 result_1 = {}
574578 result_2 = {}
579+ result_3 = {}
575580
576581 @async_to_sync
577- async def store_thread (result ):
582+ async def store_thread_info (result ):
578583 result ["thread" ] = threading .current_thread ()
584+ result ["loop" ] = asyncio .get_running_loop ()
579585
580586 with AsyncSingleThreadContext ():
581- store_thread (result_1 )
587+ store_thread_info (result_1 )
582588
583589 with AsyncSingleThreadContext ():
584- store_thread (result_2 )
590+ store_thread_info (result_2 )
591+
592+ # verify that the context does not shut down the executor or event loop
593+ store_thread_info (result_3 )
585594
586595 # They should not have run in the main thread, and on the same threads
587596 assert result_1 ["thread" ] != threading .current_thread ()
588597 assert result_1 ["thread" ] == result_2 ["thread" ]
598+ assert result_1 ["thread" ] == result_3 ["thread" ]
599+ assert result_1 ["loop" ] == result_2 ["loop" ]
600+ assert result_1 ["loop" ] == result_3 ["loop" ]
589601
590602
591603def test_async_single_thread_context_without_async_work ():
@@ -621,21 +633,54 @@ async def test_async_single_thread_context_matches_from_async_thread():
621633 """
622634 result_1 = {}
623635 result_2 = {}
636+ result_3 = {}
624637
625638 @async_to_sync
626- async def store_thread_async (result ):
639+ async def store_thread_info_async (result ):
627640 result ["thread" ] = threading .current_thread ()
641+ result ["loop" ] = asyncio .get_running_loop ()
628642
629643 def inner ():
630644 with AsyncSingleThreadContext ():
631- store_thread_async (result_1 )
632- store_thread_async (result_2 )
645+ store_thread_info_async (result_1 )
646+ store_thread_info_async (result_2 )
647+
648+ # verify that the context does not shut down the executor or event loop
649+ store_thread_info_async (result_3 )
633650
634651 await sync_to_async (inner )()
635652
636653 # They should both have run in the current thread.
637654 assert result_1 ["thread" ] == threading .current_thread ()
638655 assert result_1 ["thread" ] == result_2 ["thread" ]
656+ assert result_1 ["thread" ] == result_3 ["thread" ]
657+ assert result_1 ["loop" ] == result_2 ["loop" ]
658+ assert result_1 ["loop" ] == result_3 ["loop" ]
659+
660+
661+ def test_async_single_thread_context_shutdown ():
662+ """
663+ Verifies the shutdown process works as expected.
664+ """
665+ result = {}
666+
667+ async def inner_long_running_task ():
668+ await asyncio .sleep (5 )
669+ result ["inner_long_running_task" ] = True
670+
671+ async def handler ():
672+ # start but do not await, so it runs in the background
673+ asyncio .create_task (inner_long_running_task ())
674+ return True
675+
676+ with AsyncSingleThreadContext () as ctx :
677+ assert async_to_sync (handler )()
678+
679+ executor , loop = AsyncToSync .async_single_thread_context_map [ctx ]
680+
681+ assert loop .is_closed (), "Event loop was not closed"
682+ assert executor ._shutdown , "Executor was not shutdown"
683+ assert not result , "Background tasks should have been canceled"
639684
640685
641686@pytest .mark .asyncio
0 commit comments