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