You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
6.4KB

  1. import asyncio
  2. import sys
  3. from typing import Any
  4. from typing import Callable
  5. from typing import Coroutine
  6. import greenlet
  7. from . import compat
  8. from .langhelpers import memoized_property
  9. from .. import exc
  10. if compat.py37:
  11. try:
  12. from contextvars import copy_context as _copy_context
  13. # If greenlet.gr_context is present in current version of greenlet,
  14. # it will be set with a copy of the current context on creation.
  15. # Refs: https://github.com/python-greenlet/greenlet/pull/198
  16. getattr(greenlet.greenlet, "gr_context")
  17. except (ImportError, AttributeError):
  18. _copy_context = None
  19. else:
  20. _copy_context = None
  21. def is_exit_exception(e):
  22. # note asyncio.CancelledError is already BaseException
  23. # so was an exit exception in any case
  24. return not isinstance(e, Exception) or isinstance(
  25. e, (asyncio.TimeoutError, asyncio.CancelledError)
  26. )
  27. # implementation based on snaury gist at
  28. # https://gist.github.com/snaury/202bf4f22c41ca34e56297bae5f33fef
  29. # Issue for context: https://github.com/python-greenlet/greenlet/issues/173
  30. class _AsyncIoGreenlet(greenlet.greenlet):
  31. def __init__(self, fn, driver):
  32. greenlet.greenlet.__init__(self, fn, driver)
  33. self.driver = driver
  34. if _copy_context is not None:
  35. self.gr_context = _copy_context()
  36. def await_only(awaitable: Coroutine) -> Any:
  37. """Awaits an async function in a sync method.
  38. The sync method must be inside a :func:`greenlet_spawn` context.
  39. :func:`await_` calls cannot be nested.
  40. :param awaitable: The coroutine to call.
  41. """
  42. # this is called in the context greenlet while running fn
  43. current = greenlet.getcurrent()
  44. if not isinstance(current, _AsyncIoGreenlet):
  45. raise exc.MissingGreenlet(
  46. "greenlet_spawn has not been called; can't call await_() here. "
  47. "Was IO attempted in an unexpected place?"
  48. )
  49. # returns the control to the driver greenlet passing it
  50. # a coroutine to run. Once the awaitable is done, the driver greenlet
  51. # switches back to this greenlet with the result of awaitable that is
  52. # then returned to the caller (or raised as error)
  53. return current.driver.switch(awaitable)
  54. def await_fallback(awaitable: Coroutine) -> Any:
  55. """Awaits an async function in a sync method.
  56. The sync method must be inside a :func:`greenlet_spawn` context.
  57. :func:`await_` calls cannot be nested.
  58. :param awaitable: The coroutine to call.
  59. """
  60. # this is called in the context greenlet while running fn
  61. current = greenlet.getcurrent()
  62. if not isinstance(current, _AsyncIoGreenlet):
  63. loop = get_event_loop()
  64. if loop.is_running():
  65. raise exc.MissingGreenlet(
  66. "greenlet_spawn has not been called and asyncio event "
  67. "loop is already running; can't call await_() here. "
  68. "Was IO attempted in an unexpected place?"
  69. )
  70. return loop.run_until_complete(awaitable)
  71. return current.driver.switch(awaitable)
  72. async def greenlet_spawn(
  73. fn: Callable, *args, _require_await=False, **kwargs
  74. ) -> Any:
  75. """Runs a sync function ``fn`` in a new greenlet.
  76. The sync function can then use :func:`await_` to wait for async
  77. functions.
  78. :param fn: The sync callable to call.
  79. :param \\*args: Positional arguments to pass to the ``fn`` callable.
  80. :param \\*\\*kwargs: Keyword arguments to pass to the ``fn`` callable.
  81. """
  82. context = _AsyncIoGreenlet(fn, greenlet.getcurrent())
  83. # runs the function synchronously in gl greenlet. If the execution
  84. # is interrupted by await_, context is not dead and result is a
  85. # coroutine to wait. If the context is dead the function has
  86. # returned, and its result can be returned.
  87. switch_occurred = False
  88. try:
  89. result = context.switch(*args, **kwargs)
  90. while not context.dead:
  91. switch_occurred = True
  92. try:
  93. # wait for a coroutine from await_ and then return its
  94. # result back to it.
  95. value = await result
  96. except BaseException:
  97. # this allows an exception to be raised within
  98. # the moderated greenlet so that it can continue
  99. # its expected flow.
  100. result = context.throw(*sys.exc_info())
  101. else:
  102. result = context.switch(value)
  103. finally:
  104. # clean up to avoid cycle resolution by gc
  105. del context.driver
  106. if _require_await and not switch_occurred:
  107. raise exc.AwaitRequired(
  108. "The current operation required an async execution but none was "
  109. "detected. This will usually happen when using a non compatible "
  110. "DBAPI driver. Please ensure that an async DBAPI is used."
  111. )
  112. return result
  113. class AsyncAdaptedLock:
  114. @memoized_property
  115. def mutex(self):
  116. # there should not be a race here for coroutines creating the
  117. # new lock as we are not using await, so therefore no concurrency
  118. return asyncio.Lock()
  119. def __enter__(self):
  120. # await is used to acquire the lock only after the first calling
  121. # coroutine has created the mutex.
  122. await_fallback(self.mutex.acquire())
  123. return self
  124. def __exit__(self, *arg, **kw):
  125. self.mutex.release()
  126. def _util_async_run_coroutine_function(fn, *args, **kwargs):
  127. """for test suite/ util only"""
  128. loop = get_event_loop()
  129. if loop.is_running():
  130. raise Exception(
  131. "for async run coroutine we expect that no greenlet or event "
  132. "loop is running when we start out"
  133. )
  134. return loop.run_until_complete(fn(*args, **kwargs))
  135. def _util_async_run(fn, *args, **kwargs):
  136. """for test suite/ util only"""
  137. loop = get_event_loop()
  138. if not loop.is_running():
  139. return loop.run_until_complete(greenlet_spawn(fn, *args, **kwargs))
  140. else:
  141. # allow for a wrapped test function to call another
  142. assert isinstance(greenlet.getcurrent(), _AsyncIoGreenlet)
  143. return fn(*args, **kwargs)
  144. def get_event_loop():
  145. """vendor asyncio.get_event_loop() for python 3.7 and above.
  146. Python 3.10 deprecates get_event_loop() as a standalone.
  147. """
  148. if compat.py37:
  149. try:
  150. return asyncio.get_running_loop()
  151. except RuntimeError:
  152. return asyncio.get_event_loop_policy().get_event_loop()
  153. else:
  154. return asyncio.get_event_loop()