Skip to content

Commit ec7fd8b

Browse files
feat!: Rename _Attempt to Attempt and make it part of the public API (#210)
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
1 parent be05614 commit ec7fd8b

11 files changed

Lines changed: 71 additions & 34 deletions

File tree

‎.github/workflows/ci.yaml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ jobs:
8181
with:
8282
persist-credentials: false
8383
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
84+
with:
85+
python-version: ${{ matrix.python-version }}
8486
- name: Type checks
85-
env:
86-
TOXENV: typing-${{ matrix.python-version }}
87-
run: uvx --with tox-uv tox
87+
run: uvx --with tox-uv tox -e typing
8888
- name: Run tests
8989
env:
9090
TOXENV: ${{ matrix.python-version }}

‎backoff/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
https://github.com/python-backoff/backoff
1313
"""
1414

15+
from backoff._common import Attempt
1516
from backoff._decorator import aretry_context, on_exception, on_predicate, retry_context
1617
from backoff._jitter import full_jitter, random_jitter
1718
from backoff._wait_gen import constant, decay, expo, fibo, runtime
1819

1920
__all__ = [
21+
"Attempt",
2022
"aretry_context",
2123
"constant",
2224
"decay",

‎backoff/_async.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections.abc import Callable
77
from typing import TYPE_CHECKING, Any, TypeVar
88

9-
from backoff._common import _Attempt, _RetryState
9+
from backoff._common import Attempt, _RetryState
1010

1111
if TYPE_CHECKING:
1212
import sys
@@ -252,7 +252,7 @@ async def aretry_context(
252252
on_giveup: Iterable[_ContextHandler],
253253
raise_on_giveup: bool,
254254
wait_gen_kwargs: dict[str, Any],
255-
) -> AsyncGenerator[_Attempt, None]:
255+
) -> AsyncGenerator[Attempt, None]:
256256
giveup = _ensure_coroutine(giveup)
257257

258258
state = _RetryState(
@@ -263,7 +263,7 @@ async def aretry_context(
263263
)
264264
while True:
265265
state.start_attempt()
266-
attempt = _Attempt(exception)
266+
attempt = Attempt(exception)
267267
await _dispatch_handlers(
268268
handlers=on_try,
269269
tries=state.tries,

‎backoff/_common.py‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import logging
55
import time
66
import traceback
7-
from typing import TYPE_CHECKING, Any, TypeVar
7+
from dataclasses import dataclass, field
8+
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
89

910
if TYPE_CHECKING:
1011
import sys
@@ -26,6 +27,8 @@
2627
else:
2728
from typing_extensions import Self
2829

30+
ExceptionTypes: TypeAlias = type[Exception] | tuple[type[Exception], ...]
31+
2932

3033
# Use module-specific logger with a default null handler.
3134
_logger = logging.getLogger("backoff")
@@ -122,7 +125,8 @@ def next_wait(self, send_value: Any, jitter: _Jitterer | None) -> float:
122125
return _next_wait(self.wait, send_value, jitter, self.elapsed, self.max_time)
123126

124127

125-
class _Attempt:
128+
@dataclass(slots=True)
129+
class Attempt:
126130
"""A single attempt yielded by `retry_context`/`aretry_context`.
127131
128132
Used as `with attempt: ...`. Exceptions matching `exception_types` are
@@ -131,16 +135,10 @@ class _Attempt:
131135
exception at all, is left for the `with` block's normal exit behavior.
132136
"""
133137

134-
__slots__ = (
135-
"exception",
136-
"exception_types",
137-
)
138+
exception_types: ExceptionTypes
139+
exception: BaseException | None = field(init=False)
138140

139-
def __init__(
140-
self,
141-
exception_types: type[Exception] | tuple[type[Exception], ...],
142-
) -> None:
143-
self.exception_types = exception_types
141+
def __post_init__(self) -> None:
144142
self.exception: BaseException | None = None
145143

146144
def __enter__(self) -> Self:
@@ -153,7 +151,6 @@ def __exit__(
153151
tb: object,
154152
) -> bool:
155153
if exc_type is None:
156-
# self.outcome = _Success()
157154
return False
158155

159156
if not issubclass(exc_type, self.exception_types):

‎backoff/_decorator.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
if TYPE_CHECKING:
2121
from collections.abc import AsyncGenerator, Callable, Generator, Iterable
2222

23-
from backoff._common import _Attempt
23+
from backoff._common import Attempt
2424
from backoff._typing import (
2525
_AnyLoggerOrName,
2626
_CallableT,
@@ -301,7 +301,7 @@ def retry_context(
301301
backoff_log_level: int = logging.INFO,
302302
giveup_log_level: int = logging.ERROR,
303303
**wait_gen_kwargs: Any,
304-
) -> Generator[_Attempt, None, None]:
304+
) -> Generator[Attempt, None, None]:
305305
"""Returns a generator of retry attempts, for direct use with a `for` loop.
306306
307307
Unlike `on_exception`, this doesn't wrap a whole function; it lets a
@@ -400,7 +400,7 @@ def aretry_context(
400400
backoff_log_level: int = logging.INFO,
401401
giveup_log_level: int = logging.ERROR,
402402
**wait_gen_kwargs: Any,
403-
) -> AsyncGenerator[_Attempt, None]:
403+
) -> AsyncGenerator[Attempt, None]:
404404
"""Async counterpart to `retry_context`, for use with `async for`.
405405
406406
async for attempt in backoff.aretry_context(ValueError, backoff.expo):

‎backoff/_sync.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import time
55
from typing import TYPE_CHECKING, Any, TypeVar
66

7-
from backoff._common import _Attempt, _dispatch_handlers, _RetryState
7+
from backoff._common import Attempt, _dispatch_handlers, _RetryState
88

99
if TYPE_CHECKING:
1010
import sys
@@ -188,7 +188,7 @@ def retry_context(
188188
on_giveup: Iterable[_ContextHandler],
189189
raise_on_giveup: bool,
190190
wait_gen_kwargs: dict[str, Any],
191-
) -> Generator[_Attempt, None, None]:
191+
) -> Generator[Attempt, None, None]:
192192
state = _RetryState(
193193
wait_gen,
194194
wait_gen_kwargs,
@@ -197,7 +197,7 @@ def retry_context(
197197
)
198198
while True:
199199
state.start_attempt()
200-
attempt = _Attempt(exception)
200+
attempt = Attempt(exception)
201201
_dispatch_handlers(handlers=on_try, tries=state.tries, elapsed=state.elapsed)
202202
yield attempt
203203
elapsed = state.record_elapsed()

‎backoff/types.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from ._typing import Details
1+
from ._typing import ContextDetails, Details
22

33
__all__ = [
4+
"ContextDetails",
45
"Details",
56
]

‎docs/api/reference.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ Complete API documentation for the backoff module.
2626
show_root_heading: true
2727
show_source: true
2828

29+
::: backoff.Attempt
30+
options:
31+
show_root_heading: true
32+
show_source: true
33+
summary: true
34+
2935
## Wait Generators
3036

3137
::: backoff.expo

‎docs/user-guide/context-manager.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,36 @@ for attempt in backoff.retry_context():
1717
else:
1818
print(f"Got it (choice={choice})")
1919
```
20+
21+
`attempt` is an instance of `backoff.Attempt`, so a helper that
22+
accepts one (e.g. for logging) can be typed against it directly:
23+
24+
```python
25+
from backoff import Attempt
26+
27+
28+
def log_attempt(attempt: Attempt) -> None:
29+
print(f"exception so far: {attempt.exception}")
30+
31+
32+
for attempt in backoff.retry_context():
33+
with attempt:
34+
log_attempt(attempt)
35+
# ...
36+
```
37+
38+
Handlers passed as `on_try`/`on_success`/`on_backoff`/`on_giveup` receive a
39+
`dict` typed as `backoff.types.ContextDetails`:
40+
41+
```python
42+
from backoff.types import ContextDetails
43+
44+
45+
def log_backoff(details: ContextDetails) -> None:
46+
print(f"retrying in {details['wait']}s after {details.get('exception')}")
47+
48+
49+
for attempt in backoff.retry_context(on_backoff=log_backoff):
50+
with attempt:
51+
...
52+
```

‎pyproject.toml‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,20 +133,13 @@ commands = [
133133
]
134134

135135
[tool.tox.env.typing]
136-
base = [ "tool.tox.env_base.typing" ]
137-
base_python_file = [ ".python-version" ]
138-
description = "run type checking on the default Python version"
139-
140-
[tool.tox.env_base.typing]
141-
factors = [
142-
{ prefix = "3.", start = 10 },
143-
]
144136
description = "run type checking on Python {py_dot_ver}"
145137
dependency_groups = [ "typing" ]
146138
labels = [ "check" ]
147139
commands = [
148140
[
149141
"mypy",
142+
"--python-version={py_dot_ver}",
150143
"--show-error-codes",
151144
{ replace = "posargs", default = [
152145
"backoff",
@@ -155,6 +148,7 @@ commands = [
155148
],
156149
[
157150
"mypy",
151+
"--python-version={py_dot_ver}",
158152
"--strict",
159153
"--disallow-any-decorated",
160154
"--follow-imports=silent",
@@ -163,6 +157,7 @@ commands = [
163157
[
164158
"ty",
165159
"check",
160+
"--python-version={py_dot_ver}",
166161
{ replace = "if", condition = "env.GITHUB_ACTIONS == 'true'", then = [ "--output-format=github" ], else = [], extend = true },
167162
{ replace = "posargs", default = [
168163
"backoff",
@@ -172,6 +167,7 @@ commands = [
172167
[
173168
"pyrefly",
174169
"check",
170+
"--python-version={py_dot_ver}",
175171
{ replace = "if", condition = "env.GITHUB_ACTIONS == 'true'", then = [ "--output-format=github" ], else = [], extend = true },
176172
{ replace = "posargs", default = [
177173
"backoff",
@@ -182,6 +178,7 @@ commands = [
182178
"pyrefly",
183179
"coverage",
184180
"check",
181+
"--python-version={py_dot_ver}",
185182
{ replace = "if", condition = "env.GITHUB_ACTIONS == 'true'", then = [ "--output-format=github" ], else = [], extend = true },
186183
{ replace = "posargs", default = [
187184
"backoff",

0 commit comments

Comments
 (0)