diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 688a2e7..43dc13c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/docs/advanced/custom-strategies.md b/docs/advanced/custom-strategies.md index 0deb2b9..b53267a 100644 --- a/docs/advanced/custom-strategies.md +++ b/docs/advanced/custom-strategies.md @@ -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: diff --git a/docs/examples.md b/docs/examples.md index db7283a..648b6d8 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -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, ) @@ -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"], ) @@ -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( diff --git a/docs/faq.md b/docs/faq.md index 48ebf43..c2d23a0 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -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( diff --git a/docs/user-guide/async.md b/docs/user-guide/async.md index b3e62a7..4cb1c31 100644 --- a/docs/user-guide/async.md +++ b/docs/user-guide/async.md @@ -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"], ) diff --git a/docs/user-guide/decorators.md b/docs/user-guide/decorators.md index d4938bd..3c1535e 100644 --- a/docs/user-guide/decorators.md +++ b/docs/user-guide/decorators.md @@ -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 diff --git a/docs/user-guide/event-handlers.md b/docs/user-guide/event-handlers.md index 46d46b6..0b25d21 100644 --- a/docs/user-guide/event-handlers.md +++ b/docs/user-guide/event-handlers.md @@ -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, ) @@ -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"], ) diff --git a/pyproject.toml b/pyproject.toml index 1d75e9e..424c9dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] @@ -181,6 +181,7 @@ extend-include = ["docs/**/*.md"] line-length = 88 preview = true required-version = ">=0.15" +target-version = "py38" [tool.ruff.format] docstring-code-format = true @@ -188,11 +189,25 @@ 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` @@ -204,6 +219,10 @@ ignore = [ "**/doccmd_*.py" = [ "F811", # redefinition of unused "F821", # undefined name + "T201", # print +] +"tests/**.py" = [ + "S101", # assert ] [tool.coverage.report] diff --git a/tests/test_backoff.py b/tests/test_backoff.py index 90b6c4f..e2030cd 100644 --- a/tests/test_backoff.py +++ b/tests/test_backoff.py @@ -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) @@ -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 @@ -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 @@ -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 @@ -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() @@ -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 diff --git a/tests/test_backoff_async.py b/tests/test_backoff_async.py index f2aed99..77504b3 100644 --- a/tests/test_backoff_async.py +++ b/tests/test_backoff_async.py @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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() diff --git a/tests/test_package.py b/tests/test_package.py index c5a5ad0..41c1fbc 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -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)