@@ -377,11 +377,33 @@ async def open_loop(queue_len=None):
377
377
"""Returns a Trio-flavored async context manager which provides
378
378
an asyncio event loop running on top of Trio.
379
379
380
+ The context manager evaluates to a new `TrioEventLoop` object.
381
+
380
382
Entering the context manager is not enough on its own to immediately
381
383
run asyncio code; it just provides the context that makes running that
382
384
code possible. You additionally need to wrap any asyncio functions
383
385
that you want to run in :func:`aio_as_trio`.
384
386
387
+ Exiting the context manager will attempt to do an orderly shutdown
388
+ of the tasks it contains, analogously to :func:`asyncio.run`.
389
+ asyncio-flavored tasks are cancelled and awaited first, then
390
+ Trio-flavored tasks that were started using
391
+ :meth:`~BaseTrioEventLoop.trio_as_future` or
392
+ :meth:`~BaseTrioEventLoop.run_trio_task`. All
393
+ :meth:`~asyncio.loop.call_soon` callbacks that are submitted
394
+ before exiting the context manager will run before starting
395
+ this shutdown sequence, and all callbacks that are submitted
396
+ before the last task exits will run before the loop closes.
397
+ The exact point at which the loop stops running callbacks is
398
+ not specified.
399
+
400
+ .. warning:: As with :func:`asyncio.run`, asyncio-flavored tasks
401
+ that are started *after* exiting the context manager (such as by
402
+ another task as it unwinds) may or may not be cancelled, and will
403
+ be abandoned if they survive the shutdown sequence. This may lead
404
+ to unclosed resources, stderr spew about "coroutine ignored
405
+ GeneratorExit", etc. Trio-flavored tasks do not have this hazard.
406
+
385
407
Example usage::
386
408
387
409
async def async_main(*args):
@@ -408,12 +430,11 @@ async def async_main(*args):
408
430
await loop ._main_loop_init (tasks_nursery )
409
431
await loop_nursery .start (loop ._main_loop )
410
432
yield loop
411
- tasks_nursery .cancel_scope .cancel ()
412
433
413
- # Allow all submitted run_trio() tasks calls a chance
414
- # to start before the tasks_nursery closes , unless the
415
- # loop stops (due to someone else calling stop())
416
- # before that:
434
+ # Allow all already- submitted tasks a chance to start
435
+ # (and then immediately be cancelled) , unless the loop
436
+ # stops (due to someone else calling stop()) before
437
+ # that.
417
438
async with trio .open_nursery () as sync_nursery :
418
439
sync_nursery .cancel_scope .shield = True
419
440
@@ -425,6 +446,29 @@ async def wait_for_sync():
425
446
426
447
await loop .wait_stopped ()
427
448
sync_nursery .cancel_scope .cancel ()
449
+
450
+ # Cancel and wait on all currently-running asyncio tasks.
451
+ # Like asyncio.run(), we don't bother cancelling and waiting
452
+ # on any additional tasks that these tasks start as they
453
+ # unwind.
454
+ if sys .version_info >= (3 , 7 ):
455
+ aio_tasks = asyncio .all_tasks (loop )
456
+ else :
457
+ aio_tasks = {t for t in asyncio .Task .all_tasks (loop ) if not t .done ()}
458
+ if aio_tasks :
459
+ # Start one Trio task to wait for each still-running
460
+ # asyncio task. This provides better exception
461
+ # propagation than using asyncio.gather().
462
+ async with trio .open_nursery () as aio_cancel_nursery :
463
+ for task in aio_tasks :
464
+ aio_cancel_nursery .start_soon (run_aio_future , task )
465
+ aio_cancel_nursery .cancel_scope .cancel ()
466
+
467
+ # If there are any trio_as_aio tasks still going after
468
+ # the cancellation of asyncio tasks above, this will
469
+ # cancel them, and exiting the tasks_nursery block
470
+ # will wait for them to exit.
471
+ tasks_nursery .cancel_scope .cancel ()
428
472
finally :
429
473
try :
430
474
await loop ._main_loop_exit ()
0 commit comments