Skip to content

Uu - #1

Open
Rwadxvv wants to merge 217 commits into
BATS:masterfrom
jd:main
Open

Uu#1
Rwadxvv wants to merge 217 commits into
BATS:masterfrom
jd:main

Conversation

@Rwadxvv

@Rwadxvv Rwadxvv commented Nov 15, 2022

Copy link
Copy Markdown

Y

asqui and others added 30 commits October 26, 2020 09:06
* start_time == outcome_timestamp is possible
* logging.Formatter uses explicitly hard-coded `\n` newlines
  (as opposed to using the platform-specific os.linesep)
...rather than sometimes allowing a default return of `None`

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Slava Skvortsov <kondor1995@mail.ru>
* Fix iscoroutinefunction check (inspect & asyncio)

* Add real async wrap & preserve retry attributes

* Adjust to pep8

* Add test to ensure retryable_coroutine attributes

* Fix `blank line contains whitespace` pep8 error
* chore: add missing noqa statements

* docs: fix autoinstanceattribute
chore: add support for Python 3.9
* Copy whole internal state when retry_with (#233)

Both `retry_error_cls` and `retry_error_callback` were missing from
the copy, resulting in a copy that presents a different behavior
than the original function.

* Apply review feedback

- define `_first_set` only once
- get away with unittest
- use pytest.raises
* Warn when calling retry() on retry_base (#269)

* Wrap warning message string to 79 columns (PEP 8)
This should make sure we don't forget to put a release note before merging a
PR.

Related to #284
ci(mergify): force release notes to be present
* Make logger more compatible

* Add release note

* Fix black formatting

* Ignore D402 error in flake8

* Update PR

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
* Add retry_except_exception_type

Fixes #256

* Apply suggestions from code review

Co-authored-by: Julien Danjou <julien@danjou.info>

* rename 'except' to 'if not'

* fix test

* rename again

Co-authored-by: Julien Danjou <julien@danjou.info>
* Removed all transitional requirements (including `six`)
* Most type annotations fixed.
* Python 3.10 tested.
* Most part of code is type annotated.

Co-authored-by: Julien Danjou <julien@danjou.info>

Co-authored-by: Julien Danjou <julien@danjou.info>
- Use `black` for code formatting and validate using `black --check`. Code compatibility: py26-py39.
- Enforce maximal line length to 120 symbols
Use black instead of "flake8-black" on CI.
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
r266-tech and others added 30 commits March 23, 2026 17:21
Since license = "Apache-2.0" is already set as an SPDX expression,
the License :: OSI Approved :: Apache Software License classifier
is redundant and causes setuptools deprecation warnings.

Fixes #523

Co-authored-by: Community Contributor <anonymous@users.noreply.github.com>
* fix: reraise underlying exception when TryAgain wraps a cause

When reraise=True and TryAgain is raised from within an except block,
RetryError.reraise() now surfaces the underlying exception that caused
the retry once attempts are exhausted, rather than the opaque TryAgain
sentinel. A bare TryAgain raised without an active exception keeps
reraising TryAgain as before.

Fixes #544

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: ATOM00blue <219721791+ATOM00blue@users.noreply.github.com>
Co-authored-by: Julien Danjou <julien@danjou.info>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…_) (#643)

When Retrying(enabled=False) was used with the context-manager iterator
pattern (`for attempt in retrying` / `async for attempt in retrying`),
the `enabled` flag was silently ignored and the full retry machinery ran
(multiple attempts, stop/wait evaluated) instead of executing the body
exactly once and propagating any exception directly.

The `wraps()`-based path already handled `enabled=False` correctly, so
this patch brings the iterator API in line with it:
- BaseRetrying.__iter__: if not enabled, yield one AttemptManager and
  re-raise the stored exception if the attempt failed.
- AsyncRetrying.__aiter__ / __anext__: same semantics via a one-shot
  flag that stops after the first yielded AttemptManager.

Four regression tests are added (two sync, two async) covering the
failure and success cases for each protocol.
…exError (#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>
…exError (#647)

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>
…653)

* Keep statistics visible when @Retry is further wrapped

When a @retry-decorated function was wrapped by another decorator that
uses functools.wraps (e.g. a timing decorator), accessing
``func.statistics`` returned an empty ``{}``.

functools.wraps copies the inner wrapper's ``__dict__`` (including the
``statistics`` reference) into the outer wrapper. On each call the inner
wrapper rebound ``wrapped_f.statistics`` to a fresh dict, so the outer
wrapper kept pointing at the original, now-stale, empty dict.

Reuse the same statistics dict in place on every call (clearing it and
sharing it with the per-call copy) instead of rebinding the attribute.
The dict identity stays stable, so the stats remain reachable through
the outer wrapper's copied ``__dict__``. The same change is applied to
the async wrapper.

Fixes #519.

* Type __wrapped__ on _RetryDecorated so statistics stay type-safe

The runtime fix keeps func.statistics visible when a @retry-decorated
function is further wrapped by another functools.wraps-based decorator.
However mypy (strict) rejected the tests because the public typing did
not expose the attributes the tests rely on:

  * _RetryDecorated had no __wrapped__ member, so accessing
    my_call.__wrapped__.statistics failed with [attr-defined].
  * the sync test's outer decorator was annotated
    Callable[..., Any] -> Callable[..., Any], erasing the
    _RetryDecorated type so .statistics/.__wrapped__ were unreachable.

Declare __wrapped__ on the _RetryDecorated protocol (it is always set by
functools.wraps on the retry wrapper) and make the sync test's outer
decorator type-preserving via a TypeVar, mirroring the async test. mypy,
ruff, pytest and the docs build are all green.
Once an exponential wait exceeds its configured maximum, calculate the
threshold logarithmically and return the cap without constructing an ever
larger integer power. Add a regression test that rejects power evaluation
past the cap.

Fixes #526
retry_if_exception_message validated its message/match arguments with
truthiness checks (if message and match, if not message and not match).
An empty string is a valid message target — it matches exceptions whose
str() is empty, e.g. a bare RuntimeError() — but 'not message' is True
for '', so retry_if_exception_message(message='') wrongly raised
TypeError('missing 1 required argument'). _check had the same problem:
'if self.message:' skipped the empty-message branch.

Replace the truthiness guards with 'is None' checks so an empty message
is accepted and matched. This makes every message matchable, not every
message except the empty one.
retry_if_exception_cause_type walked __cause__ until None. Legal cycles
such as ``raise e from e`` (or mutual a<->b causes) never terminate, so
the predicate spins at 100% CPU and stop conditions never run.

Track seen exception ids while walking, matching stdlib traceback's
cycle-safe approach. Regression covers self-cause and two-node cycles.

Fixes #658.
An unpinned ruff means every new ruff release can break CI with newly
stabilized rules. Pin it to the current version so upgrades are explicit
and deliberate.

Upgrading to 0.16.1 stabilizes RUF036 (None not at the end of a type
union); fix the 14 violations in the same commit via ruff's auto-fix so
the pinned version lints clean. The changes are pure annotation
reordering with no semantic effect.


Change-Id: I252d1eff6bc8704288e2a9b0855d5a0c8851649d

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Same rationale as pinning ruff: new mypy releases can introduce new
errors and break CI without any code change. Pin to the current version
so upgrades are explicit. mypy passes clean at 2.3.0.


Change-Id: I4a514903b8798fcee59236cf97b292ddd4f93f25

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
typeguard existed for exactly one test: TestRetryTyping asserted at runtime
that @Retry preserves the decorated function's type. That check was weaker
than it looked. check_type() cannot inspect a function object's signature at
runtime, so 'check_type(with_raw, Callable[[int], str])' only ever confirmed
the object was callable -- it would have passed just as happily on a wrapper
that took different arguments or returned something else.

The real guarantee comes from the WrappedFn TypeVar in the overloads, which
is a static property, so check it statically. mypy already runs strict over
tests/, and the annotations here catch strictly more than typeguard did:
the positive cases pin the signature structurally, and the negative case
would fail if @Retry ever decayed to Any, because warn_unused_ignores turns
the then-dead ignore into an error.

Also fixes a copy-paste bug: with_constructor_result was assigned from
with_raw(1), so the @Retry(...) path's result was never checked at all.

Drops the typeguard dependency, which had already cost a round of test
breakage on its 3.x release.


Change-Id: Iba6bbce53302c81bb7a84c6a9925b6f234d22d37

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Dependabot was configured for github-actions only, so nothing watched the
Python dependencies. That was tolerable while ruff and mypy floated, but
now that they are pinned it is a trap: the pins would never move and the
tooling would silently rot.

Add the uv ecosystem, grouped into a single monthly PR to match the
github-actions entry. uv.lock is gitignored here; Dependabot updates the
pyproject.toml constraints and skips lockfile regeneration when no
lockfile is committed.


Change-Id: I10bfd34f28d44198689e5871877d886cea635552

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Bumps the github-actions group with 1 update in the / directory: [actions/checkout](https://github.com/actions/checkout).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Improve inline type annotations

Port the accepted annotation-only completeness improvements from Tenacity 8.1.0 while adapting them to current main.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Preserve existing Any annotations

Keep the existing public annotations for RetryCallState arguments and results rather than narrowing them to object-based container types.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Remove redundant inline annotations

Keep only constructor return annotations and the non-trivial regex field union; remove declarations already evident from literals, conversions, typed parameters, varargs, and singleton constructors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…668)

_RetryDecorated had no __get__, so mypy and pyright treated
instance.method the same as the unbound Class.method: an ordinary call
like instance.method(value=1) demanded an explicit self argument and
the return type resolved to Unknown/Any. Add __get__ overloads so
attribute access through an instance returns the bound form with the
correct return type. The runtime object is a real function from
functools.wraps and already binds correctly, so this is purely a
static analysis fix.
…676)

Tornado ships a `py.typed` marker, so its types resolve without help.
The override was silently downgrading every tornado symbol to `Any` if
the package ever went missing, instead of failing loudly.

`mypy` still reports no issues with the override removed.

Change-Id: I1939167c4dd59eb99ca1d71a18307af6db0f5e2d
`on.pull_request.branches` filters on the *base* branch, not the head. With
it pinned to `main`, only a pull request merging directly into `main` ran
CI -- every stacked pull request, whose base is the branch below it in the
stack, got no test, lint or mypy run at all and showed nothing but the
Mergify checks.

Drop the filter so CI runs for every pull request regardless of base.

Change-Id: I1c211a106a57b9f4986bec4b76c0680c1a0f1b61
)

