python-backoff 2.3.1__py3-none-any.whl → 3.0.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/_common.py CHANGED
@@ -1,8 +1,33 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import logging
3
- import sys
5
+ import time
4
6
  import traceback
5
- import warnings
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
9
+
10
+ if TYPE_CHECKING:
11
+ import sys
12
+ from collections.abc import Callable, Generator, Iterable
13
+
14
+ from backoff._typing import (
15
+ ContextDetails,
16
+ Details,
17
+ _AnyLogger,
18
+ _AnyLoggerOrName,
19
+ _ContextHandler,
20
+ _Jitterer,
21
+ _MaybeCallable,
22
+ _WaitGenerator,
23
+ )
24
+
25
+ if sys.version_info >= (3, 11):
26
+ from typing import Self
27
+ else:
28
+ from typing_extensions import Self
29
+
30
+ ExceptionTypes: TypeAlias = type[Exception] | tuple[type[Exception], ...]
6
31
 
7
32
 
8
33
  # Use module-specific logger with a default null handler.
@@ -10,39 +35,40 @@ _logger = logging.getLogger("backoff")
10
35
  _logger.addHandler(logging.NullHandler()) # pragma: no cover
11
36
  _logger.setLevel(logging.INFO)
12
37
 
38
+ T = TypeVar("T")
39
+ _HandlerT = TypeVar("_HandlerT")
40
+
13
41
 
14
42
  # Evaluate arg that can be either a fixed value or a callable.
15
- def _maybe_call(f, *args, **kwargs):
43
+ def _maybe_call(f: _MaybeCallable[T] | None, *args: Any, **kwargs: Any) -> T | None:
16
44
  if callable(f):
17
45
  try:
18
- return f(*args, **kwargs)
46
+ return f(*args, **kwargs) # ty:ignore[call-top-callable, invalid-return-type]
19
47
  except TypeError:
20
- return f
48
+ return f # type: ignore[return-value] # ty:ignore[invalid-return-type]
21
49
  else:
22
50
  return f
23
51
 
24
52
 
25
- def _init_wait_gen(wait_gen, wait_gen_kwargs):
53
+ def _init_wait_gen(
54
+ wait_gen: _WaitGenerator,
55
+ wait_gen_kwargs: dict[str, Any],
56
+ ) -> Generator[float, Any, None]:
26
57
  kwargs = {k: _maybe_call(v) for k, v in wait_gen_kwargs.items()}
27
58
  initialized = wait_gen(**kwargs)
28
59
  initialized.send(None) # Initialize with an empty send
29
60
  return initialized
30
61
 
31
62
 
32
- def _next_wait(wait, send_value, jitter, elapsed, max_time):
63
+ def _next_wait(
64
+ wait: Generator[float, None, None],
65
+ send_value: Any,
66
+ jitter: _Jitterer | None,
67
+ elapsed: float,
68
+ max_time: float | None,
69
+ ) -> float:
33
70
  value = wait.send(send_value)
34
- try:
35
- seconds = jitter(value) if jitter is not None else value
36
- except TypeError:
37
- warnings.warn(
38
- "Nullary jitter function signature is deprecated. Use "
39
- "unary signature accepting a wait value in seconds and "
40
- "returning a jittered version of it.",
41
- DeprecationWarning,
42
- stacklevel=2,
43
- )
44
-
45
- seconds = value + jitter()
71
+ seconds = jitter(value) if jitter is not None else value
46
72
 
47
73
  # don't sleep longer than remaining allotted max_time
48
74
  if max_time is not None:
@@ -51,7 +77,95 @@ def _next_wait(wait, send_value, jitter, elapsed, max_time):
51
77
  return seconds
52
78
 
53
79
 
