Keep statistics visible when @retry is further wrapped (fixes #519)#653
Open
apoorvdarshan wants to merge 2 commits into
Open
Keep statistics visible when @retry is further wrapped (fixes #519)#653apoorvdarshan wants to merge 2 commits into
apoorvdarshan wants to merge 2 commits into
Conversation
When a @retry-decorated function was wrapped by another decorator that
uses functools.wraps (e.g. a timing decorator), accessing
``func.statistics`` returned an empty ``{}``.
functools.wraps copies the inner wrapper's ``__dict__`` (including the
``statistics`` reference) into the outer wrapper. On each call the inner
wrapper rebound ``wrapped_f.statistics`` to a fresh dict, so the outer
wrapper kept pointing at the original, now-stale, empty dict.
Reuse the same statistics dict in place on every call (clearing it and
sharing it with the per-call copy) instead of rebinding the attribute.
The dict identity stays stable, so the stats remain reachable through
the outer wrapper's copied ``__dict__``. The same change is applied to
the async wrapper.
Fixes jd#519.
The runtime fix keeps func.statistics visible when a @retry-decorated
function is further wrapped by another functools.wraps-based decorator.
However mypy (strict) rejected the tests because the public typing did
not expose the attributes the tests rely on:
* _RetryDecorated had no __wrapped__ member, so accessing
my_call.__wrapped__.statistics failed with [attr-defined].
* the sync test's outer decorator was annotated
Callable[..., Any] -> Callable[..., Any], erasing the
_RetryDecorated type so .statistics/.__wrapped__ were unreachable.
Declare __wrapped__ on the _RetryDecorated protocol (it is always set by
functools.wraps on the retry wrapper) and make the sync test's outer
decorator type-preserving via a TypeVar, mirroring the async test. mypy,
ruff, pytest and the docs build are all green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #519.
When a
@retry-decorated function is further wrapped by another decorator that usesfunctools.wraps(for example a timing decorator), accessingfunc.statisticsreturned an empty{}instead of the real runtime statistics.Cause
functools.wrapscopies the inner wrapper's__dict__(which includes thestatisticsreference) into the outer wrapper. The innerwrapped_fsetwrapped_f.statistics = {}at decoration time, so the outer wrapper received a reference to that empty dict. On each call,wrapped_f.statisticswas then rebound to a fresh per-call dict, while the outer wrapper kept pointing at the original, now-stale, empty dict — hence{}.Fix
Reuse the same
statisticsdict in place on every call instead of rebinding the attribute: the per-call retry copy is pointed at the wrapper's stable dict (which is cleared at the start of the call and populated during it). Because the dict identity never changes, the statistics stay reachable through the outer wrapper's copied__dict__. The same change is applied to the async wrapper intenacity/asyncio.This only fixes the empty-statistics behaviour; the separate
return_statistics_inlineidea from the issue is intentionally out of scope.Tests / Verification
test_statistics_visible_through_outer_decoratorin bothtests/test_tenacity.pyandtests/test_asyncio.py. Both fail onmain(KeyError: 'attempt_number') and pass with the fix. Each also assertsfunc.statistics is func.__wrapped__.statistics, i.e. the identity is now shared through the wrapper chain.pytest tests/), including the existing statistics/sync tests.ruff checkandruff format --checkclean;mypy(strict) clean on the changed modules.releasenotes/notes/.Disclosure: prepared with AI assistance; reviewed and verified locally.