* ci: run on pull requests targeting any base branch

`on.pull_request.branches` filters on the *base* branch, not the head. With
it pinned to `main`, only a pull request merging directly into `main` ran
CI -- every stacked pull request, whose base is the branch below it in the
stack, got no test, lint or mypy run at all and showed nothing but the
Mergify checks.

Drop the filter so CI runs for every pull request regardless of base.

Change-Id: I1c211a106a57b9f4986bec4b76c0680c1a0f1b61

* fix: annotate wait_base.__radd__ as taking the int seed from sum()

`__radd__` was annotated `other: wait_base`, which is the one type it can
never receive: `wait_base.__add__` always succeeds, so Python never falls
back to the reflected operator for two wait strategies. The only caller is
`sum()`, which seeds its accumulator with the int 0 — hence the
`type: ignore[comparison-overlap]` on `other == 0`, and the dead
`return self` branch behind it that `--warn-unreachable` flags.

Annotate the parameter as `int` and return `NotImplemented` for a non-zero
left operand, so `5 + wait_fixed(1)` raises `TypeError` at the addition
rather than building a `wait_combine` that explodes later when called.

With the signature corrected, mypy infers `sum([...])` over wait strategies
on its own, so four `type: ignore[list-item]` comments in
`test_wait_arbitrary_sum` are no longer needed.

