Skip to content

Commit fd22cad

Browse files
committed
Allow nested use of asyncio.run
1 parent dff8e5d commit fd22cad

File tree

4 files changed

+43
-24
lines changed

4 files changed

+43
-24
lines changed

Doc/library/asyncio-runner.rst

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,20 @@ to simplify async code usage for common wide-spread scenarios.
2222
Running an asyncio Program
2323
==========================
2424

25-
.. function:: run(coro, *, debug=None, loop_factory=None)
25+
.. function:: run(coro, *, debug=None, loop_factory=None, running_ok=False)
2626

2727
Execute the :term:`coroutine` *coro* and return the result.
2828

29-
This function runs the passed coroutine, taking care of
29+
If *running_ok* is ``False``, this function runs the passed coroutine, taking care of
3030
managing the asyncio event loop, *finalizing asynchronous
31-
generators*, and closing the executor.
32-
33-
This function cannot be called when another asyncio event loop is
34-
running in the same thread.
31+
generators*, and closing the executor. This function cannot be called when another
32+
asyncio event loop is running in the same thread.
33+
34+
If *running_ok* is ``True``, this function allows running the passed coroutine even if
35+
this code is already running in an event loop. In other words, it allows re-entering
36+
the event loop, while an exception would be raised if *running_ok* were ``False``. If
37+
this function is called inside an already running event loop, the same loop is used,
38+
and it is not closed at the end.
3539

3640
If *debug* is ``True``, the event loop will be run in debug mode. ``False`` disables
3741
debug mode explicitly. ``None`` is used to respect the global

Lib/asyncio/base_events.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -594,21 +594,23 @@ def _do_shutdown(self, future):
594594
if not self.is_closed():
595595
self.call_soon_threadsafe(future.set_exception, ex)
596596

597-
def _check_running(self):
597+
def _check_running(self, running_ok=False):
598598
if self.is_running():
599599
raise RuntimeError('This event loop is already running')
600-
if events._get_running_loop() is not None:
600+
if not running_ok and events._get_running_loop() is not None:
601601
raise RuntimeError(
602602
'Cannot run the event loop while another loop is running')
603603

604-
def run_forever(self):
604+
def run_forever(self, running_ok=False):
605605
"""Run until stop() is called."""
606606
self._check_closed()
607-
self._check_running()
607+
self._check_running(running_ok=running_ok)
608608
self._set_coroutine_origin_tracking(self._debug)
609609

610610
old_agen_hooks = sys.get_asyncgen_hooks()
611611
try:
612+
old_thread_id = self._thread_id
613+
old_running_loop = events._get_running_loop()
612614
self._thread_id = threading.get_ident()
613615
sys.set_asyncgen_hooks(firstiter=self._asyncgen_firstiter_hook,
614616
finalizer=self._asyncgen_finalizer_hook)
@@ -620,12 +622,12 @@ def run_forever(self):
620622
break
621623
finally:
622624
self._stopping = False
623-
self._thread_id = None
624-
events._set_running_loop(None)
625+
self._thread_id = old_thread_id
626+
events._set_running_loop(old_running_loop)
625627
self._set_coroutine_origin_tracking(False)
626628
sys.set_asyncgen_hooks(*old_agen_hooks)
627629

628-
def run_until_complete(self, future):
630+
def run_until_complete(self, future, running_ok=False):
629631
"""Run until the Future is done.
630632
631633
If the argument is a coroutine, it is wrapped in a Task.
@@ -637,7 +639,7 @@ def run_until_complete(self, future):
637639
Return the Future's result, or raise its exception.
638640
"""
639641
self._check_closed()
640-
self._check_running()
642+
self._check_running(running_ok=running_ok)
641643

642644
new_task = not futures.isfuture(future)
643645
future = tasks.ensure_future(future, loop=self)
@@ -648,7 +650,7 @@ def run_until_complete(self, future):
648650

