tenacity icon indicating copy to clipboard operation
tenacity copied to clipboard

Provide a context manager

Open xmedeko opened this issue 7 years ago • 15 comments

I like the idea from https://github.com/bhearsum/redo to have retrying contextmanager:

from tenacity import retry
from tenacity.stop import stop_after_attempt


@contextmanager
def retrying(func, *retry_args, **retry_kwargs):
    yield retry(*retry_args, **retry_kwargs)(func)


def foo(max_count):
    global count
    count += 1
    print(count)
    if count < max_count:
        raise ValueError("count too small")
    return "success!"

count = 0
ret = None
max_attempt=5
with retrying(foo, stop=stop_after_attempt(max_attempt)) as f:
    ret = f(3)
print(ret)

Do you think is may be included in the tenacity? I have no clue if it works with async and futures.

xmedeko avatar Jan 12 '17 09:01 xmedeko

Yes, I think having a context manager would be an interesting feature. :)

jd avatar Jan 12 '17 12:01 jd

So there's actually no interesting thing we can do with the with statement in Python. It'd be useful if there was some macro, but, nop.

Basically your example turns down to be just:

f = tenacity.Retrying(stop=stop_after_attempt(max_attempt)
f.call(foo, 3)

I've added __call__ so it can be reduced to:

f = tenacity.Retrying(stop=stop_after_attempt(max_attempt)
f(foo, 3)

jd avatar Mar 06 '17 16:03 jd

+1 I am still novice in Python, so it seem's the redo solution is overkill. For me, the f.call() is well enough. IMHO it's just necessary to document such features.

xmedeko avatar Mar 06 '17 16:03 xmedeko

So there's actually no interesting thing we can do with the with statement in Python. It'd be useful if there was some macro, but, nop.

What about something like this:

retrying = tenacity.Retrying(stop=stop_after_attempt(max_attempt))
with retrying:
    foo(3)

With something like this:

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.attempt_number = 0
            return True
        else:
            self.attempt_number += 1
            return self.attempt_number <= self.max_attempt_number

proppy avatar Apr 13 '17 18:04 proppy

@proppy sounds like a good idea. It's not much different than doing retrying(foo, 3) but I can see the value of being more Pythonic. Wanna do a PR? ;)

jd avatar Apr 14 '17 13:04 jd

I would prefer the context manager to be more Pythonic:

with tenacity.Retrying(func, ...) as func_result:
  print(func_result.http_statuscode)

OR

with tenacity.Retrying(...)(func, args) as func_result:
  print(func_result.http_statuscode)

heri16 avatar Apr 22 '17 16:04 heri16

@heri16 There's no need for a context manager in this case… you can just do func_result = tenacity.Retrying()(func, args)

jd avatar Apr 22 '17 16:04 jd

@jd I see what you mean. I relooked at redo's context-manager documentation and produced 4 pull requests to review.

heri16 avatar Apr 23 '17 00:04 heri16

I took a look at your idea @proppy but unfortunately, there's no way to re-call the function in __exit__ since the code/function that is being called in the with statement is unknown. Unless it's passed alongside that with statement, but that adds 0 value, as the PRs from @heri16 show.

So while, it'd be a nice addition, until Python transforms itself more into a Lisp and adds some macro support or something like that, this seems impossible to implement.

jd avatar Apr 24 '17 08:04 jd

@jd Sorry for the lack of update.

I was thinking retry wouldn't really need to know about the code being retried, if it provide something that yields "Attempt context-managers".

Something like this:

for attempt in tenacity.Attempts(stop=stop_after_attempt(max_attempt)):
  with attempt:
    if some_condition:
      may_fail()
    else:
      may_fail_too()   

Each attempt could report back on __exit__ the success/failure to the generator, and Attempts could either:

  • raise StopIteration if the attempt succeeded.
  • yield another attempt if max_attempt is not reached.
  • re-raise the exception is max_attempts is reached.

What do you think?

proppy avatar Apr 24 '17 08:04 proppy

That looks like cutting the workflow in piece but I can see some value. If you can make it, go ahead.

jd avatar Apr 24 '17 08:04 jd

@jd does it looks like a big refactoring of tenacity code ? Do you have some insight to give us so to contribute ?

bersace avatar Mar 19 '19 16:03 bersace

I don't think it's a lot of refactoring nowadays. You could probably add an __iter__ method on tenacity.Retrying whose code would be based on the tenacity.Retrying.call method. The latter uses a while True internally to call self.iter: that could be split and changed to make the approach iterative.

Then __iter__ could return a context manager that handles what @proppy described in https://github.com/jd/tenacity/issues/48#issuecomment-296572380 as an API.

jd avatar Mar 21 '19 07:03 jd

maybe helpful, maybe not, but i implemented the contexts in the ruby Retriable gem; i think we ended up in a good place: https://github.com/kamui/retriable/pull/43

apurvis avatar Mar 25 '19 23:03 apurvis

@apurvis Python does not have big-lambda like Ruby's block. So the context is quite more complex in python, we need to combine a generator and a context manager.

bersace avatar Mar 26 '19 08:03 bersace