I have a callable class:
class CallMeMaybe:
__name__ = 'maybe'
def __init__(self):
self.n_calls = 0
def __call__(self):
self.n_calls += 1
raise Exception
That seems to work as advertised:
>>> f = CallMeMaybe()
>>> f.n_calls
0
>>> for i in range(7):
... try:
... f()
... except Exception:
... pass
...
>>> f.n_calls
7
I want to decorate it with an exponential backoff:
from backoff import on_exception, expo
dec = on_exception(expo, Exception, max_tries=3, on_backoff=print)
f = CallMeMaybe()
f2 = dec(f)
Now it looks like attribute access stopped working:
>>> f2.n_calls
0
>>> f2()
{'target': <__main__.CallMeMaybe object at 0xcafef00d>, 'args': (), 'kwargs': {}, 'tries': 1, 'elapsed': 2.1e-05, 'wait': 0.4843249208229148}
{'target': <__main__.CallMeMaybe object at 0xcafef00d>, 'args': (), 'kwargs': {}, 'tries': 2, 'elapsed': 0.484935, 'wait': 1.6524016553598126}
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
... blah blah blah
>>> f2.n_calls
0
My question: who copied n_calls
name into f2
's namespace, and why? Now it holds a stale value - the correct value should be 3:
>>> f2.__wrapped__.n_calls
3
The backoff
module in its implementation uses functools.wraps
which calls functools.update_wrapper
and you can see from the source code that by default it updates the __dict__
of the wrapper:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Update a wrapper function to look like the wrapped function
wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
+
−# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
# Return the wrapper so this can be used as a decorator via partial()
return wrapper
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function
Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
Unfortunately it seems impossible to achieve what you want. The backoff
module could allow optional assigned
/updated
attributes lists to be passed to wraps
to avoid the copy of the attribute. However this would really solve the issue because at that point you would not have access to n_calls
.
You probably need to use a mutable object instead of a plain int
.