base.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. # ext/asyncio/base.py
  2. # Copyright (C) 2020-2024 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: https://www.opensource.org/licenses/mit-license.php
  7. from __future__ import annotations
  8. import abc
  9. import functools
  10. from typing import Any
  11. from typing import AsyncGenerator
  12. from typing import AsyncIterator
  13. from typing import Awaitable
  14. from typing import Callable
  15. from typing import ClassVar
  16. from typing import Dict
  17. from typing import Generator
  18. from typing import Generic
  19. from typing import NoReturn
  20. from typing import Optional
  21. from typing import overload
  22. from typing import Tuple
  23. from typing import TypeVar
  24. import weakref
  25. from . import exc as async_exc
  26. from ... import util
  27. from ...util.typing import Literal
  28. from ...util.typing import Self
  29. _T = TypeVar("_T", bound=Any)
  30. _T_co = TypeVar("_T_co", bound=Any, covariant=True)
  31. _PT = TypeVar("_PT", bound=Any)
  32. class ReversibleProxy(Generic[_PT]):
  33. _proxy_objects: ClassVar[
  34. Dict[weakref.ref[Any], weakref.ref[ReversibleProxy[Any]]]
  35. ] = {}
  36. __slots__ = ("__weakref__",)
  37. @overload
  38. def _assign_proxied(self, target: _PT) -> _PT:
  39. ...
  40. @overload
  41. def _assign_proxied(self, target: None) -> None:
  42. ...
  43. def _assign_proxied(self, target: Optional[_PT]) -> Optional[_PT]:
  44. if target is not None:
  45. target_ref: weakref.ref[_PT] = weakref.ref(
  46. target, ReversibleProxy._target_gced
  47. )
  48. proxy_ref = weakref.ref(
  49. self,
  50. functools.partial(ReversibleProxy._target_gced, target_ref),
  51. )
  52. ReversibleProxy._proxy_objects[target_ref] = proxy_ref
  53. return target
  54. @classmethod
  55. def _target_gced(
  56. cls,
  57. ref: weakref.ref[_PT],
  58. proxy_ref: Optional[weakref.ref[Self]] = None, # noqa: U100
  59. ) -> None:
  60. cls._proxy_objects.pop(ref, None)
  61. @classmethod
  62. def _regenerate_proxy_for_target(cls, target: _PT) -> Self:
  63. raise NotImplementedError()
  64. @overload
  65. @classmethod
  66. def _retrieve_proxy_for_target(
  67. cls,
  68. target: _PT,
  69. regenerate: Literal[True] = ...,
  70. ) -> Self:
  71. ...
  72. @overload
  73. @classmethod
  74. def _retrieve_proxy_for_target(
  75. cls, target: _PT, regenerate: bool = True
  76. ) -> Optional[Self]:
  77. ...
  78. @classmethod
  79. def _retrieve_proxy_for_target(
  80. cls, target: _PT, regenerate: bool = True
  81. ) -> Optional[Self]:
  82. try:
  83. proxy_ref = cls._proxy_objects[weakref.ref(target)]
  84. except KeyError:
  85. pass
  86. else:
  87. proxy = proxy_ref()
  88. if proxy is not None:
  89. return proxy # type: ignore
  90. if regenerate:
  91. return cls._regenerate_proxy_for_target(target)
  92. else:
  93. return None
  94. class StartableContext(Awaitable[_T_co], abc.ABC):
  95. __slots__ = ()
  96. @abc.abstractmethod
  97. async def start(self, is_ctxmanager: bool = False) -> _T_co:
  98. raise NotImplementedError()
  99. def __await__(self) -> Generator[Any, Any, _T_co]:
  100. return self.start().__await__()
  101. async def __aenter__(self) -> _T_co:
  102. return await self.start(is_ctxmanager=True)
  103. @abc.abstractmethod
  104. async def __aexit__(
  105. self, type_: Any, value: Any, traceback: Any
  106. ) -> Optional[bool]:
  107. pass
  108. def _raise_for_not_started(self) -> NoReturn:
  109. raise async_exc.AsyncContextNotStarted(
  110. "%s context has not been started and object has not been awaited."
  111. % (self.__class__.__name__)
  112. )
  113. class GeneratorStartableContext(StartableContext[_T_co]):
  114. __slots__ = ("gen",)
  115. gen: AsyncGenerator[_T_co, Any]
  116. def __init__(
  117. self,
  118. func: Callable[..., AsyncIterator[_T_co]],
  119. args: Tuple[Any, ...],
  120. kwds: Dict[str, Any],
  121. ):
  122. self.gen = func(*args, **kwds) # type: ignore
  123. async def start(self, is_ctxmanager: bool = False) -> _T_co:
  124. try:
  125. start_value = await util.anext_(self.gen)
  126. except StopAsyncIteration:
  127. raise RuntimeError("generator didn't yield") from None
  128. # if not a context manager, then interrupt the generator, don't
  129. # let it complete. this step is technically not needed, as the
  130. # generator will close in any case at gc time. not clear if having
  131. # this here is a good idea or not (though it helps for clarity IMO)
  132. if not is_ctxmanager:
  133. await self.gen.aclose()
  134. return start_value
  135. async def __aexit__(
  136. self, typ: Any, value: Any, traceback: Any
  137. ) -> Optional[bool]:
  138. # vendored from contextlib.py
  139. if typ is None:
  140. try:
  141. await util.anext_(self.gen)
  142. except StopAsyncIteration:
  143. return False
  144. else:
  145. raise RuntimeError("generator didn't stop")
  146. else:
  147. if value is None:
  148. # Need to force instantiation so we can reliably
  149. # tell if we get the same exception back
  150. value = typ()
  151. try:
  152. await self.gen.athrow(value)
  153. except StopAsyncIteration as exc:
  154. # Suppress StopIteration *unless* it's the same exception that
  155. # was passed to throw(). This prevents a StopIteration
  156. # raised inside the "with" statement from being suppressed.
  157. return exc is not value
  158. except RuntimeError as exc:
  159. # Don't re-raise the passed in exception. (issue27122)
  160. if exc is value:
  161. return False
  162. # Avoid suppressing if a Stop(Async)Iteration exception
  163. # was passed to athrow() and later wrapped into a RuntimeError
  164. # (see PEP 479 for sync generators; async generators also
  165. # have this behavior). But do this only if the exception
  166. # wrapped
  167. # by the RuntimeError is actully Stop(Async)Iteration (see
  168. # issue29692).
  169. if (
  170. isinstance(value, (StopIteration, StopAsyncIteration))
  171. and exc.__cause__ is value
  172. ):
  173. return False
  174. raise
  175. except BaseException as exc:
  176. # only re-raise if it's *not* the exception that was
  177. # passed to throw(), because __exit__() must not raise
  178. # an exception unless __exit__() itself failed. But throw()
  179. # has to raise the exception to signal propagation, so this
  180. # fixes the impedance mismatch between the throw() protocol
  181. # and the __exit__() protocol.
  182. if exc is not value:
  183. raise
  184. return False
  185. raise RuntimeError("generator didn't stop after athrow()")
  186. def asyncstartablecontext(
  187. func: Callable[..., AsyncIterator[_T_co]]
  188. ) -> Callable[..., GeneratorStartableContext[_T_co]]:
  189. """@asyncstartablecontext decorator.
  190. the decorated function can be called either as ``async with fn()``, **or**
  191. ``await fn()``. This is decidedly different from what
  192. ``@contextlib.asynccontextmanager`` supports, and the usage pattern
  193. is different as well.
  194. Typical usage::
  195. @asyncstartablecontext
  196. async def some_async_generator(<arguments>):
  197. <setup>
  198. try:
  199. yield <value>
  200. except GeneratorExit:
  201. # return value was awaited, no context manager is present
  202. # and caller will .close() the resource explicitly
  203. pass
  204. else:
  205. <context manager cleanup>
  206. Above, ``GeneratorExit`` is caught if the function were used as an
  207. ``await``. In this case, it's essential that the cleanup does **not**
  208. occur, so there should not be a ``finally`` block.
  209. If ``GeneratorExit`` is not invoked, this means we're in ``__aexit__``
  210. and we were invoked as a context manager, and cleanup should proceed.
  211. """
  212. @functools.wraps(func)
  213. def helper(*args: Any, **kwds: Any) -> GeneratorStartableContext[_T_co]:
  214. return GeneratorStartableContext(func, args, kwds)
  215. return helper
  216. class ProxyComparable(ReversibleProxy[_PT]):
  217. __slots__ = ()
  218. @util.ro_non_memoized_property
  219. def _proxied(self) -> _PT:
  220. raise NotImplementedError()
  221. def __hash__(self) -> int:
  222. return id(self)
  223. def __eq__(self, other: Any) -> bool:
  224. return (
  225. isinstance(other, self.__class__)
  226. and self._proxied == other._proxied
  227. )
  228. def __ne__(self, other: Any) -> bool:
  229. return (
  230. not isinstance(other, self.__class__)
  231. or self._proxied != other._proxied
  232. )