runner.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. """Backported implementation of asyncio.Runner from 3.11 compatible down to Python 3.8."""
  2. import asyncio
  3. import asyncio.tasks
  4. import contextvars
  5. import enum
  6. import functools
  7. import signal
  8. import sys
  9. import threading
  10. from asyncio import coroutines, events, tasks, exceptions, AbstractEventLoop
  11. from contextvars import Context
  12. from types import TracebackType, FrameType
  13. from typing import (
  14. Callable,
  15. Coroutine,
  16. TypeVar,
  17. Any,
  18. Optional,
  19. Type,
  20. final,
  21. TYPE_CHECKING,
  22. )
  23. from ._int_to_enum import _orig_int_to_enum, _patched_int_to_enum
  24. from ._patch import _patch_object
  25. from .tasks import Task
  26. # See https://github.com/python/cpython/blob/3.11/Lib/asyncio/runners.py
  27. if TYPE_CHECKING: # pragma: no cover
  28. from signal import _HANDLER
  29. __all__ = ("Runner",)
  30. _T = TypeVar("_T")
  31. class _State(enum.Enum):
  32. CREATED = "created"
  33. INITIALIZED = "initialized"
  34. CLOSED = "closed"
  35. @final
  36. class Runner:
  37. """A context manager that controls event loop life cycle.
  38. The context manager always creates a new event loop,
  39. allows to run async functions inside it,
  40. and properly finalizes the loop at the context manager exit.
  41. If debug is True, the event loop will be run in debug mode.
  42. If loop_factory is passed, it is used for new event loop creation.
  43. asyncio.run(main(), debug=True)
  44. is a shortcut for
  45. with Runner(debug=True) as runner:
  46. runner.run(main())
  47. The run() method can be called multiple times within the runner's context.
  48. This can be useful for interactive console (e.g. IPython),
  49. unittest runners, console tools, -- everywhere when async code
  50. is called from existing sync framework and where the preferred single
  51. asyncio.run() call doesn't work.
  52. """
  53. # Note: the class is final, it is not intended for inheritance.
  54. def __init__(
  55. self,
  56. *,
  57. debug: Optional[bool] = None,
  58. loop_factory: Optional[Callable[[], AbstractEventLoop]] = None,
  59. ):
  60. self._state = _State.CREATED
  61. self._debug = debug
  62. self._loop_factory = loop_factory
  63. self._loop: Optional[AbstractEventLoop] = None
  64. self._context = None
  65. self._interrupt_count = 0
  66. self._set_event_loop = False
  67. def __enter__(self) -> "Runner":
  68. self._lazy_init()
  69. return self
  70. def __exit__(
  71. self,
  72. exc_type: Type[BaseException],
  73. exc_val: BaseException,
  74. exc_tb: TracebackType,
  75. ) -> None:
  76. self.close()
  77. def close(self) -> None:
  78. """Shutdown and close event loop."""
  79. if self._state is not _State.INITIALIZED:
  80. return
  81. try:
  82. # TYPING: self._loop should not be None
  83. loop: AbstractEventLoop = self._loop # type: ignore[assignment]
  84. _cancel_all_tasks(loop)
  85. loop.run_until_complete(loop.shutdown_asyncgens())
  86. # Backport Patch
  87. if sys.version_info >= (3, 9):
  88. loop.run_until_complete(loop.shutdown_default_executor())
  89. else:
  90. loop.run_until_complete(_shutdown_default_executor(loop))
  91. finally:
  92. if self._set_event_loop:
  93. events.set_event_loop(None)
  94. loop.close()
  95. self._loop = None
  96. self._state = _State.CLOSED
  97. # See https://github.com/python/cpython/pull/113444
  98. # Reverts signal._int_to_enum patch
  99. signal._int_to_enum = _orig_int_to_enum # type: ignore[attr-defined]
  100. def get_loop(self) -> AbstractEventLoop:
  101. """Return embedded event loop."""
  102. self._lazy_init()
  103. # TYPING: self._loop should not be None
  104. return self._loop # type: ignore[return-value]
  105. def run(
  106. self, coro: Coroutine[Any, Any, _T], *, context: Optional[Context] = None
  107. ) -> _T:
  108. """Run a coroutine inside the embedded event loop."""
  109. if not coroutines.iscoroutine(coro):
  110. raise ValueError("a coroutine was expected, got {!r}".format(coro))
  111. if events._get_running_loop() is not None:
  112. # fail fast with short traceback
  113. raise RuntimeError(
  114. "Runner.run() cannot be called from a running event loop"
  115. )
  116. self._lazy_init()
  117. if context is None:
  118. context = self._context
  119. # Backport Patch: <= 3.11 does not have create_task with context parameter
  120. # Reader note: context.run will not work here as it does not match how asyncio.Runner retains context
  121. with _patch_object(asyncio.tasks, asyncio.tasks.Task.__name__, Task):
  122. with _patch_object(
  123. contextvars, contextvars.copy_context.__name__, lambda: context
  124. ):
  125. task = self._loop.create_task(coro) # type: ignore[union-attr]
  126. if (
  127. threading.current_thread() is threading.main_thread()
  128. and signal.getsignal(signal.SIGINT) is signal.default_int_handler
  129. ):
  130. sigint_handler: "_HANDLER" = functools.partial(
  131. self._on_sigint, main_task=task
  132. )
  133. try:
  134. signal.signal(signal.SIGINT, sigint_handler)
  135. except ValueError:
  136. # `signal.signal` may throw if `threading.main_thread` does
  137. # not support signals (e.g. embedded interpreter with signals
  138. # not registered - see gh-91880)
  139. sigint_handler = None
  140. else:
  141. sigint_handler = None
  142. self._interrupt_count = 0
  143. try:
  144. return self._loop.run_until_complete(task) # type: ignore[union-attr, no-any-return]
  145. except exceptions.CancelledError:
  146. if self._interrupt_count > 0:
  147. uncancel = getattr(task, "uncancel", None)
  148. if uncancel is not None and uncancel() == 0:
  149. raise KeyboardInterrupt()
  150. raise # CancelledError
  151. finally:
  152. if (
  153. sigint_handler is not None
  154. and signal.getsignal(signal.SIGINT) is sigint_handler
  155. ):
  156. signal.signal(signal.SIGINT, signal.default_int_handler)
  157. def _lazy_init(self) -> None:
  158. if self._state is _State.CLOSED:
  159. raise RuntimeError("Runner is closed")
  160. if self._state is _State.INITIALIZED:
  161. return
  162. # See https://github.com/python/cpython/pull/113444
  163. # Patches signal._int_to_enum temporarily
  164. signal._int_to_enum = _patched_int_to_enum # type: ignore[attr-defined]
  165. # Continue original implementation
  166. if self._loop_factory is None:
  167. self._loop = events.new_event_loop()
  168. if not self._set_event_loop:
  169. # Call set_event_loop only once to avoid calling
  170. # attach_loop multiple times on child watchers
  171. events.set_event_loop(self._loop)
  172. self._set_event_loop = True
  173. else:
  174. self._loop = self._loop_factory()
  175. if self._debug is not None:
  176. self._loop.set_debug(self._debug)
  177. self._context = contextvars.copy_context() # type: ignore[assignment]
  178. self._state = _State.INITIALIZED
  179. def _on_sigint(
  180. self, signum: int, frame: Optional[FrameType], main_task: asyncio.Task
  181. ) -> None:
  182. # TYPING: self._loop should not be None
  183. self._interrupt_count += 1
  184. if self._interrupt_count == 1 and not main_task.done():
  185. main_task.cancel()
  186. # wakeup loop if it is blocked by select() with long timeout
  187. self._loop.call_soon_threadsafe(lambda: None) # type: ignore[union-attr]
  188. return
  189. raise KeyboardInterrupt()
  190. # See https://github.com/python/cpython/blob/3.11/Lib/asyncio/runners.py
  191. def _cancel_all_tasks(loop: AbstractEventLoop) -> None:
  192. to_cancel = tasks.all_tasks(loop)
  193. if not to_cancel:
  194. return
  195. for task in to_cancel:
  196. task.cancel()
  197. loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True))
  198. for task in to_cancel:
  199. if task.cancelled():
  200. continue
  201. if task.exception() is not None:
  202. loop.call_exception_handler(
  203. {
  204. "message": "unhandled exception during asyncio.run() shutdown",
  205. "exception": task.exception(),
  206. "task": task,
  207. }
  208. )
  209. # See https://github.com/python/cpython/blob/3.11/Lib/asyncio/base_events.py
  210. async def _shutdown_default_executor(loop: AbstractEventLoop) -> None:
  211. """Schedule the shutdown of the default executor."""
  212. # TYPING: Both _executor_shutdown_called and _default_executor are expected private properties of BaseEventLoop
  213. loop._executor_shutdown_called = True # type: ignore[attr-defined]
  214. if loop._default_executor is None: # type: ignore[attr-defined]
  215. return
  216. future = loop.create_future()
  217. # Backport modified to include send loop to _do_shutdown
  218. thread = threading.Thread(target=_do_shutdown, args=(loop, future))
  219. thread.start()
  220. try:
  221. await future
  222. finally:
  223. thread.join()
  224. def _do_shutdown(loop: AbstractEventLoop, future: asyncio.futures.Future) -> None:
  225. try:
  226. loop._default_executor.shutdown(wait=True) # type: ignore[attr-defined]
  227. if not loop.is_closed():
  228. loop.call_soon_threadsafe(future.set_result, None)
  229. except Exception as ex:
  230. if not loop.is_closed():
  231. loop.call_soon_threadsafe(future.set_exception, ex)