2022-03-25 03:51:16 +08:00
|
|
|
__all__ = ('Runner', 'run')
|
2017-12-14 22:42:21 +08:00
|
|
|
|
2022-03-25 03:51:16 +08:00
|
|
|
import contextvars
|
|
|
|
import enum
|
2017-12-14 22:42:21 +08:00
|
|
|
from . import coroutines
|
|
|
|
from . import events
|
2018-01-22 03:56:59 +08:00
|
|
|
from . import tasks
|
2017-12-14 22:42:21 +08:00
|
|
|
|
|
|
|
|
2022-03-25 03:51:16 +08:00
|
|
|
class _State(enum.Enum):
|
|
|
|
CREATED = "created"
|
|
|
|
INITIALIZED = "initialized"
|
|
|
|
CLOSED = "closed"
|
|
|
|
|
|
|
|
|
|
|
|
class Runner:
|
|
|
|
"""A context manager that controls event loop life cycle.
|
|
|
|
|
|
|
|
The context manager always creates a new event loop,
|
|
|
|
allows to run async functions inside it,
|
|
|
|
and properly finalizes the loop at the context manager exit.
|
|
|
|
|
|
|
|
If debug is True, the event loop will be run in debug mode.
|
|
|
|
If factory is passed, it is used for new event loop creation.
|
|
|
|
|
|
|
|
asyncio.run(main(), debug=True)
|
|
|
|
|
|
|
|
is a shortcut for
|
|
|
|
|
|
|
|
with asyncio.Runner(debug=True) as runner:
|
|
|
|
runner.run(main())
|
|
|
|
|
|
|
|
The run() method can be called multiple times within the runner's context.
|
|
|
|
|
|
|
|
This can be useful for interactive console (e.g. IPython),
|
|
|
|
unittest runners, console tools, -- everywhere when async code
|
|
|
|
is called from existing sync framework and where the preferred single
|
|
|
|
asyncio.run() call doesn't work.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Note: the class is final, it is not intended for inheritance.
|
|
|
|
|
|
|
|
def __init__(self, *, debug=None, factory=None):
|
|
|
|
self._state = _State.CREATED
|
|
|
|
self._debug = debug
|
|
|
|
self._factory = factory
|
|
|
|
self._loop = None
|
|
|
|
self._context = None
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
self._lazy_init()
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
self.close()
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
"""Shutdown and close event loop."""
|
|
|
|
if self._state is not _State.INITIALIZED:
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
loop = self._loop
|
|
|
|
_cancel_all_tasks(loop)
|
|
|
|
loop.run_until_complete(loop.shutdown_asyncgens())
|
|
|
|
loop.run_until_complete(loop.shutdown_default_executor())
|
|
|
|
finally:
|
|
|
|
loop.close()
|
|
|
|
self._loop = None
|
|
|
|
self._state = _State.CLOSED
|
|
|
|
|
|
|
|
def get_loop(self):
|
|
|
|
"""Return embedded event loop."""
|
|
|
|
self._lazy_init()
|
|
|
|
return self._loop
|
|
|
|
|
|
|
|
def run(self, coro, *, context=None):
|
|
|
|
"""Run a coroutine inside the embedded event loop."""
|
|
|
|
if not coroutines.iscoroutine(coro):
|
|
|
|
raise ValueError("a coroutine was expected, got {!r}".format(coro))
|
|
|
|
|
|
|
|
if events._get_running_loop() is not None:
|
|
|
|
# fail fast with short traceback
|
|
|
|
raise RuntimeError(
|
|
|
|
"Runner.run() cannot be called from a running event loop")
|
|
|
|
|
|
|
|
self._lazy_init()
|
|
|
|
|
|
|
|
if context is None:
|
|
|
|
context = self._context
|
|
|
|
task = self._loop.create_task(coro, context=context)
|
|
|
|
return self._loop.run_until_complete(task)
|
|
|
|
|
|
|
|
def _lazy_init(self):
|
|
|
|
if self._state is _State.CLOSED:
|
|
|
|
raise RuntimeError("Runner is closed")
|
|
|
|
if self._state is _State.INITIALIZED:
|
|
|
|
return
|
|
|
|
if self._factory is None:
|
|
|
|
self._loop = events.new_event_loop()
|
|
|
|
else:
|
|
|
|
self._loop = self._factory()
|
|
|
|
if self._debug is not None:
|
|
|
|
self._loop.set_debug(self._debug)
|
|
|
|
self._context = contextvars.copy_context()
|
|
|
|
self._state = _State.INITIALIZED
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-09-03 12:54:46 +08:00
|
|
|
def run(main, *, debug=None):
|
2019-10-01 08:12:21 +08:00
|
|
|
"""Execute the coroutine and return the result.
|
2017-12-14 22:42:21 +08:00
|
|
|
|
|
|
|
This function runs the passed coroutine, taking care of
|
|
|
|
managing the asyncio event loop and finalizing asynchronous
|
|
|
|
generators.
|
|
|
|
|
|
|
|
This function cannot be called when another asyncio event loop is
|
|
|
|
running in the same thread.
|
|
|
|
|
|
|
|
If debug is True, the event loop will be run in debug mode.
|
|
|
|
|
|
|
|
This function always creates a new event loop and closes it at the end.
|
|
|
|
It should be used as a main entry point for asyncio programs, and should
|
|
|
|
ideally only be called once.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
print('hello')
|
|
|
|
|
|
|
|
asyncio.run(main())
|
|
|
|
"""
|
|
|
|
if events._get_running_loop() is not None:
|
2022-03-25 03:51:16 +08:00
|
|
|
# fail fast with short traceback
|
2017-12-14 22:42:21 +08:00
|
|
|
raise RuntimeError(
|
|
|
|
"asyncio.run() cannot be called from a running event loop")
|
|
|
|
|
2022-03-25 03:51:16 +08:00
|
|
|
with Runner(debug=debug) as runner:
|
|
|
|
return runner.run(main)
|
2018-01-22 03:56:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
def _cancel_all_tasks(loop):
|
2018-05-29 05:54:02 +08:00
|
|
|
to_cancel = tasks.all_tasks(loop)
|
2018-01-22 03:56:59 +08:00
|
|
|
if not to_cancel:
|
|
|
|
return
|
|
|
|
|
|
|
|
for task in to_cancel:
|
|
|
|
task.cancel()
|
|
|
|
|
2020-11-28 16:21:17 +08:00
|
|
|
loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True))
|
2018-01-22 03:56:59 +08:00
|
|
|
|
|
|
|
for task in to_cancel:
|
|
|
|
if task.cancelled():
|
|
|
|
continue
|
|
|
|
if task.exception() is not None:
|
|
|
|
loop.call_exception_handler({
|
|
|
|
'message': 'unhandled exception during asyncio.run() shutdown',
|
|
|
|
'exception': task.exception(),
|
|
|
|
'task': task,
|
|
|
|
})
|