From d0b835f3c5b30b3446cb6e6f5865ecbc7f47622f Mon Sep 17 00:00:00 2001 From: Arun Soman Date: Sat, 29 Aug 2026 19:01:10 +0530 Subject: [PATCH] forge: repair tenacity/__init__.py (round 1, green, diff 2) --- .gitignore | 2 ++ tenacity/__init__.py | 2 ++ tests/test_forge_531.py | 64 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 tests/test_forge_531.py diff --git a/.gitignore b/.gitignore index 3c966caa..c157eefe 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ doc/_build tenacity/_version.py /.pytest_cache +.cie/ +.forge/ diff --git a/tenacity/__init__.py b/tenacity/__init__.py index e4660039..f11e65cd 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -457,6 +457,8 @@ def _begin_iter(self, retry_state: "RetryCallState") -> None: def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None: if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result): + if self.after is not None: + self._add_action_func(self.after) self._add_action_func(lambda rs: rs.outcome.result()) return diff --git a/tests/test_forge_531.py b/tests/test_forge_531.py new file mode 100644 index 00000000..10658705 --- /dev/null +++ b/tests/test_forge_531.py @@ -0,0 +1,64 @@ +# Regression test for forge issue #531 +# +# Bug: The `after` callback is not called on the final successful attempt. +# In BaseRetrying._post_retry_check_actions, when the function succeeds and +# no retry is needed (retry_run_result is False), the code takes an early +# return and never invokes the `after` callback. The `after` callback is only +# called when a retry is about to happen. This means users cannot use `after` +# to log a "retry_success" message when a function eventually succeeds after +# one or more failures. + +import pytest + +from tenacity import ( + RetryCallState, + Retrying, + stop_after_attempt, + wait_fixed, + retry_if_exception_type, +) + + +def test_after_callback_called_on_final_success_after_retry(): + """The `after` callback must be invoked on every attempt, including the + final successful one that follows a failed attempt.""" + + after_calls = [] + + def after_cb(retry_state: RetryCallState) -> None: + after_calls.append({ + "attempt": retry_state.attempt_number, + "failed": retry_state.outcome.failed if retry_state.outcome else None, + }) + + attempts = {"count": 0} + + def might_fail(): + attempts["count"] += 1 + if attempts["count"] == 1: + raise ValueError("fail first") + return "ok" + + r = Retrying( + wait=wait_fixed(0), + stop=stop_after_attempt(3), + retry=retry_if_exception_type(ValueError), + after=after_cb, + reraise=True, + ) + result = r(might_fail) + + assert result == "ok" + # The function was attempted twice: once failed, once succeeded. + assert attempts["count"] == 2 + # The `after` callback should have been called for BOTH attempts: + # - attempt 1 (failed, retry triggered) + # - attempt 2 (succeeded, no retry needed) + # Bug: currently `after` is only called when a retry is about to happen, + # so the successful attempt 2 is missing from after_calls. + assert len(after_calls) == 2, ( + f"Expected `after` to be called 2 times (once per attempt), " + f"but it was called {len(after_calls)} times: {after_calls}" + ) + # The last call should correspond to the successful attempt. + assert after_calls[-1]["failed"] is False \ No newline at end of file