Change-Id: Iad1bcef4412d6afc1a6f1335970cfbfcc4a34fcb
* ci: run on pull requests targeting any base branch

`on.pull_request.branches` filters on the *base* branch, not the head. With
it pinned to `main`, only a pull request merging directly into `main` ran
CI -- every stacked pull request, whose base is the branch below it in the
stack, got no test, lint or mypy run at all and showed nothing but the
Mergify checks.

Drop the filter so CI runs for every pull request regardless of base.

Change-Id: I1c211a106a57b9f4986bec4b76c0680c1a0f1b61

* fix: annotate wait_base.__radd__ as taking the int seed from sum()

`__radd__` was annotated `other: wait_base`, which is the one type it can
never receive: `wait_base.__add__` always succeeds, so Python never falls
back to the reflected operator for two wait strategies. The only caller is
`sum()`, which seeds its accumulator with the int 0 — hence the
`type: ignore[comparison-overlap]` on `other == 0`, and the dead
`return self` branch behind it that `--warn-unreachable` flags.

Annotate the parameter as `int` and return `NotImplemented` for a non-zero
left operand, so `5 + wait_fixed(1)` raises `TypeError` at the addition
rather than building a `wait_combine` that explodes later when called.

With the signature corrected, mypy infers `sum([...])` over wait strategies
on its own, so four `type: ignore[list-item]` comments in
`test_wait_arbitrary_sum` are no longer needed.