54
- def _prepare_logger(logger):
80
+ class _RetryState:
81
+ """Bookkeeping shared by the sync and async retry loops.
82
+
83
+ Tracks try count and elapsed time, and owns the initialized wait
84
+ generator, so `retry_predicate`/`retry_exception` only need to drive
85
+ the call/check/sleep sequence around it.
86
+ """
87
+
88
+ __slots__ = (
89
+ "elapsed",
90
+ "max_time",
91
+ "max_tries",
92
+ "start",
93
+ "tries",
94
+ "wait",
95
+ )
96
+
97
+ def __init__(
98
+ self,
99
+ wait_gen: _WaitGenerator,
100
+ wait_gen_kwargs: dict[str, Any],
101
+ *,
102
+ max_tries: _MaybeCallable[int] | None = None,
103
+ max_time: _MaybeCallable[float] | None = None,
104
+ ) -> None:
105
+ self.tries = 0
106
+ self.elapsed: float = 0
107
+ self.start = time.monotonic()
108
+ self.max_tries = _maybe_call(max_tries)
109
+ self.max_time = _maybe_call(max_time)
110
+ self.wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
111
+
112
+ def start_attempt(self) -> None:
113
+ self.tries += 1
114
+
115
+ def record_elapsed(self) -> float:
116
+ self.elapsed = time.monotonic() - self.start
117
+ return self.elapsed
118
+
119
+ def exhausted(self) -> bool:
120
+ max_tries_exceeded = self.tries == self.max_tries
121
+ max_time_exceeded = self.max_time is not None and self.elapsed >= self.max_time
122
+ return max_tries_exceeded or max_time_exceeded
123
+
124
+ def next_wait(self, send_value: Any, jitter: _Jitterer | None) -> float:
125
+ return _next_wait(self.wait, send_value, jitter, self.elapsed, self.max_time)
126
+
127
+
128
+ @dataclass(slots=True)
129
+ class Attempt:
130
+ """A single attempt yielded by `retry_context`/`aretry_context`.
131
+
132
+ Used as `with attempt: ...`. Exceptions matching `exception_types` are
133
+ caught here so the driving generator (not this object) decides whether
134
+ to retry, sleep, or let the exception propagate; anything else, or no
135
+ exception at all, is left for the `with` block's normal exit behavior.
136
+ """
137
+
138
+ exception_types: ExceptionTypes
139
+ exception: BaseException | None = field(init=False)
140
+
141
+ def __post_init__(self) -> None:
142
+ self.exception: BaseException | None = None
143
+
144
+ def __enter__(self) -> Self:
145
+ return self
146
+
147
+ def __exit__(
148
+ self,
149
+ exc_type: type[BaseException] | None,
150
+ exc: BaseException | None,
151
+ tb: object,
152
+ ) -> bool:
153
+ if exc_type is None:
154
+ return False
155
+
156
+ if not issubclass(exc_type, self.exception_types):
157
+ return False # not ours; propagate immediately
158
+
159
+ self.exception = exc
160
+ return True # suppress for now; the driving generator decides next
161
+
162
+
163
+ def _dispatch_handlers(handlers: Iterable[_ContextHandler], **details: Any) -> None:
164
+ for hdlr in handlers:
165
+ hdlr(details) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
166
+
167
+
168
+ def _prepare_logger(logger: _AnyLoggerOrName | None) -> _AnyLogger | None:
55
169
  if isinstance(logger, str):
56
170
  logger = logging.getLogger(logger)
57
171
  return logger
@@ -60,22 +174,23 @@ def _prepare_logger(logger):
60
174
  # Configure handler list with user specified handler and optionally
61
175
  # with a default handler bound to the specified logger.
62
176
  def _config_handlers(
63
- user_handlers,
177
+ user_handlers: _HandlerT | Iterable[_HandlerT] | None,
64
178
  *,
65
- default_handler=None,
66
- logger=None,
67
- log_level=None,
68
- ):
69
- handlers = []
179
+ default_handler: Callable[..., None] | None = None,
180
+ logger: _AnyLogger | None = None,
181
+ log_level: int | None = None,
182
+ ) -> list[_HandlerT]:
183
+ handlers: list[_HandlerT] = []
70
184
  if logger is not None:
