Skip to content

async retry: validate | and & operands at the operator site#1

Draft
HrachShah wants to merge 5 commits into
mainfrom
fix/retry-predicate-rejects-non-callable
Draft

async retry: validate | and & operands at the operator site#1
HrachShah wants to merge 5 commits into
mainfrom
fix/retry-predicate-rejects-non-callable

Conversation

@HrachShah

@HrachShah HrachShah commented Jul 7, 2026

Copy link
Copy Markdown
Owner

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, then crashed inside the retry loop with TypeError: object bool can t be used in await expression, far from the offending line.

This change adds the same validation to the async operator methods so the error surfaces at the construction site as a clear TypeError naming the operand type, matching the sync behavior.

Summary by Sourcery

Validate retry operator operands for both sync and async paths and ensure wait_chain rejects empty strategy lists with clear errors.

Bug Fixes:

  • Add operand validation to sync retry_base operator overloads (|, &, and reflected forms) so non-callable, non-retry_base values fail fast with a TypeError at construction time.
  • Extend the same operand validation to async_retry_base and async retry_any/retry_all operator overloads to prevent late await/call errors deep in the retry loop.
  • Make wait_chain() raise a ValueError when constructed without strategies instead of failing later with an IndexError in call.

Documentation:

  • Add release notes documenting the new validation behavior for sync and async retry operators and the early ValueError raised by wait_chain() when called without strategies.

Tests:

  • Add synchronous tests covering retry_base operator composition with invalid and valid operands, asserting clear TypeError messages and preserved valid compositions.
  • Add asyncio tests covering async retry operator composition and retry_any/retry_all flattening behavior, ensuring invalid operands are rejected and valid combinations still work.

HrachShah and others added 5 commits July 1, 2026 08:52
…exError (jd#646)

Calling wait_chain() with no strategies stored an empty tuple in
self.strategies. The first call to __call__ then evaluated
min(max(1, 1), 0) == 0, so self.strategies[wait_func_no - 1]
turned into self.strategies[-1] and raised IndexError, which is
opaque for what is really a programmer error at construction time.

Validate in __init__ and raise ValueError('wait_chain() requires
at least one strategy') so the failure happens at the point where
the bad configuration is created, not later when the wait function
is first evaluated inside a running retry.

Co-authored-by: Zo Bot <github-automation@zo.computer>
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v6.0.3...v7.0.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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).
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d8dcc96-62d6-406d-b49e-aeef53e5b423

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/retry-predicate-rejects-non-callable

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds operand validation for sync and async retry composition operators so non-callable, non-retry_base operands fail fast with clear TypeErrors, tightens async retry_any/all flattening behavior, makes wait_chain() reject empty strategies with a ValueError, and extends tests and release notes to cover these behaviors.

Sequence diagram for async retry operator fast-fail validation

sequenceDiagram
    actor User
    participant bool_operand as bool
    participant async_predicate as async_retry_base
    participant retry_any

    User->>bool_operand: True | async_predicate
    bool_operand-->>async_predicate: __ror__(other)
    async_predicate->>async_predicate: _validate_predicate(other)
    alt invalid_operand
        async_predicate-->>User: TypeError
    else valid_operand
        async_predicate-->>retry_any: __ror__(other)
        retry_any-->>User: composed_predicate
    end
Loading

Flow diagram for wait_chain empty strategies validation

flowchart LR
    A[wait_chain] --> B[wait_chained.__init__ with strategies]
    B --> C{strategies empty?}
    C -->|yes| D[raise ValueError]
    C -->|no| E[store strategies]
    E --> F[wait_chained.__call__ with retry_state]
Loading

File-Level Changes

Change Details Files
Introduce shared operand validation for retry composition operators and apply it to sync retry_base.
  • Add a static _validate_predicate helper on retry_base that enforces operands are retry_base instances or callables and raises a descriptive TypeError otherwise.
  • Invoke _validate_predicate at the start of retry_base.and, rand, or, and ror before composing predicates, so invalid operands fail at construction time rather than inside the retry loop.
  • Extend sync retry composition tests to cover rejection of non-callable operands and document the new behavior.
tenacity/retry.py
tests/test_tenacity.py
releasenotes/notes/reject-non-callable-retry-operands-7c4e9f2b1a3d8c5e.yaml
Align async retry composition behavior with sync by validating operands for async_retry_base and async retry_any/retry_all.
  • Call _validate_predicate at the start of async_retry_base.and, rand, or, and ror so async compositions reject non-callable, non-retry_base operands at the operator site.
  • In retry_any.ror and retry_all.rand, validate the operand on the non-flattening (leaf) branch to prevent invalid raw values from bypassing async_retry_base validators when composing through nested groups.
  • Add asyncio tests that assert invalid operands raise clear TypeErrors and that valid async/sync/callable compositions still work, including tests for the retry_any.ror and retry_all.rand leaf paths.
  • Document the async fix in release notes with concrete failing examples and the new error behavior.
tenacity/asyncio/retry.py
tests/test_asyncio.py
releasenotes/notes/fix-async-retry-composition-validates-operand-8f3a2b1e6c4d9a72.yaml
Make wait_chain() and its underlying wait_chained type reject empty strategy lists with a clear error.
  • Add a constructor guard in wait_chained.init that raises ValueError when instantiated with no strategies, replacing a later IndexError from call.
  • Add tests ensuring both direct wait_chain() usage and use via Retrying(wait=wait_chain()) surface the same ValueError on empty input.
  • Capture the behavioral change in release notes as a clarity/usability fix.
tenacity/wait.py
tests/test_tenacity.py
releasenotes/notes/fix-wait-chain-empty-strategies-d3a8f7c2b1e4f569.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant