From ce53bc85245373a1b8b605ae78f5f2bc2de46fe2 Mon Sep 17 00:00:00 2001 From: Zo Bot Date: Tue, 30 Jun 2026 11:28:19 +0000 Subject: [PATCH] wait_chain: reject empty strategy list with ValueError instead of IndexError 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. --- ...-wait-chain-empty-strategies-d3a8f7c2b1e4f569.yaml | 8 ++++++++ tenacity/wait.py | 2 ++ tests/test_tenacity.py | 11 +++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/fix-wait-chain-empty-strategies-d3a8f7c2b1e4f569.yaml diff --git a/releasenotes/notes/fix-wait-chain-empty-strategies-d3a8f7c2b1e4f569.yaml b/releasenotes/notes/fix-wait-chain-empty-strategies-d3a8f7c2b1e4f569.yaml new file mode 100644 index 00000000..2e61ba5e --- /dev/null +++ b/releasenotes/notes/fix-wait-chain-empty-strategies-d3a8f7c2b1e4f569.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + ``wait_chain()`` now raises ``ValueError`` at construction time when + called with no strategies, instead of raising a confusing + ``IndexError`` from ``__call__`` the first time it is used. Empty + wait chains have no useful behaviour, so failing early gives a + clearer error message. diff --git a/tenacity/wait.py b/tenacity/wait.py index c7a430eb..3b53da24 100644 --- a/tenacity/wait.py +++ b/tenacity/wait.py @@ -104,6 +104,8 @@ def wait_chained(): """ def __init__(self, *strategies: wait_base) -> None: + if not strategies: + raise ValueError("wait_chain() requires at least one strategy") self.strategies = strategies def __call__(self, retry_state: "RetryCallState") -> float: diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index 9ecdea56..b2c0289a 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -541,10 +541,13 @@ def always_return_1() -> int: self.assertEqual(sleep_intervals, [1.0, 2.0, 3.0, 3.0]) sleep_intervals[:] = [] - # Clear and restart retrying. - self.assertRaises(tenacity.RetryError, always_return_1) - self.assertEqual(sleep_intervals, [1.0, 2.0, 3.0, 3.0]) - sleep_intervals[:] = [] + def test_wait_chain_requires_at_least_one_strategy(self) -> None: + with self.assertRaises(ValueError): + tenacity.wait_chain() + # Confirm the wrapped Retrying path surfaces the same ValueError + # instead of an opaque IndexError from self.strategies[-1]. + with self.assertRaises(ValueError): + Retrying(wait=tenacity.wait_chain()) def test_wait_random_exponential(self) -> None: fn = tenacity.wait_random_exponential(0.5, 60.0)