python-backoff 2.3.1__py3-none-any.whl → 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
backoff/__init__.py CHANGED
@@ -12,20 +12,22 @@ For examples and full documentation see the README at
12
12
  https://github.com/python-backoff/backoff
13
13
  """
14
14
 
15
- from backoff._decorator import on_exception, on_predicate
15
+ from backoff._decorator import aretry_context, on_exception, on_predicate, retry_context
16
16
  from backoff._jitter import full_jitter, random_jitter
17
- from backoff._wait_gen import constant, expo, fibo, runtime, decay
17
+ from backoff._wait_gen import constant, decay, expo, fibo, runtime
18
18
 
19
19
  __all__ = [
20
- "on_predicate",
21
- "on_exception",
20
+ "aretry_context",
22
21
  "constant",
23
- "expo",
24
22
  "decay",
23
+ "expo",
25
24
  "fibo",
26
- "runtime",
27
25
  "full_jitter",
26
+ "on_exception",
27
+ "on_predicate",
28
28
  "random_jitter",
29
+ "retry_context",
30
+ "runtime",
29
31
  ]
30
32
 
31
- __version__ = "2.3.1"
33
+ __version__ = "2.4.0"
backoff/_async.py CHANGED
@@ -1,53 +1,101 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import functools
3
5
  import inspect
4
- import time
6
+ from typing import TYPE_CHECKING, Any, Callable, TypeVar
7
+
8
+ from backoff._common import _Attempt, _RetryState
9
+
10
+ if TYPE_CHECKING:
11
+ import sys
12
+ from collections.abc import AsyncGenerator, Coroutine, Iterable
13
+
14
+ from backoff._typing import (
15
+ ContextDetails,
16
+ Details,
17
+ _BaseDetails,
18
+ _CallDetails,
19
+ _ContextHandler,
20
+ _Handler,
21
+ _Jitterer,
22
+ _MaybeCallable,
23
+ _MaybeTuple,
24
+ _Predicate,
25
+ _WaitGenerator,
26
+ )
27
+
28
+ if sys.version_info >= (3, 10):
29
+ from typing import ParamSpec
30
+ else:
31
+ from typing_extensions import ParamSpec
32
+
33
+ if sys.version_info >= (3, 11):
34
+ from typing import Unpack
35
+ else:
36
+ from typing_extensions import Unpack
37
+
38
+ T = TypeVar("T")
39
+ P = ParamSpec("P")
5
40
 
6
- from backoff._common import _init_wait_gen, _maybe_call, _next_wait
41
+ _AsyncHandler = Callable[["Details"], "Coroutine[Any, Any, None]"]
7
42
 
8
43
 
9
- def _ensure_coroutine(coro_or_func):
44
+ def _ensure_coroutine(
45
+ coro_or_func: Callable[..., Any],
46
+ ) -> Callable[..., Coroutine[Any, Any, Any]]:
10
47
  if inspect.iscoroutinefunction(coro_or_func):
11
48
  return coro_or_func
12
- else:
13
49
 
14
- @functools.wraps(coro_or_func)
15
- async def f(*args, **kwargs):
16
- return coro_or_func(*args, **kwargs)
50
+ @functools.wraps(coro_or_func)
51
+ async def f(*args: Any, **kwargs: Any) -> Any: # ruff:ignore[unused-async]
52
+ return coro_or_func(*args, **kwargs)
17
53
 
18
- return f
54
+ return f
19
55
 
20
56
 
21
- def _ensure_coroutines(coros_or_funcs):
57
+ def _ensure_coroutines(
58
+ coros_or_funcs: Iterable[Callable[..., Any]],
59
+ ) -> list[Callable[..., Coroutine[Any, Any, Any]]]:
22
60
  return [_ensure_coroutine(f) for f in coros_or_funcs]
23
61
 
24
62
 
25
- async def _call_handlers(handlers, *, target, args, kwargs, tries, elapsed, **extra):
26
- details = {
63
+ async def _call_handlers(
64
+ handlers: Iterable[_AsyncHandler],
65
+ *,
66
+ target: Callable[..., Any],
67
+ args: tuple[Any, ...],
68
+ kwargs: dict[str, Any],
69
+ tries: int,
70
+ elapsed: float,
71
+ **extra: Unpack[_CallDetails],
72
+ ) -> None:
73
+ details: Details = {
27
74
  "target": target,
28
75
  "args": args,
29
76
  "kwargs": kwargs,
30
77
  "tries": tries,
31
78
  "elapsed": elapsed,
32
79
  }
80
+ # pyrefly: ignore [no-matching-overload]
33
81
  details.update(extra)
34
82
  for handler in handlers:
35
83
  await handler(details)
36
84
 
37
85
 
38
86
  def retry_predicate(
39
- target,
40
- wait_gen,
41
- predicate,
87
+ target: Callable[P, T],
88
+ wait_gen: _WaitGenerator,
89
+ predicate: _Predicate[T],
42
90
  *,
43
- max_tries,
44
- max_time,
45
- jitter,
46
- on_success,
47
- on_backoff,
48
- on_giveup,
49
- wait_gen_kwargs,
50
- ):
91
+ max_tries: _MaybeCallable[int] | None,
92
+ max_time: _MaybeCallable[float] | None,
93
+ jitter: _Jitterer | None,
94
+ on_success: Iterable[_Handler],
95
+ on_backoff: Iterable[_Handler],
96
+ on_giveup: Iterable[_Handler],
97
+ wait_gen_kwargs: dict[str, Any],
98
+ ) -> Callable[P, T]:
51
99
  on_success = _ensure_coroutines(on_success)
52
100
  on_backoff = _ensure_coroutines(on_backoff)
53
101
  on_giveup = _ensure_coroutines(on_giveup)
@@ -59,38 +107,31 @@ def retry_predicate(
59
107
  assert inspect.iscoroutinefunction(target)
60
108
 
61
109
  @functools.wraps(target)
62
- async def retry(*args, **kwargs):
63
- # update variables from outer function args
64
- max_tries_value = _maybe_call(max_tries)
65
- max_time_value = _maybe_call(max_time)
66
-
67
- tries = 0
68
- start = time.monotonic()
69
- wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
110
+ async def retry(*args: P.args, **kwargs: P.kwargs) -> T:
111
+ state = _RetryState(
112
+ wait_gen,
113
+ wait_gen_kwargs,
114
+ max_tries=max_tries,
115
+ max_time=max_time,
116
+ )
70
117
  while True:
71
- tries += 1
72
- elapsed = time.monotonic() - start
73
- details = {
118
+ state.start_attempt()
119
+ ret = await target(*args, **kwargs)
120
+ details: _BaseDetails = {
74
121
  "target": target,
75
122
  "args": args,
76
123
  "kwargs": kwargs,
77
- "tries": tries,
78
- "elapsed": elapsed,
124
+ "tries": state.tries,
125
+ "elapsed": state.record_elapsed(),
79
126
  }
80
127
 
81
- ret = await target(*args, **kwargs)
82
128
  if predicate(ret):
83
- max_tries_exceeded = tries == max_tries_value
84
- max_time_exceeded = (
85
- max_time_value is not None and elapsed >= max_time_value
86
- )
87
-
88
- if max_tries_exceeded or max_time_exceeded:
129
+ if state.exhausted():
89
130
  await _call_handlers(on_giveup, **details, value=ret)
90
131
  break
91
132
 
92
133
  try:
93
- seconds = _next_wait(wait, ret, jitter, elapsed, max_time_value)
134
+ seconds = state.next_wait(ret, jitter)
94
135
  except StopIteration:
95
136
  await _call_handlers(on_giveup, **details, value=ret)
96
137
  break
@@ -108,30 +149,48 @@ def retry_predicate(
108
149
  # <https://bugs.python.org/issue28613>
109
150
  await asyncio.sleep(seconds)
110
151
  continue
111
- else:
112
- await _call_handlers(on_success, **details, value=ret)
113
- break
152
+ await _call_handlers(on_success, **details, value=ret)
153
+ break
114
154
 
115
155
  return ret
116
156
 
117
- return retry
157
+ return retry # type: ignore[return-value] # ty:ignore[invalid-return-type]
158
+
159
+
160
+ def _adapt_context_handlers(
161
+ handlers: Iterable[_AsyncHandler],
162
+ target: Callable[..., Any],
163
+ args: tuple[Any, ...],
164
+ kwargs: dict[str, Any],
165
+ ) -> list[_ContextHandler]:
166
+ async def adapted(details: ContextDetails) -> None:
167
+ full_details: Details = {
168
+ "target": target,
169
+ "args": args,
170
+ "kwargs": kwargs,
171
+ **details,
172
+ }
173
+ for hdlr in handlers:
174
+ await hdlr(full_details)
175
+
176
+ return [adapted]
118
177
 
119
178
 
120
179
  def retry_exception(
121
- target,
122
- wait_gen,
123
- exception,
180
+ target: Callable[P, T],
181
+ wait_gen: _WaitGenerator,
182
+ exception: _MaybeTuple[type[Exception]],
124
183
  *,
125
- max_tries,
126
- max_time,
127
- jitter,
128
- giveup,
129
- on_success,
130
- on_backoff,
131
- on_giveup,
132
- raise_on_giveup,
133
- wait_gen_kwargs,
134
- ):
184
+ max_tries: _MaybeCallable[int] | None,
185
+ max_time: _MaybeCallable[float] | None,
186
+ jitter: _Jitterer | None,
187
+ giveup: _Predicate[Exception],
188
+ on_success: Iterable[_Handler],
189
+ on_backoff: Iterable[_Handler],
190
+ on_giveup: Iterable[_Handler],
191
+ raise_on_giveup: bool,
192
+ wait_gen_kwargs: dict[str, Any],
193
+ ) -> Callable[P, T]:
135
194
  on_success = _ensure_coroutines(on_success)
136
195
  on_backoff = _ensure_coroutines(on_backoff)
137
196
  on_giveup = _ensure_coroutines(on_giveup)
@@ -142,60 +201,101 @@ def retry_exception(
142
201
  assert not inspect.iscoroutinefunction(jitter)
143
202
 
144
203
  @functools.wraps(target)
145
- async def retry(*args, **kwargs):
146
- max_tries_value = _maybe_call(max_tries)
147
- max_time_value = _maybe_call(max_time)
204
+ async def retry(
205
+ *args: P.args,
206
+ **kwargs: P.kwargs,
207
+ ) -> T:
208
+ ret: T = None # type: ignore[assignment] # ty:ignore[invalid-assignment]
209
+
210
+ async for attempt in aretry_context(
211
+ exception,
212
+ wait_gen,
213
+ max_tries=max_tries,
214
+ max_time=max_time,
215
+ jitter=jitter,
216
+ giveup=giveup,
217
+ on_success=_adapt_context_handlers(on_success, target, args, kwargs),
218
+ on_backoff=_adapt_context_handlers(on_backoff, target, args, kwargs),
219
+ on_giveup=_adapt_context_handlers(on_giveup, target, args, kwargs),
220
+ raise_on_giveup=raise_on_giveup,
221
+ wait_gen_kwargs=wait_gen_kwargs,
222
+ ):
223
+ with attempt:
224
+ ret = await target(*args, **kwargs) # type: ignore[misc] # ty:ignore[invalid-await]
148
225
 
149
- tries = 0
150
- start = time.monotonic()
151
- wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
152
- while True:
153
- tries += 1
154
- elapsed = time.monotonic() - start
155
- details = {
156
- "target": target,
157
- "args": args,
158
- "kwargs": kwargs,
159
- "tries": tries,
160
- "elapsed": elapsed,
161
- }
226
+ return ret
162
227
 
163
- try:
164
- ret = await target(*args, **kwargs)
165
- except exception as e:
166
- giveup_result = await giveup(e)
167
- max_tries_exceeded = tries == max_tries_value
168
- max_time_exceeded = (
169
- max_time_value is not None and elapsed >= max_time_value
170
- )
171
-
172
- if giveup_result or max_tries_exceeded or max_time_exceeded:
173
- await _call_handlers(on_giveup, **details, exception=e)
174
- if raise_on_giveup:
175
- raise
176
- return None
228
+ return retry # type: ignore[return-value] # ty:ignore[invalid-return-type]
177
229
 
178
- try:
179
- seconds = _next_wait(wait, e, jitter, elapsed, max_time_value)
180
- except StopIteration:
181
- await _call_handlers(on_giveup, **details, exception=e)
182
- raise e
183
230
 
184
- await _call_handlers(on_backoff, **details, wait=seconds, exception=e)
231
+ async def _dispatch_handlers(
232
+ handlers: Iterable[_ContextHandler], **details: Any
233
+ ) -> None:
234
+ for hdlr in _ensure_coroutines(handlers):
235
+ await hdlr(details)
185
236
 
186
- # Note: there is no convenient way to pass explicit event
187
- # loop to decorator, so here we assume that either default
188
- # thread event loop is set and correct (it mostly is
189
- # by default), or Python >= 3.5.3 or Python >= 3.6 is used
190
- # where loop.get_event_loop() in coroutine guaranteed to
191
- # return correct value.
192
- # See for details:
193
- # <https://groups.google.com/forum/#!topic/python-tulip/yF9C-rFpiKk>
194
- # <https://bugs.python.org/issue28613>
195
- await asyncio.sleep(seconds)
196
- else:
197
- await _call_handlers(on_success, **details)
198
237
 
199
- return ret
238
+ async def aretry_context(
239
+ exception: _MaybeTuple[type[Exception]],
240
+ wait_gen: _WaitGenerator,
241
+ *,
242
+ max_tries: _MaybeCallable[int] | None,
243
+ max_time: _MaybeCallable[float] | None,
244
+ jitter: _Jitterer | None,
245
+ giveup: _Predicate[BaseException],
246
+ on_success: Iterable[_ContextHandler],
247
+ on_backoff: Iterable[_ContextHandler],
248
+ on_giveup: Iterable[_ContextHandler],
249
+ raise_on_giveup: bool,
250
+ wait_gen_kwargs: dict[str, Any],
251
+ ) -> AsyncGenerator[_Attempt, None]:
252
+ giveup = _ensure_coroutine(giveup)
200
253
 
201
- return retry
254
+ state = _RetryState(
255
+ wait_gen,
256
+ wait_gen_kwargs,
257
+ max_tries=max_tries,
258
+ max_time=max_time,
259
+ )
260
+ while True:
261
+ state.start_attempt()
262
+ attempt = _Attempt(exception)
263
+ yield attempt
264
+ elapsed = state.record_elapsed()
265
+
266
+ exc = attempt.exception
267
+ if exc is None:
268
+ await _dispatch_handlers(on_success, tries=state.tries, elapsed=elapsed)
269
+ return
270
+
271
+ if await giveup(exc) or state.exhausted():
272
+ await _dispatch_handlers(
273
+ on_giveup,
274
+ tries=state.tries,
275
+ elapsed=elapsed,
276
+ exception=exc,
277
+ )
278
+ if raise_on_giveup:
279
+ raise exc
280
+ return
281
+
282
+ try:
283
+ seconds = state.next_wait(exc, jitter)
284
+ except StopIteration:
285
+ await _dispatch_handlers(
286
+ on_giveup,
287
+ tries=state.tries,
288
+ elapsed=elapsed,
289
+ exception=exc,
290
+ )
291
+ raise exc from None
292
+
293
+ await _dispatch_handlers(
294
+ on_backoff,
295
+ tries=state.tries,
296
+ elapsed=elapsed,
297
+ wait=seconds,
298
+ exception=exc,
299
+ )
300
+
301
+ await asyncio.sleep(seconds)