From 220001e35912cf438a14e5d811f560b63caa17bb Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Wed, 5 Aug 2026 15:53:27 +0200 Subject: [PATCH 1/2] 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 --- pyproject.toml | 1 + tenacity/__init__.py | 16 ++++++++-------- tenacity/asyncio/__init__.py | 9 +++------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0a4209fe..cec48a4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,7 @@ enable_error_code = [ "exhaustive-match", "ignore-without-code", "redundant-expr", + "truthy-bool", "truthy-iterable", "unused-awaitable", ] diff --git a/tenacity/__init__.py b/tenacity/__init__.py index 21d7f507..4e1bbb2a 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -88,6 +88,11 @@ except ImportError: tornado = None # type: ignore[assignment] +# mypy resolves `tornado` to the module (it only ever sees the `try` branch), +# so testing the module object for truthiness reads as an always-true check. +# Keep the availability answer in a plain bool instead. +_HAS_TORNADO = tornado is not None + if t.TYPE_CHECKING: if sys.version_info >= (3, 11): from typing import Self @@ -403,12 +408,7 @@ def _run_retry(self, retry_state: "RetryCallState") -> None: self.iter_state.retry_run_result = self.retry(retry_state) def _run_wait(self, retry_state: "RetryCallState") -> None: - if self.wait: - sleep = self.wait(retry_state) - else: - sleep = 0.0 - - retry_state.upcoming_sleep = sleep + retry_state.upcoming_sleep = self.wait(retry_state) def _run_stop(self, retry_state: "RetryCallState") -> None: self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start @@ -770,7 +770,7 @@ def wrap(f: t.Callable[P, R]) -> _RetryDecorated[P, R]: ): r = AsyncRetrying(*dargs, **dkw) elif ( - tornado + _HAS_TORNADO and hasattr(tornado.gen, "is_coroutine_function") and tornado.gen.is_coroutine_function(f) ): @@ -785,7 +785,7 @@ def wrap(f: t.Callable[P, R]) -> _RetryDecorated[P, R]: from tenacity.asyncio import AsyncRetrying # noqa: E402 -if tornado: +if _HAS_TORNADO: from tenacity.tornadoweb import TornadoRetrying diff --git a/tenacity/asyncio/__init__.py b/tenacity/asyncio/__init__.py index 6291b02f..a91ca577 100644 --- a/tenacity/asyncio/__init__.py +++ b/tenacity/asyncio/__init__.py @@ -142,12 +142,9 @@ async def _run_retry(self, retry_state: "RetryCallState") -> None: # type: igno ) async def _run_wait(self, retry_state: "RetryCallState") -> None: # type: ignore[override] - if self.wait: - sleep = await _utils.wrap_to_async_func(self.wait)(retry_state) - else: - sleep = 0.0 - - retry_state.upcoming_sleep = sleep + retry_state.upcoming_sleep = await _utils.wrap_to_async_func(self.wait)( + retry_state + ) async def _run_stop(self, retry_state: "RetryCallState") -> None: # type: ignore[override] self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start From 4fc06649168f0a70b33f7dec98a997672c5e05e0 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Wed, 5 Aug 2026 15:54:43 +0200 Subject: [PATCH 2/2] 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 --- pyproject.toml | 1 + tenacity/__init__.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cec48a4e..518a86e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,6 +124,7 @@ extra_checks = true enable_error_code = [ "exhaustive-match", "ignore-without-code", + "mutable-override", "redundant-expr", "truthy-bool", "truthy-iterable", diff --git a/tenacity/__init__.py b/tenacity/__init__.py index 4e1bbb2a..e993396c 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -153,8 +153,8 @@ class BaseAction: - NAME: for identification in retry object methods and callbacks """ - REPR_FIELDS: t.Sequence[str] = () - NAME: str | None = None + REPR_FIELDS: t.ClassVar[t.Sequence[str]] = () + NAME: t.ClassVar[str | None] = None def __repr__(self) -> str: state_str = ", ".join( @@ -167,8 +167,8 @@ def __str__(self) -> str: class RetryAction(BaseAction): - REPR_FIELDS = ("sleep",) - NAME = "retry" + REPR_FIELDS: t.ClassVar[t.Sequence[str]] = ("sleep",) + NAME: t.ClassVar[str | None] = "retry" def __init__(self, sleep: t.SupportsFloat) -> None: self.sleep = float(sleep)