649651
future.add_done_callback(_run_until_complete_cb)
650652
try:
651-
self.run_forever()
653+
self.run_forever(running_ok=running_ok)
652654
except:
653655
if new_task and future.done() and not future.cancelled():
654656
# The coroutine raised a BaseException. Consume the exception

Lib/asyncio/runners.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ class Runner:
4545

4646
# Note: the class is final, it is not intended for inheritance.
4747

48-
def __init__(self, *, debug=None, loop_factory=None):
48+
def __init__(self, *, debug=None, loop_factory=None, running_ok=False):
4949
self._state = _State.CREATED
5050
self._debug = debug
5151
self._loop_factory = loop_factory
52+
self._running_ok = running_ok
5253
self._loop = None
5354
self._context = None
5455
self._interrupt_count = 0
@@ -59,7 +60,15 @@ def __enter__(self):
5960
return self
6061

6162
def __exit__(self, exc_type, exc_val, exc_tb):
62-
self.close()
63+
close = True
64+
try:
65+
events.get_running_loop()
66+
if self._running_ok:
67+
close = False
68+
except:
69+
pass
70+
if close:
71+
self.close()
6372

6473
def close(self):
6574
"""Shutdown and close event loop."""
@@ -68,9 +77,11 @@ def close(self):
6877
try:
6978
loop = self._loop
7079
_cancel_all_tasks(loop)
71-
loop.run_until_complete(loop.shutdown_asyncgens())
80+
loop.run_until_complete(loop.shutdown_asyncgens(), running_ok=self._running_ok)
7281
loop.run_until_complete(
73-
loop.shutdown_default_executor(constants.THREAD_JOIN_TIMEOUT))
82+
loop.shutdown_default_executor(constants.THREAD_JOIN_TIMEOUT),
83+
running_ok=self._running_ok,
84+
)
7485
finally:
7586
if self._set_event_loop:
7687
events.set_event_loop(None)
@@ -88,7 +99,7 @@ def run(self, coro, *, context=None):
8899
if not coroutines.iscoroutine(coro):
89100
raise ValueError("a coroutine was expected, got {!r}".format(coro))
90101

91-
if events._get_running_loop() is not None:
102+
if not self._running_ok and events._get_running_loop() is not None:
92103
# fail fast with short traceback
93104
raise RuntimeError(
94105
"Runner.run() cannot be called from a running event loop")
@@ -115,7 +126,7 @@ def run(self, coro, *, context=None):
115126

116127
self._interrupt_count = 0
117128
try:
118-
return self._loop.run_until_complete(task)
129+
return self._loop.run_until_complete(task, running_ok=self._running_ok)
119130
except exceptions.CancelledError:
120131
if self._interrupt_count > 0:
121132
uncancel = getattr(task, "uncancel", None)
@@ -157,7 +168,7 @@ def _on_sigint(self, signum, frame, main_task):
157168
raise KeyboardInterrupt()
158169

159170

160-
def run(main, *, debug=None, loop_factory=None):
171+
def run(main, *, debug=None, loop_factory=None, running_ok=False):
161172
"""Execute the coroutine and return the result.
162173
163174
This function runs the passed coroutine, taking care of
@@ -185,12 +196,12 @@ async def main():
185196
186197
asyncio.run(main())
187198
"""
188-
if events._get_running_loop() is not None:
199+
if not running_ok and events._get_running_loop() is not None:
189200
# fail fast with short traceback
190201
raise RuntimeError(
191202
"asyncio.run() cannot be called from a running event loop")
192203

193-
with Runner(debug=debug, loop_factory=loop_factory) as runner:
204+
with Runner(debug=debug, loop_factory=loop_factory, running_ok=running_ok) as runner:
194205
return runner.run(main)
195206

196207

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow the event loop to be re-entrant, by making it possible to call
2+
``asyncio.run(coro, running_ok=True)`` inside an already running event loop.

0 commit comments

Comments
 (0)