From a879f623561cf95bd03429b1df13234ec4a1893a Mon Sep 17 00:00:00 2001 From: Burt Matthews <80060660+earfman@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:59:00 -0700 Subject: [PATCH 1/2] fix(retry): guard retry_if_exception_cause_type against cause cycles --- tenacity/retry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tenacity/retry.py b/tenacity/retry.py index e59f4c0a..5f5ea517 100644 --- a/tenacity/retry.py +++ b/tenacity/retry.py @@ -176,10 +176,12 @@ def __call__(self, retry_state: "RetryCallState") -> bool: raise RuntimeError("__call__ called before outcome was set") if retry_state.outcome.failed: + seen: set[int] = set() exc = retry_state.outcome.exception() - while exc is not None: + while exc is not None and id(exc) not in seen: if isinstance(exc.__cause__, self.exception_cause_types): return True + seen.add(id(exc)) exc = exc.__cause__ return False From 3adbf7eea4a8926bad9c3bb8bd75f107be28b28e Mon Sep 17 00:00:00 2001 From: Burt Matthews <80060660+earfman@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:00:34 -0700 Subject: [PATCH 2/2] test: cause-cycle regression for retry_if_exception_cause_type --- tests/test_tenacity.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index ba44b0c8..1885fc1a 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -1441,6 +1441,31 @@ def test_retry_if_exception_cause_type(self) -> None: except NameError: pass + def test_retry_if_exception_cause_type_with_cause_cycle(self) -> None: + # A cyclic __cause__ chain (e.g. ``raise e from e``) must not hang the + # cause scan; the predicate should give up once the chain repeats. + def _raise_self_caused() -> None: + try: + raise ValueError("inner") + except ValueError as e: + raise e from e + + def _raise_two_node_cycle() -> None: + first = KeyError("first") + second = OSError("second") + first.__cause__ = second + second.__cause__ = first + raise first + + for raiser in (_raise_self_caused, _raise_two_node_cycle): + r = Retrying( + retry=tenacity.retry_if_exception_cause_type(NameError), + stop=tenacity.stop_after_attempt(2), + reraise=True, + ) + with self.assertRaises((ValueError, KeyError)): + r(raiser) + def test_retry_preserves_argument_defaults(self) -> None: def function_with_defaults(a: int = 1) -> int: return a