From 2ad2ef3ba15ff48350cb3f58f8c47aa933562c01 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 19 Feb 2026 10:45:33 +0100 Subject: [PATCH] fix: guard against None seconds_since_start in after_log `RetryCallState.seconds_since_start` returns None when `outcome_timestamp` is not set. The `%` format operator in `after_log` would crash with a TypeError in this case. Fall back to "?" when the value is None. Co-Authored-By: Claude Opus 4.6 Change-Id: Ib18f8aced63c084204b2d4719579fae4b8e557f5 --- tenacity/after.py | 3 ++- tests/test_after.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tenacity/after.py b/tenacity/after.py index a4b89224..707f7434 100644 --- a/tenacity/after.py +++ b/tenacity/after.py @@ -39,10 +39,11 @@ def log_it(retry_state: "RetryCallState") -> None: fn_name = "" else: fn_name = _utils.get_callback_name(retry_state.fn) + secs = retry_state.seconds_since_start logger.log( log_level, f"Finished call to '{fn_name}' " - f"after {sec_format % retry_state.seconds_since_start}(s), " + f"after {sec_format % secs if secs is not None else '?'}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) diff --git a/tests/test_after.py b/tests/test_after.py index 25d8a7c0..71b93b50 100644 --- a/tests/test_after.py +++ b/tests/test_after.py @@ -50,6 +50,29 @@ def test_01_default(self) -> None: f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) + def test_02_none_seconds_since_start(self) -> None: + """Test log formatting when seconds_since_start is None.""" + log = unittest.mock.MagicMock(spec="logging.Logger.log") + logger = unittest.mock.MagicMock(spec="logging.Logger", log=log) + + retry_state = test_tenacity.make_retry_state(self.previous_attempt_number, 0.1) + retry_state.outcome_timestamp = None + assert retry_state.seconds_since_start is None + + fun = after_log(logger=logger, log_level=self.log_level) + fun(retry_state) + fn_name = ( + "" + if retry_state.fn is None + else _utils.get_callback_name(retry_state.fn) + ) + log.assert_called_once_with( + self.log_level, + f"Finished call to '{fn_name}' " + f"after ?(s), " + f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", + ) + def test_02_custom_sec_format(self) -> None: """Test log formatting with custom int format..""" log = unittest.mock.MagicMock(spec="logging.Logger.log")