9.0.0: Unintended or missing behavior in retry_with
Hi tenacity-devs. Love the project, but I went to try something today and encountered behavior that surprised me. I'm attaching a code snippet with two things I think aren't working right.
- retry_with does not appear to be modify logging behavior as I understand the documents
- retry_with seems to mask or entirely box the return type on success, which results in an exception when trying to capture teh value!
It may be that I'm missing a use-pattern here. If so, sorry -- please point me at the right spot.
My intended use-case necessitates a dynamic number of stop=stop_after_attempts, and the retry_with docs seemed perfectly suited to this vs. generating/passing a closure.
#!/usr/bin/env python3
# tenacity==9.0.0
# python 3.8.19
import logging
import random
import sys
from tenacity import before_log, retry, stop_after_attempt, retry_if_exception_type
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
log = logging.getLogger(__name__)
@retry(
reraise=True,
retry=retry_if_exception_type(ZeroDivisionError),
stop=stop_after_attempt(3)
)
def do_something_always_fails():
"""
Try to return a string but screw it up.
"""
raise ZeroDivisionError("terrible")
return "you never get here, but this returns a string"
# Just have logging turned on to watch what happens
@retry(
reraise=True,
retry=retry_if_exception_type(ZeroDivisionError),
stop=stop_after_attempt(3),
before=before_log(log, logging.INFO)
)
def do_something_unreliable():
"""
But return a string!
"""
if random.randint(0, 10) > 2:
raise ZeroDivisionError("sometimes fails")
else:
return "Yay, it worked"
try:
x1 = do_something_always_fails()
# X has no value ever
except Exception as e :
print("[pass] Trapped and worked as intended")
pass
try:
x2 = do_something_always_fails().retry_with(retry=stop_after_attempt(4))
except Exception as e :
# x2 has to be undefined
print(f"[pass] Trapped and overrode as intended")
pass
# Seems correct....
try:
x3 = do_something_unreliable()
print(f"[pass] unreliable thing worked with raw function {x3=}")
# I work sometimes, and this is fine.
except ZeroDivisionError as e:
# I Print sometimes, and this is fine.
print(f"[pass] Unreliable thing kicked out")
# Seems buggy.
try:
x4 = do_something_unreliable().retry_with(
stop=stop_after_attempt(15),
# bug 1?: Modifying the log level doesn't work, I expect to see failures at warn
# but they land at info
before=before_log(log, logging.WARNING)
)
# The below line has never been seen
print(f"[pass] unreliable thing.retry_with worked {x4=}")
except ZeroDivisionError as e:
print("I expect to never get here, statistically") # But I guess it's possible
except Exception as e: # This exception always happens...
print(f"This is a bug? {e}")
# str(e) == "'str' object has no attribute 'retry_with'" !
# Where is my response, why isn't it yay it worked?
Sample output
❯ python tenacity_bug.py
[pass] Trapped and worked as intended
[pass] Trapped and overrode as intended
INFO:__main__:Starting call to '__main__.do_something_unreliable', this is the 1st time calling it.
INFO:__main__:Starting call to '__main__.do_something_unreliable', this is the 2nd time calling it.
INFO:__main__:Starting call to '__main__.do_something_unreliable', this is the 3rd time calling it.
[pass] Unreliable thing kicked out
INFO:__main__:Starting call to '__main__.do_something_unreliable', this is the 1st time calling it.
INFO:__main__:Starting call to '__main__.do_something_unreliable', this is the 2nd time calling it.
This is a bug? 'str' object has no attribute 'retry_with'
I expect:
- To see logs at warning instead of info level
- For the last output to show x4='yay it worked', not an exception.
I think you are using this mechanism wrong, instead of:
x4 = do_something_unreliable().retry_with(
you should have:
x4 = do_something_unreliable.retry_with(
Edit: and you add a function call at the end of the retry_with call.
Otherwise the regular retry mechanism of do_something_unreliable kicks in when you call the function and of course you get a string as output. This is consistent with the documentation so I would say it's not a bug.
as @guillermobox explained , retry_with is a callable attribute of the decorated function
decorated_function.retry_with()
this is not a bug
This seems to be working, but mypy isn't liking the type signature and raises an error with missing retry_with:
"Callable[[date | None], Coroutine[Any, Any, JobResult]]" has no attribute "retry_with" [attr-defined]
and pylance too:
Cannot access attribute "retry_with" for class "FunctionType"
Attribute "retry_with" is unknown [Pylance [reportFunctionMemberAccess](https://github.com/microsoft/pylance-release/blob/main/docs/diagnostics/reportFunctionMemberAccess.md)]