Change-Id: Iad1bcef4412d6afc1a6f1335970cfbfcc4a34fcb

* chore(mypy): enable the strictness options `strict` leaves off

`strict = true` is a curated subset, not every check mypy has. These five
error codes and three flags are all off under `strict` and all report zero
errors on the current tree, so turning them on costs nothing today and
catches regressions from here on:

- `warn_unreachable` -- dead branches, which are usually a wrong annotation
  rather than dead code (see the `wait_base.__radd__` fix below this commit)
- `disallow_any_unimported` -- silent `Any` leaking in from untyped deps
- `extra_checks` -- unsafely overlapping operator signatures, among others
- `ignore-without-code` -- keeps `type: ignore` comments specific
- `redundant-expr`, `truthy-iterable`, `unused-awaitable`, `exhaustive-match`

Deliberately left off: `disallow_any_expr` (655 errors -- unusable for a
decorator library) and `disallow_any_decorated` (63 errors, all inherent to
`@retry` wrapping untyped test helpers).

Change-Id: I9eee43a749050d2bcf0aa0abbedf8725301eba18
…679)

`truthy-bool` reports objects tested for truthiness that implement neither
`__bool__` nor `__len__`, and so can only ever be true. Three sites:

`BaseRetrying._run_wait` and `AsyncRetrying._run_wait` both guarded the
wait call with `if self.wait:`. `wait` is typed `WaitBaseT` and defaults to
a `wait_none()` instance, so it is never falsy and the `sleep = 0.0` branch
has been dead since 17aefd9 -- a leftover from when the surrounding code
still used `if self.after is not None:` style guards. Call `self.wait`
unconditionally.

`if tornado:` guarded the optional import in two places. mypy only ever
sees the `try` branch, so it resolves the name to the module and reads the
test as always-true. Compute `_HAS_TORNADO` once and branch on that
instead; this also keeps `tornado.gen` fully typed, which annotating the
name as `ModuleType | None` would have thrown away.

Change-Id: Icb9981f6797707e070dc2423015f2fbf6c94e4c4
* refactor: drop always-true truthiness checks and enable truthy-bool

