Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# See https://pre-commit.ci/ for more information about pre-commit.ci
ci:
autofix_commit_msg: "style: pre-commit fixes"
autoupdate_schedule: quarterly
autoupdate_commit_msg: 'chore(deps): update pre-commit hooks'
skip:
- pip-compile

Expand Down
4 changes: 2 additions & 2 deletions docs/advanced/custom-strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,13 @@ def my_function():
### Time-of-Day Aware

```python
from datetime import datetime
from datetime import datetime, timezone


def business_hours_backoff():
"""Shorter waits during business hours"""
while True:
hour = datetime.now().hour
hour = datetime.now(tz=timezone.utc).hour
if 9 <= hour < 17:
yield 5 # 5 seconds during business hours
else:
Expand Down
17 changes: 12 additions & 5 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,12 @@ def wait_for_job(job_id):
### Wait for Resource Availability

```python
import operator


@backoff.on_predicate(
backoff.fibo,
lambda result: not result,
operator.not_, # Retry on falsey values
max_value=30,
max_time=300,
)
Expand Down Expand Up @@ -263,14 +266,18 @@ logger = logging.getLogger(__name__)

def log_retry(details):
logger.warning(
f"Backing off {details['wait']:.1f}s after {details['tries']} tries "
f"calling {details['target'].__name__}"
"Backing off %s:%.1fs after %d tries calling %s",
details["wait"],
details["tries"],
details["target"].__name__,
)


def log_giveup(details):
logger.error(
f"Giving up after {details['tries']} tries and {details['elapsed']:.1f}s"
"Giving up after %d tries and %.1fs",
details["tries"],
details["elapsed"],
)


Expand Down Expand Up @@ -445,7 +452,7 @@ class RetryExhaustedError(Exception):


def raise_custom_error(details):
raise RetryExhaustedError(f"Failed after {details['tries']} attempts")
raise RetryExhaustedError("Failed after %d attempts", details["tries"])


