From e187f5499479a81c411bc704389169fabe696f76 Mon Sep 17 00:00:00 2001 From: Anton Petnitsky <168552591+Mukller@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:21:27 +0300 Subject: [PATCH 1/2] fix: show caller name in before_log when used as context manager (#511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Retrying is used as a context manager without an explicit name= parameter, retry_state.fn is None and get_fn_name() falls back to str(self.retry_object) which returns ''. Fix: in __iter__, capture sys._getframe(1) — the for-loop frame in user code — and store the qualified function name as retry_state._inferred_name. get_fn_name() checks this attribute before returning '', so before_log() now logs the actual enclosing function instead of ''. Users who explicitly pass name='my_fn' are unaffected. Python < 3.11 falls back from co_qualname to co_name gracefully. --- tenacity/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tenacity/__init__.py b/tenacity/__init__.py index 6b591464..24460a1d 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -491,6 +491,15 @@ def __iter__(self) -> t.Generator[AttemptManager, None, None]: self.begin() retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) + # When used as a context manager without an explicit name=, infer the + # caller's qualified name from the frame that called next() on this + # generator (i.e. the for-loop body in user code). + if self._name is None: + _frame = sys._getframe(1) + retry_state._inferred_name = ( + getattr(_frame.f_code, "co_qualname", None) + or _frame.f_code.co_name + ) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): @@ -603,10 +612,15 @@ def get_fn_name(self) -> str: Returns the fully-qualified name of the wrapped function when used as a decorator, the ``name`` passed to the retrying object when used as a - context manager, or ``""`` if neither is available. + context manager, the inferred caller name when used as a context manager + without an explicit ``name``, or ``""`` if none is available. """ if self.fn is not None: return _utils.get_callback_name(self.fn) + # Inferred from the caller's frame in __iter__ (context-manager usage) + inferred: str | None = getattr(self, "_inferred_name", None) + if inferred is not None: + return inferred return str(self.retry_object) @property From ce8d088716dadabb32141f0cd0fcf821fb8f2e8d Mon Sep 17 00:00:00 2001 From: Anton Petnitsky <168552591+Mukller@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:45:31 +0300 Subject: [PATCH 2/2] test: regression for #511 (before_log infers caller name in context manager) When Retrying is used as a context manager without an explicit name=, before_log previously logged "" because retry_state.fn is None in that path. Add test_logging_infers_caller_name to verify that the enclosing function name ("my_retry_function") appears in the log message after the fix. --- tests/test_tenacity.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index 8f74ec86..8cd2546e 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -176,6 +176,42 @@ def test_logging_uses_name(self) -> None: args = log.call_args[0] assert "my_block" in args[1] + def test_logging_infers_caller_name(self) -> None: + """before_log infers the enclosing function name when no name= is given (#511). + + When Retrying is used as a context manager without an explicit name= + parameter, retry_state.fn is None. Before the fix, get_fn_name() would + fall back to str(retry_object) == "". Now __iter__ captures + sys._getframe(1) — the for-loop frame — and stores co_qualname / co_name + as retry_state._inferred_name so that before_log() can log something + meaningful instead of ''. + """ + import unittest.mock + + log = unittest.mock.MagicMock() + logger = unittest.mock.MagicMock(log=log) + + def my_retry_function() -> None: + with contextlib.suppress(Exception): + for attempt in Retrying( + before=tenacity.before_log(logger, logging.INFO), + stop=tenacity.stop_after_attempt(1), + ): + with attempt: + raise ValueError("boom") + + my_retry_function() + + # before_log must have been called at least once + assert log.call_args is not None, "before_log was never called" + msg = log.call_args[0][1] + assert "" not in msg, ( + f"Expected an inferred caller name in the log message, got: {msg!r}" + ) + assert "my_retry_function" in msg, ( + f"Expected 'my_retry_function' in the log message, got: {msg!r}" + ) + class TestStopConditions(unittest.TestCase): def test_never_stop(self) -> None: