tenacity icon indicating copy to clipboard operation
tenacity copied to clipboard

9.0.0: Unintended or missing behavior in retry_with

Open at3560k opened this issue 1 year ago • 3 comments

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.

  1. retry_with does not appear to be modify logging behavior as I understand the documents
  2. 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:

  1. To see logs at warning instead of info level
  2. For the last output to show x4='yay it worked', not an exception.

at3560k avatar Aug 15 '24 17:08 at3560k

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.

guillermobox avatar Feb 19 '25 13:02 guillermobox

as @guillermobox explained , retry_with is a callable attribute of the decorated function decorated_function.retry_with()

this is not a bug

LotfiRafik avatar Mar 09 '25 13:03 LotfiRafik

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)]

slkoo-cc avatar Nov 21 '25 02:11 slkoo-cc