`truthy-bool` reports objects tested for truthiness that implement neither
`__bool__` nor `__len__`, and so can only ever be true. Three sites:

`BaseRetrying._run_wait` and `AsyncRetrying._run_wait` both guarded the
wait call with `if self.wait:`. `wait` is typed `WaitBaseT` and defaults to
a `wait_none()` instance, so it is never falsy and the `sleep = 0.0` branch
has been dead since 17aefd9 -- a leftover from when the surrounding code
still used `if self.after is not None:` style guards. Call `self.wait`
unconditionally.

`if tornado:` guarded the optional import in two places. mypy only ever
sees the `try` branch, so it resolves the name to the module and reads the
test as always-true. Compute `_HAS_TORNADO` once and branch on that
instead; this also keeps `tornado.gen` fully typed, which annotating the
name as `ModuleType | None` would have thrown away.

Change-Id: Icb9981f6797707e070dc2423015f2fbf6c94e4c4

* refactor: declare BaseAction's REPR_FIELDS and NAME as ClassVar

`mutable-override` rejects narrowing a mutable attribute in a subclass:
`RetryAction.REPR_FIELDS = ("sleep",)` inferred `tuple[str]` against the
base's `Sequence[str]`, and `NAME = "retry"` inferred `str` against
`str | None`. Both are unsound in general -- code holding a `BaseAction`
could assign a longer sequence or `None` through the base type.

`BaseAction`'s docstring already calls these class variables, so mark them
`ClassVar` and repeat the base annotation on the override. This documents
the extension point for subclasses outside tenacity too, which hit the
same error when they type check strictly.

Change-Id: I012caeaad2f93327c69467776a53e87c573b6403
Two more error codes mypy leaves off under `strict`, both with real hits
in the test suite.

`possibly-undefined`: `test_retry_state` bound `retry_state` only inside
an `except ExtractCallState` block, then used it unconditionally. Had the
retry stopped raising, the test would have failed with `NameError` instead
of a useful assertion. Use `assertRaises` as a context manager, which both
asserts the exception is raised and binds the state unconditionally.

`deprecated`: `asyncio.iscoroutinefunction` is deprecated since 3.14 and
removed in 3.16, and was emitting a `DeprecationWarning` on every test run.
The line right below it already asserts the same property via
`inspect.iscoroutinefunction`, which is the documented replacement, so drop
the deprecated call rather than pin the suite to an API that is going away.

Change-Id: I135cf5364f3e471d954c878f2599be7441104971
* test: fix possibly-undefined and deprecated call sites

Two more error codes mypy leaves off under `strict`, both with real hits
in the test suite.

`possibly-undefined`: `test_retry_state` bound `retry_state` only inside
an `except ExtractCallState` block, then used it unconditionally. Had the
retry stopped raising, the test would have failed with `NameError` instead
of a useful assertion. Use `assertRaises` as a context manager, which both
asserts the exception is raised and binds the state unconditionally.

`deprecated`: `asyncio.iscoroutinefunction` is deprecated since 3.14 and
removed in 3.16, and was emitting a `DeprecationWarning` on every test run.
The line right below it already asserts the same property via
`inspect.iscoroutinefunction`, which is the documented replacement, so drop
the deprecated call rather than pin the suite to an API that is going away.

Change-Id: I135cf5364f3e471d954c878f2599be7441104971

* refactor: mark overridden methods with @OverRide

`explicit-override` is off under `strict`, and 63 methods across the
library and tests were overriding a base method without saying so. That
makes a whole class of change silently lossy: rename or drop a method on
`retry_base`/`wait_base`/`stop_base`/`BaseRetrying` and every subclass keeps
its now-orphaned implementation, still importable, never called again.

`typing.override` only exists from Python 3.12, and tenacity has no runtime
dependencies, so `_utils` imports it from `typing_extensions` under
TYPE_CHECKING (as the module already does for `Self`) and falls back to a
small PEP 698 shim at runtime on 3.10/3.11. Type checkers only ever see the
`typing_extensions` name, so the check works regardless of the interpreter
mypy runs under.