71
185
  assert log_level is not None, "Log level is not specified"
186
+ assert default_handler is not None, "Default handler is not specified"
72
187
  # bind the specified logger to the default log handler
73
188
  log_handler = functools.partial(
74
189
  default_handler,
75
190
  logger=logger,
76
191
  log_level=log_level,
77
192
  )
78
- handlers.append(log_handler)
193
+ handlers.append(log_handler) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
79
194
 
80
195
  if user_handlers is None:
81
196
  return handlers
@@ -84,22 +199,24 @@ def _config_handlers(
84
199
  # or a single handler. either way append them to the list.
85
200
  if hasattr(user_handlers, "__iter__"):
86
201
  # add all handlers in the iterable
87
- handlers += list(user_handlers)
202
+ # pyrefly: ignore [bad-argument-type]
203
+ handlers += list(user_handlers) # ty:ignore[invalid-argument-type]
88
204
  else:
89
205
  # append a single handler
206
+ # pyrefly: ignore [bad-argument-type]
90
207
  handlers.append(user_handlers)
91
208
 
92
209
  return handlers
93
210
 
94
211
 
95
212
  # Default backoff handler
96
- def _log_backoff(details, logger, log_level):
213
+ def _log_backoff(details: Details, logger: _AnyLogger, log_level: int) -> None:
97
214
  msg = "Backing off %s(...) for %.1fs (%s)"
98
- log_args = [details["target"].__name__, details["wait"]]
215
+ log_args = [details["target"].__name__, details["wait"]] # ty:ignore[unresolved-attribute]
99
216
 
100
- exc_typ, exc, _ = sys.exc_info()
217
+ exc = details.get("exception")
101
218
  if exc is not None:
102
- exc_fmt = traceback.format_exception_only(exc_typ, exc)[-1]
219
+ exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
103
220
  log_args.append(exc_fmt.rstrip("\n"))
104
221
  else:
105
222
  log_args.append(details["value"])
@@ -107,15 +224,45 @@ def _log_backoff(details, logger, log_level):
107
224
 
108
225
 
109
226
  # Default giveup handler
110
- def _log_giveup(details, logger, log_level):
227
+ def _log_giveup(details: Details, logger: _AnyLogger, log_level: int) -> None:
111
228
  msg = "Giving up %s(...) after %d tries (%s)"
112
- log_args = [details["target"].__name__, details["tries"]]
229
+ log_args = [details["target"].__name__, details["tries"]] # ty:ignore[unresolved-attribute]
113
230
 
114
- exc_typ, exc, _ = sys.exc_info()
231
+ exc = details.get("exception")
115
232
  if exc is not None:
116
- exc_fmt = traceback.format_exception_only(exc_typ, exc)[-1]
233
+ exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
117
234
  log_args.append(exc_fmt.rstrip("\n"))
118
235
  else:
119
236
  log_args.append(details["value"])
120
237
 
121
238
  logger.log(log_level, msg, *log_args)
239
+
240
+
241
+ # Default backoff handler for retry_context/aretry_context (no wrapped
242
+ # callable, so no name to log; the exception is read from `details`
243
+ # directly since it's no longer the active exception by this point).
244
+ def _log_backoff_context(
245
+ details: ContextDetails,
246
+ logger: _AnyLogger,
247
+ log_level: int,
248
+ ) -> None:
249
+ logger.log(
250
+ log_level,
251
+ "Backing off retry_context(...) for %.1fs (%s)",
252
+ details["wait"],
253
+ details.get("exception"),
254
+ )
255
+
256
+
257
+ # Default giveup handler for retry_context/aretry_context.
258
+ def _log_giveup_context(
259
+ details: ContextDetails,
260
+ logger: logging.Logger | logging.LoggerAdapter[Any],
261
+ log_level: int,
262
+ ) -> None:
263
+ logger.log(
264
+ log_level,
265
+ "Giving up retry_context(...) after %d tries (%s)",
266
+ details["tries"],
267
+ details.get("exception"),
268
+ )