runners.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. __all__ = ('Runner', 'run')
  2. import contextvars
  3. import enum
  4. import functools
  5. import threading
  6. import signal
  7. from . import coroutines
  8. from . import events
  9. from . import exceptions
  10. from . import tasks
  11. from . import constants
  12. class _State(enum.Enum):
  13. CREATED = "created"
  14. INITIALIZED = "initialized"
  15. CLOSED = "closed"
  16. class Runner:
  17. """A context manager that controls event loop life cycle.
  18. The context manager always creates a new event loop,
  19. allows to run async functions inside it,
  20. and properly finalizes the loop at the context manager exit.
  21. If debug is True, the event loop will be run in debug mode.
  22. If loop_factory is passed, it is used for new event loop creation.
  23. asyncio.run(main(), debug=True)
  24. is a shortcut for
  25. with asyncio.Runner(debug=True) as runner:
  26. runner.run(main())
  27. The run() method can be called multiple times within the runner's context.
  28. This can be useful for interactive console (e.g. IPython),
  29. unittest runners, console tools, -- everywhere when async code
  30. is called from existing sync framework and where the preferred single
  31. asyncio.run() call doesn't work.
  32. """
  33. # Note: the class is final, it is not intended for inheritance.
  34. def __init__(self, *, debug=None, loop_factory=None):
  35. self._state = _State.CREATED
  36. self._debug = debug
  37. self._loop_factory = loop_factory
  38. self._loop = None
  39. self._context = None
  40. self._interrupt_count = 0
  41. self._set_event_loop = False
  42. def __enter__(self):
  43. self._lazy_init()
  44. return self
  45. def __exit__(self, exc_type, exc_val, exc_tb):
  46. self.close()
  47. def close(self):
  48. """Shutdown and close event loop."""
  49. if self._state is not _State.INITIALIZED:
  50. return
  51. try:
  52. loop = self._loop
  53. _cancel_all_tasks(loop)
  54. loop.run_until_complete(loop.shutdown_asyncgens())
  55. loop.run_until_complete(
  56. loop.shutdown_default_executor(constants.THREAD_JOIN_TIMEOUT))
  57. finally:
  58. if self._set_event_loop:
  59. events.set_event_loop(None)
  60. loop.close()
  61. self._loop = None
  62. self._state = _State.CLOSED
  63. def get_loop(self):
  64. """Return embedded event loop."""
  65. self._lazy_init()
  66. return self._loop
  67. def run(self, coro, *, context=None):
  68. """Run a coroutine inside the embedded event loop."""
  69. if not coroutines.iscoroutine(coro):
  70. raise ValueError("a coroutine was expected, got {!r}".format(coro))
  71. if events._get_running_loop() is not None:
  72. # fail fast with short traceback
  73. raise RuntimeError(
  74. "Runner.run() cannot be called from a running event loop")
  75. self._lazy_init()
  76. if context is None:
  77. context = self._context
  78. task = self._loop.create_task(coro, context=context)
  79. if (threading.current_thread() is threading.main_thread()
  80. and signal.getsignal(signal.SIGINT) is signal.default_int_handler
  81. ):
  82. sigint_handler = functools.partial(self._on_sigint, main_task=task)
  83. try:
  84. signal.signal(signal.SIGINT, sigint_handler)
  85. except ValueError:
  86. # `signal.signal` may throw if `threading.main_thread` does
  87. # not support signals (e.g. embedded interpreter with signals
  88. # not registered - see gh-91880)
  89. sigint_handler = None
  90. else:
  91. sigint_handler = None
  92. self._interrupt_count = 0
  93. try:
  94. return self._loop.run_until_complete(task)
  95. except exceptions.CancelledError:
  96. if self._interrupt_count > 0:
  97. uncancel = getattr(task, "uncancel", None)
  98. if uncancel is not None and uncancel() == 0:
  99. raise KeyboardInterrupt()
  100. raise # CancelledError
  101. finally:
  102. if (sigint_handler is not None
  103. and signal.getsignal(signal.SIGINT) is sigint_handler
  104. ):
  105. signal.signal(signal.SIGINT, signal.default_int_handler)
  106. def _lazy_init(self):
  107. if self._state is _State.CLOSED:
  108. raise RuntimeError("Runner is closed")
  109. if self._state is _State.INITIALIZED:
  110. return
  111. if self._loop_factory is None:
  112. self._loop = events.new_event_loop()
  113. if not self._set_event_loop:
  114. # Call set_event_loop only once to avoid calling
  115. # attach_loop multiple times on child watchers
  116. events.set_event_loop(self._loop)
  117. self._set_event_loop = True
  118. else:
  119. self._loop = self._loop_factory()
  120. if self._debug is not None:
  121. self._loop.set_debug(self._debug)
  122. self._context = contextvars.copy_context()
  123. self._state = _State.INITIALIZED
  124. def _on_sigint(self, signum, frame, main_task):
  125. self._interrupt_count += 1
  126. if self._interrupt_count == 1 and not main_task.done():
  127. main_task.cancel()
  128. # wakeup loop if it is blocked by select() with long timeout
  129. self._loop.call_soon_threadsafe(lambda: None)
  130. return
  131. raise KeyboardInterrupt()
  132. def run(main, *, debug=None, loop_factory=None):
  133. """Execute the coroutine and return the result.
  134. This function runs the passed coroutine, taking care of
  135. managing the asyncio event loop, finalizing asynchronous
  136. generators and closing the default executor.
  137. This function cannot be called when another asyncio event loop is
  138. running in the same thread.
  139. If debug is True, the event loop will be run in debug mode.
  140. This function always creates a new event loop and closes it at the end.
  141. It should be used as a main entry point for asyncio programs, and should
  142. ideally only be called once.
  143. The executor is given a timeout duration of 5 minutes to shutdown.
  144. If the executor hasn't finished within that duration, a warning is
  145. emitted and the executor is closed.
  146. Example:
  147. async def main():
  148. await asyncio.sleep(1)
  149. print('hello')
  150. asyncio.run(main())
  151. """
  152. if events._get_running_loop() is not None:
  153. # fail fast with short traceback
  154. raise RuntimeError(
  155. "asyncio.run() cannot be called from a running event loop")
  156. with Runner(debug=debug, loop_factory=loop_factory) as runner:
  157. return runner.run(main)
  158. def _cancel_all_tasks(loop):
  159. to_cancel = tasks.all_tasks(loop)
  160. if not to_cancel:
  161. return
  162. for task in to_cancel:
  163. task.cancel()
  164. loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True))
  165. for task in to_cancel:
  166. if task.cancelled():
  167. continue
  168. if task.exception() is not None:
  169. loop.call_exception_handler({
  170. 'message': 'unhandled exception during asyncio.run() shutdown',
  171. 'exception': task.exception(),
  172. 'task': task,
  173. })