__init__.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import asyncio
  2. import enum
  3. import sys
  4. import warnings
  5. from types import TracebackType
  6. from typing import Optional, Type
  7. if sys.version_info >= (3, 8):
  8. from typing import final
  9. else:
  10. from typing_extensions import final
  11. if sys.version_info >= (3, 11):
  12. def _uncancel_task(task: "asyncio.Task[object]") -> None:
  13. task.uncancel()
  14. else:
  15. def _uncancel_task(task: "asyncio.Task[object]") -> None:
  16. pass
  17. __version__ = "4.0.3"
  18. __all__ = ("timeout", "timeout_at", "Timeout")
  19. def timeout(delay: Optional[float]) -> "Timeout":
  20. """timeout context manager.
  21. Useful in cases when you want to apply timeout logic around block
  22. of code or in cases when asyncio.wait_for is not suitable. For example:
  23. >>> async with timeout(0.001):
  24. ... async with aiohttp.get('https://github.com') as r:
  25. ... await r.text()
  26. delay - value in seconds or None to disable timeout logic
  27. """
  28. loop = asyncio.get_running_loop()
  29. if delay is not None:
  30. deadline = loop.time() + delay # type: Optional[float]
  31. else:
  32. deadline = None
  33. return Timeout(deadline, loop)
  34. def timeout_at(deadline: Optional[float]) -> "Timeout":
  35. """Schedule the timeout at absolute time.
  36. deadline argument points on the time in the same clock system
  37. as loop.time().
  38. Please note: it is not POSIX time but a time with
  39. undefined starting base, e.g. the time of the system power on.
  40. >>> async with timeout_at(loop.time() + 10):
  41. ... async with aiohttp.get('https://github.com') as r:
  42. ... await r.text()
  43. """
  44. loop = asyncio.get_running_loop()
  45. return Timeout(deadline, loop)
  46. class _State(enum.Enum):
  47. INIT = "INIT"
  48. ENTER = "ENTER"
  49. TIMEOUT = "TIMEOUT"
  50. EXIT = "EXIT"
  51. @final
  52. class Timeout:
  53. # Internal class, please don't instantiate it directly
  54. # Use timeout() and timeout_at() public factories instead.
  55. #
  56. # Implementation note: `async with timeout()` is preferred
  57. # over `with timeout()`.
  58. # While technically the Timeout class implementation
  59. # doesn't need to be async at all,
  60. # the `async with` statement explicitly points that
  61. # the context manager should be used from async function context.
  62. #
  63. # This design allows to avoid many silly misusages.
  64. #
  65. # TimeoutError is raised immediately when scheduled
  66. # if the deadline is passed.
  67. # The purpose is to time out as soon as possible
  68. # without waiting for the next await expression.
  69. __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler", "_task")
  70. def __init__(
  71. self, deadline: Optional[float], loop: asyncio.AbstractEventLoop
  72. ) -> None:
  73. self._loop = loop
  74. self._state = _State.INIT
  75. self._task: Optional["asyncio.Task[object]"] = None
  76. self._timeout_handler = None # type: Optional[asyncio.Handle]
  77. if deadline is None:
  78. self._deadline = None # type: Optional[float]
  79. else:
  80. self.update(deadline)
  81. def __enter__(self) -> "Timeout":
  82. warnings.warn(
  83. "with timeout() is deprecated, use async with timeout() instead",
  84. DeprecationWarning,
  85. stacklevel=2,
  86. )
  87. self._do_enter()
  88. return self
  89. def __exit__(
  90. self,
  91. exc_type: Optional[Type[BaseException]],
  92. exc_val: Optional[BaseException],
  93. exc_tb: Optional[TracebackType],
  94. ) -> Optional[bool]:
  95. self._do_exit(exc_type)
  96. return None
  97. async def __aenter__(self) -> "Timeout":
  98. self._do_enter()
  99. return self
  100. async def __aexit__(
  101. self,
  102. exc_type: Optional[Type[BaseException]],
  103. exc_val: Optional[BaseException],
  104. exc_tb: Optional[TracebackType],
  105. ) -> Optional[bool]:
  106. self._do_exit(exc_type)
  107. return None
  108. @property
  109. def expired(self) -> bool:
  110. """Is timeout expired during execution?"""
  111. return self._state == _State.TIMEOUT
  112. @property
  113. def deadline(self) -> Optional[float]:
  114. return self._deadline
  115. def reject(self) -> None:
  116. """Reject scheduled timeout if any."""
  117. # cancel is maybe better name but
  118. # task.cancel() raises CancelledError in asyncio world.
  119. if self._state not in (_State.INIT, _State.ENTER):
  120. raise RuntimeError(f"invalid state {self._state.value}")
  121. self._reject()
  122. def _reject(self) -> None:
  123. self._task = None
  124. if self._timeout_handler is not None:
  125. self._timeout_handler.cancel()
  126. self._timeout_handler = None
  127. def shift(self, delay: float) -> None:
  128. """Advance timeout on delay seconds.
  129. The delay can be negative.
  130. Raise RuntimeError if shift is called when deadline is not scheduled
  131. """
  132. deadline = self._deadline
  133. if deadline is None:
  134. raise RuntimeError("cannot shift timeout if deadline is not scheduled")
  135. self.update(deadline + delay)
  136. def update(self, deadline: float) -> None:
  137. """Set deadline to absolute value.
  138. deadline argument points on the time in the same clock system
  139. as loop.time().
  140. If new deadline is in the past the timeout is raised immediately.
  141. Please note: it is not POSIX time but a time with
  142. undefined starting base, e.g. the time of the system power on.
  143. """
  144. if self._state == _State.EXIT:
  145. raise RuntimeError("cannot reschedule after exit from context manager")
  146. if self._state == _State.TIMEOUT:
  147. raise RuntimeError("cannot reschedule expired timeout")
  148. if self._timeout_handler is not None:
  149. self._timeout_handler.cancel()
  150. self._deadline = deadline
  151. if self._state != _State.INIT:
  152. self._reschedule()
  153. def _reschedule(self) -> None:
  154. assert self._state == _State.ENTER
  155. deadline = self._deadline
  156. if deadline is None:
  157. return
  158. now = self._loop.time()
  159. if self._timeout_handler is not None:
  160. self._timeout_handler.cancel()
  161. self._task = asyncio.current_task()
  162. if deadline <= now:
  163. self._timeout_handler = self._loop.call_soon(self._on_timeout)
  164. else:
  165. self._timeout_handler = self._loop.call_at(deadline, self._on_timeout)
  166. def _do_enter(self) -> None:
  167. if self._state != _State.INIT:
  168. raise RuntimeError(f"invalid state {self._state.value}")
  169. self._state = _State.ENTER
  170. self._reschedule()
  171. def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None:
  172. if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT:
  173. assert self._task is not None
  174. _uncancel_task(self._task)
  175. self._timeout_handler = None
  176. self._task = None
  177. raise asyncio.TimeoutError
  178. # timeout has not expired
  179. self._state = _State.EXIT
  180. self._reject()
  181. return None
  182. def _on_timeout(self) -> None:
  183. assert self._task is not None
  184. self._task.cancel()
  185. self._state = _State.TIMEOUT
  186. # drop the reference early
  187. self._timeout_handler = None