From 00b0100de0166b1efed8e41567cecff6b7ad1501 Mon Sep 17 00:00:00 2001 From: Austin Varga Date: Mon, 6 Apr 2026 19:57:19 -0600 Subject: [PATCH] fix: replace self-chaining exception raise with 'from None' in retry loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #132 — raising 'last_exception from last_exception' created a circular __cause__ reference on the exception object. Using 'from None' cleanly suppresses the implicit context at the final retry attempt. --- dotflow/core/action.py | 2 +- tests/core/test_action.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/dotflow/core/action.py b/dotflow/core/action.py index bddf1624..f3b45e70 100644 --- a/dotflow/core/action.py +++ b/dotflow/core/action.py @@ -166,7 +166,7 @@ def _run_action(self, *args, task=None, **kwargs): raise ExecutionWithClassError() from None if attempt == self.retry: - raise last_exception from last_exception + raise last_exception from None if task is not None: task.retry_count += 1 diff --git a/tests/core/test_action.py b/tests/core/test_action.py index 81c2dde7..84244c50 100644 --- a/tests/core/test_action.py +++ b/tests/core/test_action.py @@ -79,6 +79,21 @@ def flaky_step(): self.assertEqual(len(statuses), 1) self.assertEqual(statuses[0], TypeStatus.RETRY) + def test_retry_exception_does_not_chain_to_itself(self): + def always_fail(): + raise ValueError("fail") + + inside = Action(always_fail, retry=2, retry_delay=0) + + try: + inside() + except ValueError as error: + self.assertIsNot( + error.__cause__, + error, + "Exception must not be its own __cause__ (circular chain)", + ) + def test_backoff_does_not_mutate_retry_delay(self): def always_fail(): raise RuntimeError("fail")