forked from invl/retry
-
Notifications
You must be signed in to change notification settings - Fork 0
retry generator + tests #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import logging | ||
| import random | ||
| import time | ||
| import types | ||
|
|
||
| from functools import partial | ||
|
|
||
|
|
@@ -51,7 +52,52 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, | |
| _delay = min(_delay, max_delay) | ||
|
|
||
|
|
||
| def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger): | ||
| def __retry_internal_generator(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, | ||
| logger=logging_logger): | ||
| """ | ||
| Iterate through a generator returned by a given function and retry from the start if it failed. | ||
|
|
||
| :param f: the function that returns a generator | ||
| :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. | ||
| :param tries: the maximum number of attempts. default: -1 (infinite). | ||
| :param delay: initial delay between attempts. default: 0. | ||
| :param max_delay: the maximum value of delay. default: None (no limit). | ||
| :param backoff: multiplier applied to delay between attempts. default: 1 (no backoff). | ||
| :param jitter: extra seconds added to delay between attempts. default: 0. | ||
| fixed if a number, random if a range tuple (min, max) | ||
| :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. | ||
| default: retry.logging_logger. if None, logging is disabled. | ||
| :returns: the result of the f function. | ||
| """ | ||
| _tries, _delay = tries, delay | ||
| while _tries: | ||
| try: | ||
| for x in f(): | ||
| yield x | ||
| raise StopIteration | ||
| except StopIteration: | ||
| raise | ||
| except exceptions as e: | ||
| _tries -= 1 | ||
| if not _tries: | ||
| raise | ||
|
|
||
| if logger is not None: | ||
| logger.warning('%s, retrying in %s seconds...', e, _delay) | ||
|
|
||
| time.sleep(_delay) | ||
| _delay *= backoff | ||
|
|
||
| if isinstance(jitter, tuple): | ||
| _delay += random.uniform(*jitter) | ||
| else: | ||
| _delay += jitter | ||
|
|
||
| if max_delay is not None: | ||
| _delay = min(_delay, max_delay) | ||
|
|
||
|
|
||
| def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger, generator=False): | ||
| """Returns a retry decorator. | ||
|
|
||
| :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. | ||
|
|
@@ -63,15 +109,22 @@ def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, ji | |
| fixed if a number, random if a range tuple (min, max) | ||
| :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. | ||
| default: retry.logging_logger. if None, logging is disabled. | ||
| :param generator: if True, assumes decorated function returns a generator and wraps | ||
| it in a new generator. Will retry from start on failure. | ||
| :returns: a retry decorator. | ||
| """ | ||
|
|
||
| @decorator | ||
| def retry_decorator(f, *fargs, **fkwargs): | ||
| args = fargs if fargs else list() | ||
| kwargs = fkwargs if fkwargs else dict() | ||
| return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, | ||
| logger) | ||
| pf = partial(f, *args, **kwargs) | ||
| if generator: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if inspect.isgeneratorfunction(f):
...
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my other comments on interface. I could, however, error if |
||
| return __retry_internal_generator(pf, exceptions, tries, delay, max_delay, backoff, jitter, | ||
| logger) | ||
| else: | ||
| return __retry_internal(pf, exceptions, tries, delay, max_delay, backoff, jitter, | ||
| logger) | ||
|
|
||
| return retry_decorator | ||
|
|
||
|
|
@@ -99,3 +152,30 @@ def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, dela | |
| args = fargs if fargs else list() | ||
| kwargs = fkwargs if fkwargs else dict() | ||
| return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, logger) | ||
|
|
||
|
|
||
| def retry_generator(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, | ||
| logger=logging_logger): | ||
| """ | ||
| Return a generator that wraps a generator returned by a given function, retrying from the start on failure. | ||
|
|
||
| :param f: the function that returns a generator | ||
| :param fargs: the positional arguments of the function to execute. | ||
| :param fkwargs: the named arguments of the function to execute. | ||
| :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. | ||
| :param tries: the maximum number of attempts. default: -1 (infinite). | ||
| :param delay: initial delay between attempts. default: 0. | ||
| :param max_delay: the maximum value of delay. default: None (no limit). | ||
| :param backoff: multiplier applied to delay between attempts. default: 1 (no backoff). | ||
| :param jitter: extra seconds added to delay between attempts. default: 0. | ||
| fixed if a number, random if a range tuple (min, max) | ||
| :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. | ||
| default: retry.logging_logger. if None, logging is disabled. | ||
| :returns: the result of the f function. | ||
| """ | ||
| args = fargs if fargs else list() | ||
| kwargs = fkwargs if fkwargs else dict() | ||
| return __retry_internal_generator(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, logger) | ||
|
|
||
|
|
||
|
|
||
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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Python compiles generator functions in a different fashion than regular functions, you can determine this via inspection instead of requiring the user to know whether a function is a generator or not: https://docs.python.org/3/library/inspect.html#inspect.isgeneratorfunction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was requiring the user to do so here to ensure no change in interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do you mean "no change in interface"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Say you have this method:
Before this PR, you could
@retryfooand expect certain behaviors. I don't want to change that behavior, instead requiring the developer to usegenerator=Trueto change behavior in that scenario.