summaryrefslogtreecommitdiffstats
path: root/Lib/asyncio
diff options
context:
space:
mode:
authorAndrew Svetlov <andrew.svetlov@gmail.com>2022-03-24 19:51:16 (GMT)
committerGitHub <noreply@github.com>2022-03-24 19:51:16 (GMT)
commit4119d2d7c9e25acd4f16994fb92d656f8b7816d7 (patch)
tree13f5086fc5ad0247381d347c4271a8ca79a20fd7 /Lib/asyncio
parent2f49b97cc5426087b46515254b9a97a22ee8c807 (diff)
downloadcpython-4119d2d7c9e25acd4f16994fb92d656f8b7816d7.zip
cpython-4119d2d7c9e25acd4f16994fb92d656f8b7816d7.tar.gz
cpython-4119d2d7c9e25acd4f16994fb92d656f8b7816d7.tar.bz2
bpo-47062: Implement asyncio.Runner context manager (GH-31799)
Co-authored-by: Zachary Ware <zach@python.org>
Diffstat (limited to 'Lib/asyncio')
-rw-r--r--Lib/asyncio/runners.py124
1 files changed, 106 insertions, 18 deletions
diff --git a/Lib/asyncio/runners.py b/Lib/asyncio/runners.py
index 9a5e9a4..975509c 100644
--- a/Lib/asyncio/runners.py
+++ b/Lib/asyncio/runners.py
@@ -1,10 +1,112 @@
-__all__ = 'run',
+__all__ = ('Runner', 'run')
+import contextvars
+import enum
from . import coroutines
from . import events
from . import tasks
+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
+
+
+
def run(main, *, debug=None):
"""Execute the coroutine and return the result.
@@ -30,26 +132,12 @@ def run(main, *, debug=None):
asyncio.run(main())
"""
if events._get_running_loop() is not None:
+ # fail fast with short traceback
raise RuntimeError(
"asyncio.run() cannot be called from a running event loop")
- if not coroutines.iscoroutine(main):
- raise ValueError("a coroutine was expected, got {!r}".format(main))
-
- loop = events.new_event_loop()
- try:
- events.set_event_loop(loop)
- if debug is not None:
- loop.set_debug(debug)
- return loop.run_until_complete(main)
- finally:
- try:
- _cancel_all_tasks(loop)
- loop.run_until_complete(loop.shutdown_asyncgens())
- loop.run_until_complete(loop.shutdown_default_executor())
- finally:
- events.set_event_loop(None)
- loop.close()
+ with Runner(debug=debug) as runner:
+ return runner.run(main)
def _cancel_all_tasks(loop):