retry operators: validate | and & operands on both sync and async retry_base#649
Open
HrachShah wants to merge 3 commits into
Open
retry operators: validate | and & operands on both sync and async retry_base#649HrachShah wants to merge 3 commits into
HrachShah wants to merge 3 commits into
Conversation
jd
previously requested changes
Jul 3, 2026
| steps: | ||
| - name: Checkout 🛎️ | ||
| uses: actions/checkout@v7.0.0 | ||
| uses: actions/checkout@v6.0.3 |
The sync retry_base operators | and & got a guard in jd#648 that rejects non-callable, non-retry_base operands with a clear TypeError at the construction site. The async_retry_base (and async retry_any.__ror__ / retry_all.__rand__ that override the inherited methods) define the same operator surface but did not call _validate_predicate, so the guard did not extend to the async path. Before this fix: True | retry_if_exception(...) # built silently ...later, inside the retry loop, awaited a bool and crashed with 'TypeError: object bool can t be used in await expression', far from the line that actually wrote the bad expression. After this fix the same misuse raises synchronously at the | / & site with a clear message naming the bad operand's type and repr, matching the sync behaviour. Adds four new tests in tests/test_asyncio.py: - test_async_retry_composition_rejects_non_callable: covers the four operator overloads (|, __ror__, &, __rand__) with bool/int/str/None. - test_async_retry_composition_accepts_valid_operands: regression guard for legitimate async | async, async | sync, async | callable compositions. - test_async_retry_any_ror_validates_leaf_operand: covers the override in retry_any that flattens nested retry_any groups; the leaf branch must still validate. - test_async_retry_all_rand_validates_leaf_operand: mirror of the above for retry_all.__rand__. 171 tests pass (was 167).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The
|and&operators onretry_baseandasync_retry_baseaccept either a retry strategy or a plain callable. A non-callable
non-retry_base value (e.g.
True | retry_always,retry & 42,None & async_retry) used to silently build aretry_anyorretry_allwhose__call__later raisedTypeError: '<type>' object is not callable(sync) orTypeError: object bool can't be used in 'await' expression(async) from inside the running retryloop, far from the original misuse. The operators now raise a clear
TypeErrornaming the bad operand at the construction site, socommon typos like
retry or TrueorTrue | async_retryfail fastwith an actionable message.
Where
tenacity/retry.py:retry_base._validate_predicate(staticmethod), called from
__and__,__rand__,__or__,__ror__.tenacity/asyncio/retry.py: same guard added toasync_retry_base.__and__,__rand__,__or__,__ror__,and to the leaf branches of
retry_any.__ror__andretry_all.__rand__(which are reached when Python's reflectedoperator dispatch goes through the subclass's override rather than
the base class's).
Tests
tests/test_tenacity.py::TestRetryConditions::test_retry_composition_rejects_non_callablecovering
True | retry,retry | False,retry & 42,"nope" | retry, andNone & retry.tests/test_asyncio.py::TestAsyncRetryComposition:test_async_retry_composition_rejects_non_callable(mirrors thesync test with async operators),
test_async_retry_composition_accepts_valid_operands(regression guard for valid compositions still building retry_any /
retry_all),
test_async_retry_any_ror_validates_leaf_operandandtest_async_retry_all_rand_validates_leaf_operand(cover thesubclass-operator leaf branches that don't go through
async_retry_base\.__ror__/__rand__).python3 -m pytest tests/passes 171 tests (was 167).ruff check tenacity/asyncio/retry.py tenacity/retry.pyclean.True | tenacity.retry_alwaysraisesTypeError: Retry predicates must be retry_base instances or callables, got bool: True;True | tenacity.asyncio.retry.retry_if_exception(lambda exc: True)raises the same; legitimate compositions
async_retry | sync_retry,async_retry | async_retry,async_retry | lambdastill build aretry_anywith theexpected operands.
Release notes
releasenotes/notes/reject-non-callable-retry-operands-7c4e9f2b1a3d8c5e.yaml(sync, added previously) and
releasenotes/notes/fix-async-retry-composition-validates-operand-8f3a2b1e6c4d9a72.yaml(async, this commit) per the project's reno convention.