That last point exposed a second problem worth fixing here: mypy's
`python_version` defaults to whatever interpreter runs it, so CI on 3.14 was
never verifying that the code is valid on the 3.10 we claim to support. Pin
`python_version = "3.10"` to match `requires-python`. It caught one real
case immediately -- `RetryCallState.__getstate__` is only an override from
3.11 on, where `object.__getstate__` was introduced, so it must not carry
the decorator.

Verified `mypy` reports the same result under both a 3.10 and a 3.14
interpreter, and the runtime shim sets `__override__` correctly on 3.10.

Change-Id: Ide71dd204215e82012f6a9b1afc725dd7c5db8ee
)

Three runtime regressions from the mypy-strictness stack (#679, #677), all
A/B verified against a2af454. None were caught by the test suite, and all
three only fire once a retry actually happens, so happy-path callers see
nothing until production.

1. `Retrying(wait=None)`, `wait=0` and `wait=sum([])` now raise
   `TypeError: 'NoneType' object is not callable` from inside `iter()`.
   #679 removed the `if self.wait:` guard on the grounds that `truthy-bool`
   proved it always true -- but that only holds for the *declared* type.
   Untyped callers pass `None`/`0` to mean "no wait", and `sum([])` over an
   empty strategy list returns the int 0. The `TypeError` is raised outside
   the attempt's try/except, so it escapes uncaught and destroys the caller's
   real exception. Restore the guard with a local `truthy-bool` suppression
   explaining why the "impossible" branch is reachable. `before`, `after`,
   `before_sleep` and `retry_error_callback` all kept their `is not None`
   guards; `wait` was the only one dropped.

2. `plain_callable + wait_strategy` now raises `TypeError`. `WaitBaseT`
   admits plain callables, and a function has no `__add__`, so this went
   through `wait_base.__radd__` -- which #677 narrowed to `int`. Widen it to
   `WaitBaseT | int` and build the `wait_combine` again. `5 + strategy` is
   still rejected, now via `NotImplemented` rather than a combination that
   fails later. The parameter stays `int` rather than `Literal[0]` because
   typeshed's `sum()` protocol requires `__radd__(x: int)`; narrowing it
   would make every `sum()` over strategies need a `type: ignore`.

3. `wait_combine.__call__` passed the state as `retry_state=`, which crashes
   on any `WaitBaseT` callable whose parameter has another name. Pass it
   positionally, as `BaseRetrying._run_wait` already does.

Adds regression tests for all three plus the async path -- the original
change shipped with no test covering any of them.

Change-Id: Ia40c32a22cdac09cbfba4280fb0a7f0c2cb7b7e0
…696)

#679 replaced `if tornado:` with a `_HAS_TORNADO` bool computed once at
import, to stop `truthy-bool` flagging the module object as always true.
But only one of the two call sites was converted: the `retry()` decorator
still dereferences the live `tornado` global right after testing the
snapshot. The two desync as soon as anything reassigns the global.

A/B verified against a2af454: `tenacity.tornado = None` followed by any
`@tenacity.retry` decoration returned normally before, and now raises
`AttributeError: 'NoneType' object has no attribute 'gen'`. Nulling the
module global is the standard way a downstream suite exercises the
non-tornado path without uninstalling tornado.

Replace the constant with a `_has_tornado()` function so both call sites
read the live global and cannot drift apart. As a bonus this needs no
`redundant-expr` suppression: the check reads as a plain return rather than
the left operand of an `and`.

Change-Id: I6f0fbb9cad3d4e46599b0cae1b5b60378742fd1f
* fix: respect enabled=False when calling the controller directly

`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 #643): with `enabled=False` the
function is executed exactly once and any exception propagates
unchanged.

* fix(types): correct type-ignore codes in asyncio early-return path
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.