fix: infer caller name in before_log when used as context manager (#511) - #665
fix: infer caller name in before_log when used as context manager (#511)#665Mukller wants to merge 2 commits into
Conversation
) 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 '<unknown>'. 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 '<unknown>', so before_log() now logs the actual enclosing function instead of '<unknown>'. Users who explicitly pass name='my_fn' are unaffected. Python < 3.11 falls back from co_qualname to co_name gracefully.
… manager)
When Retrying is used as a context manager without an explicit name=, before_log
previously logged "<unknown>" 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.
Mukller
left a comment
There was a problem hiding this comment.
Code review
Correctness of sys._getframe(1) inside a generator
The key question is: what does sys._getframe(1) return inside __iter__ (a generator)?
When for attempt in Retrying(...): executes:
- Python creates the generator object by calling
__iter__()— the body does not run yet. - The
forloop callsnext()on the generator, which resumes the body. - At that moment the call stack is:
[0] __iter__ generator body → [1] the for-loop frame.
Frame 1 is therefore the function that contains the for attempt in ... loop — exactly what we want. This is consistent across CPython versions (the generator protocol has not changed in this regard).
co_qualname availability
co_qualname was added in Python 3.11 (PEP 667). The fix uses:
getattr(_frame.f_code, "co_qualname", None) or _frame.f_code.co_nameOn Python < 3.11 this degrades gracefully to co_name (unqualified function name). The fix does not tighten the minimum Python version requirement.
No public API change
_inferred_name is set as an instance attribute with a leading underscore (internal). RetryCallState.__init__ signature is unchanged. Users who construct RetryCallState directly (as in the existing unit tests) receive no _inferred_name attribute, so getattr(self, "_inferred_name", None) returns None and behaviour is identical to before.
enabled=False path
The if not self.enabled: fast path in __iter__ is not touched — it skips before_log entirely (no self.before(retry_state) call), so it doesn't need _inferred_name.
Thread safety
_inferred_name is set once on the retry_state object, which is local to a single __iter__ call. No shared mutable state is introduced.
Suggested improvements (non-blocking)
- Could also apply the same inference to
AsyncRetrying.__aiter__intenacity/asyncio.pyfor parity. - The docstring for
RetryCallState.get_fn_namenow accurately describes all three cases (decorator, explicitname=, inferred frame).
Overall the change is minimal, backward-compatible, and targets the exact failure mode described in the issue.
Summary
Fixes #511 —
before_log()logs'<unknown>'whenRetryingis used as a context manager without an explicitname=parameter.Root cause
__iter__createsRetryCallState(self, fn=None, ...)because there is no wrapped function in the context-manager path.get_fn_name()then falls back tostr(self.retry_object)which returns"<unknown>"when noname=was supplied.Fix
Two minimal changes in
tenacity/__init__.py:BaseRetrying.__iter__— after the firstnext()resumes the generator,sys._getframe(1)is the user'sfor-loop frame. We readco_qualname(Python ≥ 3.11) /co_name(older) and store it asretry_state._inferred_namewhenself._name is None.RetryCallState.get_fn_name— checks_inferred_namebefore returningstr(self.retry_object), so the inferred name is used when available.Behaviour
Users who pass
name="something"explicitly are completely unaffected.Changes
tenacity/__init__.py__iter__; check_inferred_nameinget_fn_name()tests/test_tenacity.pytest_logging_infers_caller_nameregression test