@backoff.on_exception(
Expand Down
2 changes: 1 addition & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ Use event handlers:

```python
def log_backoff(details):
logger.warning(f"Retry {details['tries']} after {details['elapsed']:.1f}s")
logger.warning("Retry %d after %.1fs", details["tries"], details["elapsed"])


@backoff.on_exception(
Expand Down
7 changes: 4 additions & 3 deletions docs/user-guide/async.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,10 @@ logger = logging.getLogger(__name__)

async def log_async_retry(details):
logger.warning(
f"Async retry {details['tries']}: "
f"wait={details['wait']:.1f}s, "
f"elapsed={details['elapsed']:.1f}s"
"Async retry %d: wait=%.1fs, elapsed=%.1fs",
details["tries"],
details["wait"],
details["elapsed"],
)


Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ The `on_predicate` decorator retries when a condition is true about the return v
)
def poll_for_result(job_id):
result = check_job(job_id)
return result if result else None
return result or None
```

### Parameters
Expand Down
37 changes: 24 additions & 13 deletions docs/user-guide/event-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,11 @@ def detailed_exception_log(details):
tb_str = "".join(traceback.format_tb(exc_tb))

logger.error(
f"Retry {details['tries']} due to {exc_type.__name__}: {exc_value}\\n"
f"Traceback:\\n{tb_str}"
"Retry %d due to %s: %s\\nTraceback:\\n%s",
details["tries"],
exc_type.__name__,
exc_value,
tb_str,
)


Expand Down Expand Up @@ -343,38 +346,46 @@ def my_function():

```python
import logging
from datetime import datetime
from datetime import datetime, timezone

logger = logging.getLogger(__name__)


def log_attempt(details):
logger.info(
f"[{datetime.now()}] Attempt {details['tries']} "
f"for {details['target'].__name__}"
"[%s] Attempt %d for %s",
datetime.now(tz=timezone.utc).isoformat(),
details["tries"],
details["target"].__name__,
)


def log_backoff(details):
logger.warning(
f"Backing off {details['wait']:.1f}s after {details['tries']} tries. "
f"Total elapsed: {details['elapsed']:.1f}s. "
f"Error: {details.get('exception', 'N/A')}"
"Backing off %.1fs after %d tries. Total elapsed: %.1fs. Error: %s",
details["wait"],
details["tries"],
details["elapsed"],
details.get("exception", "N/A"),
)


def log_giveup(details):
logger.error(
f"Gave up on {details['target'].__name__} after "
f"{details['tries']} tries and {details['elapsed']:.1f}s. "
f"Final error: {details.get('exception', 'N/A')}"
"Gave up on %s after %d tries and %.1fs. Final error: %s",
details["target"].__name__,
details["tries"],
details["elapsed"],
details.get("exception", "N/A"),
)


def log_success(details):
logger.info(
f"Success for {details['target'].__name__} after "
f"{details['tries']} tries in {details['elapsed']:.1f}s"
"Success for %s after %d tries in %.1fs",
details["target"].__name__,
details["tries"],
details["elapsed"],
)


Expand Down
35 changes: 27 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,15 @@ commands = [
description = "format code"
dependency_groups = [ "lint" ]
commands = [
[ "ruff", "check", "--fix", { replace = "posargs", default = [ "backoff", "tests" ], extend = true } ],
[ "ruff", "format", { replace = "posargs", default = [ "backoff", "tests" ], extend = true } ],
[ "ruff", "check", "--fix", { replace = "posargs", default = [ "backoff", "tests", "docs" ], extend = true } ],
[ "ruff", "format", { replace = "posargs", default = [ "backoff", "tests", "docs" ], extend = true } ],
]

[tool.tox.env.lint]
description = "lint code"
dependency_groups = [ "lint" ]
commands = [
[ "ruff", "check", { replace = "posargs", default = [ "backoff", "tests" ], extend = true } ],
[ "ruff", "check", { replace = "posargs", default = [ "backoff", "tests", "docs" ], extend = true } ],
]

[tool.tox.env.typing]
Expand Down Expand Up @@ -181,18 +181,33 @@ extend-include = ["docs/**/*.md"]
line-length = 88
preview = true
required-version = ">=0.15"
target-version = "py38"

[tool.ruff.format]
docstring-code-format = true
docstring-code-line-length = 20

[tool.ruff.lint]
extend-select = [
"RET", # flake8-return
"B", # flake8-bugbear
"I", # isort
"SIM", # flake8-simplify
"UP", # pyupgrade
"YTT", # flake8-2020
"ASYNC", # flake8-async
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"ISC", # flake8-implicit-str-concat
"ICN", # flake8-import-conventions
"LOG", # flake8-logging
"G", # flake8-logging-format
"PIE", # flake8-pie
"T20", # flake8-print
"PT", # flake8-pytest-style
"RET", # flake8-return
"SIM", # flake8-simplify
"TC", # flake8-type-checking
"I", # isort
"PERF", # Perflint
"UP", # pyupgrade
"FURB", # refurb
]
ignore = [
# converting to a `yield from` expression is not safe because this library sends values via `send`
Expand All @@ -204,6 +219,10 @@ ignore = [
"**/doccmd_*.py" = [
"F811", # redefinition of unused
"F821", # undefined name
"T201", # print
]
"tests/**.py" = [
"S101", # assert
]

[tool.coverage.report]
Expand Down
20 changes: 10 additions & 10 deletions tests/test_backoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ def exceptor(*args, **kwargs):
raise ValueError("catch me")

if raise_on_giveup:
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="catch me"):
exceptor(1, 2, 3, foo=1, bar=2)
else:
exceptor(1, 2, 3, foo=1, bar=2)
Expand Down Expand Up @@ -397,7 +397,7 @@ def on_baz(e):
def foo_bar_baz():
raise ValueError(vals.pop())

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="(baz|bar|foo)"):
foo_bar_baz()

assert not vals
Expand Down Expand Up @@ -646,9 +646,9 @@ def test_on_exception_callable_max_tries(monkeypatch):
@backoff.on_exception(backoff.constant, ValueError, max_tries=lambda: 3)
def exceptor():
log.append(True)
raise ValueError()
raise ValueError("aah")

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
exceptor()

assert len(log) == 3
Expand All @@ -665,12 +665,12 @@ def lookup_max_tries():

@backoff.on_exception(backoff.constant, ValueError, max_tries=lookup_max_tries)
def exceptor():
raise ValueError()
raise ValueError("aah")

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
exceptor()

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
exceptor()

assert len(lookups) == 2
Expand All @@ -691,7 +691,7 @@ def wait_gen(foo=None, bar=None):
def exceptor():
raise ValueError("aah")

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
exceptor()


Expand Down Expand Up @@ -876,10 +876,10 @@ def _on_exception_factory(
giveup_log_level=giveup_log_level,
)
def value_error():
raise ValueError
raise ValueError("aah")

def func():
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
value_error()

return func
Expand Down
18 changes: 9 additions & 9 deletions tests/test_backoff_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ async def exceptor(*args, **kwargs):
raise ValueError("catch me")

if raise_on_giveup:
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="catch me"):
await exceptor(1, 2, 3, foo=1, bar=2)
else:
await exceptor(1, 2, 3, foo=1, bar=2)
Expand Down Expand Up @@ -337,7 +337,7 @@ def on_baz(e):
async def foo_bar_baz():
raise ValueError(vals.pop())

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="(baz|bar|foo)"):
await foo_bar_baz()

assert not vals
Expand All @@ -356,7 +356,7 @@ async def on_baz(e):
async def foo_bar_baz():
raise ValueError(vals.pop())

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="(baz|bar|foo)"):
await foo_bar_baz()

assert not vals
Expand Down Expand Up @@ -637,9 +637,9 @@ def lookup_max_tries():
@backoff.on_exception(backoff.constant, ValueError, max_tries=lookup_max_tries)
async def exceptor():
log.append(True)
raise ValueError()
raise ValueError("aah")

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
await exceptor()

assert len(log) == 3
Expand All @@ -657,12 +657,12 @@ def lookup_max_tries():

@backoff.on_exception(backoff.constant, ValueError, max_tries=lookup_max_tries)
async def exceptor():
raise ValueError()
raise ValueError("aah")

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
await exceptor()

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
await exceptor()

assert len(lookups) == 2
Expand All @@ -684,7 +684,7 @@ def wait_gen(foo=None, bar=None):
async def exceptor():
raise ValueError("aah")

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="aah"):
await exceptor()


Expand Down
10 changes: 4 additions & 6 deletions tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,10 @@ def test_python_classifiers():
with open("pyproject.toml", "rb") as f:
data = tomllib.load(f)

versions = map(
lambda x: Version(x.split(" :: ")[-1]),
filter(
lambda x: x.startswith("Programming Language :: Python :: 3."),
data["project"]["classifiers"],
),
versions = (
Version(x.split(" :: ")[-1])
for x in data["project"]["classifiers"]
if x.startswith("Programming Language :: Python :: 3.")
)
requires_python = SpecifierSet(data["project"]["requires-python"])
assert all(v in requires_python for v in versions)
Loading