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