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.

_concurrency_py3k.py 6.4KB

3 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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()