Skip to content

v4.2.3

Choose a tag to compare

@Llucs Llucs released this 10 Jul 20:47
· 44 commits to main since this release

Release v4.2.3 consolidates the working-tree progress since v4.1.1: [#19] fast-path contextvars, [#16] hedge crash, [#17] raise_on_giveup ignored in fast path, [#18] "stop triggered on success", [#15] composed wait generators losing kwargs, [#22] assert helpers ignoring their fn, [#20] RetryingCaller/AsyncRetryingCaller dropping most options, [#21] README private imports, [#23] stop_before_delay never stopping the loop + README claims, [#24] AGENTS.md Architecture section outdated.

All 567 tests pass; ruff / mypy clean; coverage 96.11%.


4.2.3 - 2026-07-10

  • Documentation: rewrite the AGENTS.md Architecture section to match the actual repository layout (#24). The previous text pointed AI agents and contributors to backon/_sync.py, backon/_async.py, and a non-existent backon/_retry.py for retry logic; those are now deprecation stubs / non-existent files. The new section documents the _retry/ subpackage (_api.py, _classes.py, _decide.py, _fast.py, _helpers.py, _inner.py, _loops.py) alongside _circuit_breaker.py, _conditions.py, _context.py, _hedging.py, _instrumentation.py, _rate_limiter.py, _state.py, _testing.py, _trio.py, and the py.typed PEP 561 marker, and explicitly calls out _sync.py / _async.py as DeprecationWarning stubs. Notes that tests/ uses feature-oriented naming rather than a one-test_-per-source mirror.

4.2.2 - 2026-07-10

  • Fix stop_before_delay(max_delay) actually stopping the retry loop (#23). _decide_outcome now writes the upcoming wait into state.outcome.wait before invoking the Stop predicate, so elapsed + wait >= max_delay evaluates against the real upcoming wait rather than 0. Verified with a wait-incrementing loop where stop_before_delay(0.3) stops at the 3rd attempt (sum of sleeps < 0.31s). The README description is updated to: "Stop if the upcoming wait would make elapsed + wait >= max_delay".
  • Documentation (#23): the README "Context manager" section now clarifies that Retrying.__exit__/__aexit__ (and the BreakerRetrying/HedgingRetrying twins) are a scoping device and do not re-raise the final exception — use r.statistics after the call. The Rate Limiter section now clarifies that RateLimitError is raised only by direct RateLimiter usage; inside a retry loop exceeding the limit just throttles by sleeping. The migration section adds a note that backon.__version__ is read from importlib.metadata (a pip install backon is required; running from a source checkout without installing returns "0.0.0").
  • The contextvars claim ("thread-safe and async-safe") is now backed by every retry configuration (the fast-path fix landed in #19 / 4.1.4 already covers it), and the operator-composition example is now backed by code (the kwarg-preservation fix landed in #15 / 4.1.5).
  • Add test_stop_before_delay_actually_stops_retry_loop and test_stop_before_delay_predicate_based_actually_stops (tests/test_edge_cases.py) covering the exception-based and predicate-based retry paths.

4.2.1 - 2026-07-10

  • Documentation: replace private-module imports in README examples with the public API (#21). The Circuit Breaker, Hedging, Metrics, Testing Utilities, and auto-detection snippets now use from backon import ... for symbols already in __all__. The _auto_detect_collector private helper was replaced with backon.get_metrics_collector(). The Trio section and migration table now explicitly document backon._trio as the deliberately private, optional-dependency surface (trio helpers are intentionally not re-exported).

4.2.0 - 2026-07-10

  • RetryingCaller and AsyncRetryingCaller now accept and forward the full set of retry options (#20). Previously they silently dropped giveup, predicate, condition, stop, the on_* handlers, retry_error_callback, raise_on_giveup, logger, before/after, and name. The Dynamic Backoff feature (giveup predicate / float return) and the Handlers API are now reachable through the callable caller surface, consistent with Retrying, retry(), and the decorators.
  • Add TestCallerForwardsFullOptions (12 tests) to tests/test_retrying_caller.py covering giveup, predicate-based retry, on_attempt / on_giveup handlers, condition / stop composition objects, retry_error_callback, raise_on_giveup, copy preservation, and the async twins.

4.1.5 - 2026-07-10

  • Fix assert_retried() / assert_not_retried() ignoring their fn argument (#22). Both helpers now wrap the user-supplied fn in a counting spy, drive it through on_exception, and assert the spy was invoked exactly expected_tries (resp. exactly 1) times. assert_retried failing when fn does not actually raise is now detectable instead of silently passing.
  • Add tests/test_assertions.py (7 tests) covering fn invocation, the non-raising-fn failure path, non-ValueError exceptions, and the raising-fn path of assert_not_retried.
  • Fix wait generators losing pre-configured kwargs when composed with +, wait_combine(), or wait_chain() (#15). _Wait now stores its original constructor args/kwargs and __call__ merges them with any caller-supplied kwargs, so pre-configured instances such as backon.expo(base=3) and backon.constant(interval=0.2) preserve their configuration through composition and re-instantiation inside the retry loop.
  • Add TestPreconfiguredKwargsPreserved (7 tests) to tests/test_wait_combine.py covering wait_combine, the + operator, wait_chain, decorator integration, and call-time kwarg override.

4.1.4 - 2026-07-10

  • Fix is_retrying() / get_attempt_number() returning None inside a retry when the fast path was active (#19). The fast paths _retry_fast_sync and _retry_fast_async now enter _retry_context_manager(state.tries) around the target() call, matching the non-fast path. The README's "contextvars — thread-safe and async-safe" guarantee now holds for every retry configuration, not only those that force the slow path.
  • Add TestFastPathRetryContext (5 tests) to tests/test_fast_path.py covering sync, async, functional API, and the outside-retry default.

4.1.3 - 2026-07-10

  • Fix raise_on_giveup=False being silently ignored in the fast path (#17): _retry_fast_sync and _retry_fast_async now thread the raise_on_giveup flag and return None on giveup when it is False, matching the non-fast path (_retry_loop_*).
  • Fix the fast path raising RuntimeError("stop triggered on success") when the stop predicate fires on the same iteration where the target call succeeds but the condition still asks for a retry (#18): the fast path now returns the successful ret in that case, matching _decide_outcome/_retry_loop_* behaviour.
  • Add tests/test_fast_path.py covering both regressions (sync + async, direct _retry_fast_* calls and via decorators).

4.1.2 - 2026-07-10

  • Fix hedge() raising TypeError: 'NoneType' object is not callable whenever the target function raised (issue #16). The hedge helpers now build a default RetryCondition via _make_default_condition(exception, giveup, predicate) before fanning out, mirroring _retry_sync/_retry_async. Hedge futures also honour the final exception by passing raise_on_giveup=True so that a fully-failed hedge surfaces the real error instead of None.
  • Add tests/test_hedging.py with sync/async/decorator/context-manager coverage for hedge(), on_hedge(), and HedgingRetrying (18 tests, 1 skipped pending #18).