Respect enabled=False on the direct-call path - #645
Conversation
`Retrying.__call__` and `AsyncRetrying.__call__` ignored the `enabled` flag: calling a controller created with `enabled=False` still ran the full retry loop, so a failing function was retried and the original exception was wrapped in a `RetryError`. This makes the direct-call protocol consistent with the decorator (`@retry(enabled=False)`) and the iterator protocols (`__iter__`/`__aiter__`, fixed in jd#643): with `enabled=False` the function is executed exactly once and any exception propagates unchanged.
731b7c6 to
6b6acfb
Compare
jd
left a comment
There was a problem hiding this comment.
Approving — I verified the premise and the fix empirically against current main, and the merged result passes the full poe all (lint, mypy, 183 tests, docs).
The bug is real. On main, Retrying(enabled=False, stop=stop_after_attempt(5))(fails) calls the function 5 times, fires before/after on every attempt, and raises RetryError. With this PR: 1 call, no hooks, original ValueError. Same for AsyncRetrying, including the sync-callable-passed-to-AsyncRetrying case.
It is consistent with the paths that already got this right. wrapped_f (the decorator) and __iter__/__aiter__ both short-circuit the same way, and all three leave statistics as {} and skip the callbacks when disabled — I checked that the new path matches rather than inventing a third behaviour. Hoisting is_async above the check is the right call since the early return needs it.
The type: ignore codes in the async early return are correct and still load-bearing under the repo's current config — warn_unused_ignores is on, and mypy is clean.
Two follow-ups, neither blocking:
TornadoRetrying.__call__still ignoresenabled=False. It is the one remaining direct-call path. Verified on this branch:TornadoRetrying(enabled=False, stop=stop_after_attempt(5))runs the function 5 times and raisesRetryError. Worth a separate PR (it needs a@gen.coroutine-shaped early return, so it is not a copy-paste of this one).- No release note. The repo's contributing checklist asks for one, though #643 landed without one too, so this is a nit rather than something I would hold the PR for.
Thanks for the clear reproduction in the description — it matched exactly what I measured.
Merge Queue Status
This pull request spent 16 seconds in the queue, including 2 seconds running CI. Required conditions to merge
|
Retrying(enabled=False)(fn)andAsyncRetrying(enabled=False)(fn)ignoreenabled=Falseon the direct-call path — they run the full retry loop and wrap the original exception inRetryError, instead of calling the function once and propagating its exception.PR #643 added the
enabled=Falseshort-circuit to the iterator protocols (__iter__/__aiter__/__anext__) but the direct-call protocol (__call__) was left behind, sinceiter()itself never consultsenabled. The fix adds the same short-circuit at the top of each__call__(awaiting coroutine callables for the async path).Verified both ways: old runs the function 5× and raises
RetryError(sync and async); fixed calls once and raises the originalValueError, matching the iterator/decorator paths. 154 existing tests green; +4 regression tests (which fail on the unfixed code) → 158 passed.