tenacity
Advanced tools
| version: 2 | ||
| updates: | ||
| - package-ecosystem: 'github-actions' | ||
| directory: '/' | ||
| schedule: | ||
| interval: 'monthly' | ||
| groups: | ||
| github-actions: | ||
| patterns: | ||
| - '*' |
| --- | ||
| features: | ||
| - | | ||
| Added a new stop function: stop_before_delay, which will stop execution | ||
| if the next sleep time would cause overall delay to exceed the specified delay. | ||
| Useful for use cases where you have some upper bound on retry times that you must | ||
| not exceed, so returning before that timeout is preferable than returning after that timeout. |
| --- | ||
| other: | ||
| - Add a \"test\" extra |
| --- | ||
| other: | ||
| - | | ||
| Add a Dependabot configuration submit PRs monthly (as needed) | ||
| to keep GitHub action versions updated. |
| --- | ||
| fixes: | ||
| - | | ||
| Preserve __defaults__ and __kwdefaults__ through retry decorator |
| import functools | ||
| from tenacity import _utils | ||
| def test_is_coroutine_callable() -> None: | ||
| async def async_func() -> None: | ||
| pass | ||
| def sync_func() -> None: | ||
| pass | ||
| class AsyncClass: | ||
| async def __call__(self) -> None: | ||
| pass | ||
| class SyncClass: | ||
| def __call__(self) -> None: | ||
| pass | ||
| lambda_fn = lambda: None # noqa: E731 | ||
| partial_async_func = functools.partial(async_func) | ||
| partial_sync_func = functools.partial(sync_func) | ||
| partial_async_class = functools.partial(AsyncClass().__call__) | ||
| partial_sync_class = functools.partial(SyncClass().__call__) | ||
| partial_lambda_fn = functools.partial(lambda_fn) | ||
| assert _utils.is_coroutine_callable(async_func) is True | ||
| assert _utils.is_coroutine_callable(sync_func) is False | ||
| assert _utils.is_coroutine_callable(AsyncClass) is False | ||
| assert _utils.is_coroutine_callable(AsyncClass()) is True | ||
| assert _utils.is_coroutine_callable(SyncClass) is False | ||
| assert _utils.is_coroutine_callable(SyncClass()) is False | ||
| assert _utils.is_coroutine_callable(lambda_fn) is False | ||
| assert _utils.is_coroutine_callable(partial_async_func) is True | ||
| assert _utils.is_coroutine_callable(partial_sync_func) is False | ||
| assert _utils.is_coroutine_callable(partial_async_class) is True | ||
| assert _utils.is_coroutine_callable(partial_sync_class) is False | ||
| assert _utils.is_coroutine_callable(partial_lambda_fn) is False |
@@ -21,4 +21,2 @@ name: Continuous Integration | ||
| include: | ||
| - python: "3.7" | ||
| tox: py37 | ||
| - python: "3.8" | ||
@@ -32,11 +30,11 @@ tox: py38 | ||
| tox: py311 | ||
| - python: "3.11" | ||
| - python: "3.12" | ||
| tox: py312 | ||
| - python: "3.12" | ||
| tox: pep8 | ||
| - python: "3.11" | ||
| tox: black-ci | ||
| - python: "3.11" | ||
| tox: mypy | ||
| steps: | ||
| - name: Checkout 🛎️ | ||
| uses: actions/checkout@v3.3.0 | ||
| uses: actions/checkout@v4.1.1 | ||
| with: | ||
@@ -46,5 +44,6 @@ fetch-depth: 0 | ||
| - name: Setup Python 🔧 | ||
| uses: actions/setup-python@v4.5.0 | ||
| uses: actions/setup-python@v5.0.0 | ||
| with: | ||
| python-version: ${{ matrix.python }} | ||
| python-version: ${{ matrix.python }} | ||
| allow-prereleases: true | ||
@@ -51,0 +50,0 @@ - name: Build 🔧 & Test 🔍 |
| name: Release deploy | ||
| on: | ||
| push: | ||
| tags: | ||
| release: | ||
| types: | ||
| - published | ||
| jobs: | ||
| test: | ||
| publish: | ||
| timeout-minutes: 20 | ||
| runs-on: ubuntu-20.04 | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout 🛎️ | ||
| uses: actions/checkout@v3.3.0 | ||
| uses: actions/checkout@v4.1.1 | ||
| with: | ||
@@ -18,3 +19,3 @@ fetch-depth: 0 | ||
| - name: Setup Python 🔧 | ||
| uses: actions/setup-python@v4.5.0 | ||
| uses: actions/setup-python@v5.0.0 | ||
| with: | ||
@@ -21,0 +22,0 @@ python-version: 3.11 |
+16
-39
| queue_rules: | ||
| - name: default | ||
| conditions: &CheckRuns | ||
| - "check-success=test (3.7, py37)" | ||
| merge_method: squash | ||
| queue_conditions: | ||
| - or: | ||
| - author = jd | ||
| - "#approved-reviews-by >= 1" | ||
| - author = dependabot[bot] | ||
| - or: | ||
| - files ~= ^releasenotes/notes/ | ||
| - label = no-changelog | ||
| - author = dependabot[bot] | ||
| - "check-success=test (3.8, py38)" | ||
@@ -9,5 +17,4 @@ - "check-success=test (3.9, py39)" | ||
| - "check-success=test (3.11, py311)" | ||
| - "check-success=test (3.11, black-ci)" | ||
| - "check-success=test (3.11, pep8)" | ||
| - "check-success=test (3.11, mypy)" | ||
| - "check-success=test (3.12, py312)" | ||
| - "check-success=test (3.12, pep8)" | ||
@@ -26,38 +33,8 @@ pull_request_rules: | ||
| a changelog entry. | ||
| - name: automatic merge without changelog | ||
| conditions: | ||
| - and: *CheckRuns | ||
| - "#approved-reviews-by>=1" | ||
| - label=no-changelog | ||
| - name: automatic queue | ||
| conditions: [] | ||
| actions: | ||
| queue: | ||
| name: default | ||
| method: squash | ||
| - name: automatic merge with changelog | ||
| conditions: | ||
| - and: *CheckRuns | ||
| - "#approved-reviews-by>=1" | ||
| - files~=^releasenotes/notes/ | ||
| actions: | ||
| queue: | ||
| name: default | ||
| method: squash | ||
| - name: automatic merge for jd without changelog | ||
| conditions: | ||
| - author=jd | ||
| - and: *CheckRuns | ||
| - label=no-changelog | ||
| actions: | ||
| queue: | ||
| name: default | ||
| method: squash | ||
| - name: automatic merge for jd with changelog | ||
| conditions: | ||
| - author=jd | ||
| - and: *CheckRuns | ||
| - files~=^releasenotes/notes/ | ||
| actions: | ||
| queue: | ||
| name: default | ||
| method: squash | ||
| - name: dismiss reviews | ||
@@ -64,0 +41,0 @@ conditions: [] |
@@ -20,2 +20,5 @@ =============== | ||
| .. autoclass:: tenacity.RetryCallState | ||
| :members: | ||
| After Functions | ||
@@ -22,0 +25,0 @@ --------------- |
+17
-43
@@ -127,2 +127,12 @@ Tenacity | ||
| If you're on a tight deadline, and exceeding your delay time isn't ok, | ||
| then you can give up on retries one attempt before you would exceed the delay. | ||
| .. testcode:: | ||
| @retry(stop=stop_before_delay(10)) | ||
| def stop_before_10_s(): | ||
| print("Stopping 1 attempt before 10 seconds") | ||
| raise Exception | ||
| You can combine several stop conditions by using the `|` operator: | ||
@@ -406,40 +416,4 @@ | ||
| ``retry_state`` argument is an object of `RetryCallState` class: | ||
| ``retry_state`` argument is an object of :class:`~tenacity.RetryCallState` class. | ||
| .. autoclass:: tenacity.RetryCallState | ||
| Constant attributes: | ||
| .. autoattribute:: start_time(float) | ||
| :annotation: | ||
| .. autoattribute:: retry_object(BaseRetrying) | ||
| :annotation: | ||
| .. autoattribute:: fn(callable) | ||
| :annotation: | ||
| .. autoattribute:: args(tuple) | ||
| :annotation: | ||
| .. autoattribute:: kwargs(dict) | ||
| :annotation: | ||
| Variable attributes: | ||
| .. autoattribute:: attempt_number(int) | ||
| :annotation: | ||
| .. autoattribute:: outcome(tenacity.Future or None) | ||
| :annotation: | ||
| .. autoattribute:: outcome_timestamp(float or None) | ||
| :annotation: | ||
| .. autoattribute:: idle_for(float) | ||
| :annotation: | ||
| .. autoattribute:: next_action(tenacity.RetryAction or None) | ||
| :annotation: | ||
| Other Custom Callbacks | ||
@@ -452,3 +426,3 @@ ~~~~~~~~~~~~~~~~~~~~~~ | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| :return: whether or not retrying should stop | ||
@@ -459,3 +433,3 @@ :rtype: bool | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| :return: number of seconds to wait before next retry | ||
@@ -466,3 +440,3 @@ :rtype: float | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| :return: whether or not retrying should continue | ||
@@ -473,11 +447,11 @@ :rtype: bool | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| .. function:: my_after(retry_state) | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| .. function:: my_before_sleep(retry_state) | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
@@ -484,0 +458,0 @@ Here's an example with a custom ``before_sleep`` function: |
+4
-3
| Metadata-Version: 2.1 | ||
| Name: tenacity | ||
| Version: 8.2.3 | ||
| Version: 8.3.0 | ||
| Summary: Retry code until it succeeds | ||
@@ -14,3 +14,2 @@ Home-page: https://github.com/jd/tenacity | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
@@ -20,7 +19,9 @@ Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Topic :: Utilities | ||
| Requires-Python: >=3.7 | ||
| Requires-Python: >=3.8 | ||
| Provides-Extra: doc | ||
| Provides-Extra: test | ||
| License-File: LICENSE | ||
| Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. |
+5
-5
@@ -11,10 +11,10 @@ [build-system] | ||
| [tool.black] | ||
| line-length = 120 | ||
| safe = true | ||
| target-version = ["py37", "py38", "py39", "py310", "py311"] | ||
| [tool.ruff] | ||
| line-length = 88 | ||
| indent-width = 4 | ||
| target-version = "py38" | ||
| [tool.mypy] | ||
| strict = true | ||
| files = ["tenacity"] | ||
| files = ["tenacity", "tests"] | ||
| show_error_codes = true | ||
@@ -21,0 +21,0 @@ |
+17
-43
@@ -127,2 +127,12 @@ Tenacity | ||
| If you're on a tight deadline, and exceeding your delay time isn't ok, | ||
| then you can give up on retries one attempt before you would exceed the delay. | ||
| .. testcode:: | ||
| @retry(stop=stop_before_delay(10)) | ||
| def stop_before_10_s(): | ||
| print("Stopping 1 attempt before 10 seconds") | ||
| raise Exception | ||
| You can combine several stop conditions by using the `|` operator: | ||
@@ -406,40 +416,4 @@ | ||
| ``retry_state`` argument is an object of `RetryCallState` class: | ||
| ``retry_state`` argument is an object of :class:`~tenacity.RetryCallState` class. | ||
| .. autoclass:: tenacity.RetryCallState | ||
| Constant attributes: | ||
| .. autoattribute:: start_time(float) | ||
| :annotation: | ||
| .. autoattribute:: retry_object(BaseRetrying) | ||
| :annotation: | ||
| .. autoattribute:: fn(callable) | ||
| :annotation: | ||
| .. autoattribute:: args(tuple) | ||
| :annotation: | ||
| .. autoattribute:: kwargs(dict) | ||
| :annotation: | ||
| Variable attributes: | ||
| .. autoattribute:: attempt_number(int) | ||
| :annotation: | ||
| .. autoattribute:: outcome(tenacity.Future or None) | ||
| :annotation: | ||
| .. autoattribute:: outcome_timestamp(float or None) | ||
| :annotation: | ||
| .. autoattribute:: idle_for(float) | ||
| :annotation: | ||
| .. autoattribute:: next_action(tenacity.RetryAction or None) | ||
| :annotation: | ||
| Other Custom Callbacks | ||
@@ -452,3 +426,3 @@ ~~~~~~~~~~~~~~~~~~~~~~ | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| :return: whether or not retrying should stop | ||
@@ -459,3 +433,3 @@ :rtype: bool | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| :return: number of seconds to wait before next retry | ||
@@ -466,3 +440,3 @@ :rtype: float | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| :return: whether or not retrying should continue | ||
@@ -473,11 +447,11 @@ :rtype: bool | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| .. function:: my_after(retry_state) | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
| .. function:: my_before_sleep(retry_state) | ||
| :param RetryState retry_state: info about current retry invocation | ||
| :param RetryCallState retry_state: info about current retry invocation | ||
@@ -484,0 +458,0 @@ Here's an example with a custom ``before_sleep`` function: |
+5
-2
@@ -16,3 +16,2 @@ [metadata] | ||
| Programming Language :: Python :: 3 :: Only | ||
| Programming Language :: Python :: 3.7 | ||
| Programming Language :: Python :: 3.8 | ||
@@ -22,2 +21,3 @@ Programming Language :: Python :: 3.9 | ||
| Programming Language :: Python :: 3.11 | ||
| Programming Language :: Python :: 3.12 | ||
| Topic :: Utilities | ||
@@ -27,3 +27,3 @@ | ||
| install_requires = | ||
| python_requires = >=3.7 | ||
| python_requires = >=3.8 | ||
| packages = tenacity | ||
@@ -41,3 +41,6 @@ | ||
| sphinx | ||
| test = | ||
| pytest | ||
| tornado>=4.5 | ||
| typeguard | ||
@@ -44,0 +47,0 @@ [tool:pytest] |
| Metadata-Version: 2.1 | ||
| Name: tenacity | ||
| Version: 8.2.3 | ||
| Version: 8.3.0 | ||
| Summary: Retry code until it succeeds | ||
@@ -14,3 +14,2 @@ Home-page: https://github.com/jd/tenacity | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
@@ -20,7 +19,9 @@ Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Topic :: Utilities | ||
| Requires-Python: >=3.7 | ||
| Requires-Python: >=3.8 | ||
| Provides-Extra: doc | ||
| Provides-Extra: test | ||
| License-File: LICENSE | ||
| Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. |
@@ -5,2 +5,6 @@ | ||
| sphinx | ||
| [test] | ||
| pytest | ||
| tornado>=4.5 | ||
| typeguard |
@@ -12,2 +12,3 @@ .editorconfig | ||
| tox.ini | ||
| .github/dependabot.yml | ||
| .github/workflows/ci.yaml | ||
@@ -23,2 +24,4 @@ .github/workflows/deploy.yaml | ||
| releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml | ||
| releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml | ||
| releasenotes/notes/add-test-extra-55e869261b03e56d.yaml | ||
| releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml | ||
@@ -32,2 +35,3 @@ releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml | ||
| releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml | ||
| releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml | ||
| releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml | ||
@@ -46,2 +50,3 @@ releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml | ||
| releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml | ||
| releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml | ||
| releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml | ||
@@ -72,2 +77,3 @@ releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml | ||
| tests/test_tenacity.py | ||
| tests/test_tornado.py | ||
| tests/test_tornado.py | ||
| tests/test_utils.py |
+136
-38
@@ -18,4 +18,3 @@ # Copyright 2016-2018 Julien Danjou | ||
| # limitations under the License. | ||
| import dataclasses | ||
| import functools | ||
@@ -54,2 +53,3 @@ import sys | ||
| from .stop import stop_after_delay # noqa | ||
| from .stop import stop_before_delay # noqa | ||
| from .stop import stop_all # noqa | ||
@@ -101,2 +101,25 @@ from .stop import stop_any # noqa | ||
| dataclass_kwargs = {} | ||
| if sys.version_info >= (3, 10): | ||
| dataclass_kwargs.update({"slots": True}) | ||
| @dataclasses.dataclass(**dataclass_kwargs) | ||
| class IterState: | ||
| actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field( | ||
| default_factory=list | ||
| ) | ||
| retry_run_result: bool = False | ||
| delay_since_first_attempt: int = 0 | ||
| stop_run_result: bool = False | ||
| is_explicit_retry: bool = False | ||
| def reset(self) -> None: | ||
| self.actions = [] | ||
| self.retry_run_result = False | ||
| self.delay_since_first_attempt = 0 | ||
| self.stop_run_result = False | ||
| self.is_explicit_retry = False | ||
| class TryAgain(Exception): | ||
@@ -130,3 +153,5 @@ """Always retry the executed function when raised.""" | ||
| def __repr__(self) -> str: | ||
| state_str = ", ".join(f"{field}={getattr(self, field)!r}" for field in self.REPR_FIELDS) | ||
| state_str = ", ".join( | ||
| f"{field}={getattr(self, field)!r}" for field in self.REPR_FIELDS | ||
| ) | ||
| return f"{self.__class__.__name__}({state_str})" | ||
@@ -227,6 +252,10 @@ | ||
| after: t.Union[t.Callable[["RetryCallState"], None], object] = _unset, | ||
| before_sleep: t.Union[t.Optional[t.Callable[["RetryCallState"], None]], object] = _unset, | ||
| before_sleep: t.Union[ | ||
| t.Optional[t.Callable[["RetryCallState"], None]], object | ||
| ] = _unset, | ||
| reraise: t.Union[bool, object] = _unset, | ||
| retry_error_cls: t.Union[t.Type[RetryError], object] = _unset, | ||
| retry_error_callback: t.Union[t.Optional[t.Callable[["RetryCallState"], t.Any]], object] = _unset, | ||
| retry_error_callback: t.Union[ | ||
| t.Optional[t.Callable[["RetryCallState"], t.Any]], object | ||
| ] = _unset, | ||
| ) -> "BaseRetrying": | ||
@@ -244,3 +273,5 @@ """Copy this object with some parameters changed if needed.""" | ||
| retry_error_cls=_first_set(retry_error_cls, self.retry_error_cls), | ||
| retry_error_callback=_first_set(retry_error_callback, self.retry_error_callback), | ||
| retry_error_callback=_first_set( | ||
| retry_error_callback, self.retry_error_callback | ||
| ), | ||
| ) | ||
@@ -287,2 +318,10 @@ | ||
| @property | ||
| def iter_state(self) -> IterState: | ||
| try: | ||
| return self._local.iter_state # type: ignore[no-any-return] | ||
| except AttributeError: | ||
| self._local.iter_state = IterState() | ||
| return self._local.iter_state | ||
| def wraps(self, f: WrappedFn) -> WrappedFn: | ||
@@ -294,3 +333,5 @@ """Wrap a function for retrying. | ||
| @functools.wraps(f) | ||
| @functools.wraps( | ||
| f, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__") | ||
| ) | ||
| def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any: | ||
@@ -313,38 +354,85 @@ return self(f, *args, **kw) | ||
| def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None: | ||
| self.iter_state.actions.append(fn) | ||
| def _run_retry(self, retry_state: "RetryCallState") -> None: | ||
| self.iter_state.retry_run_result = self.retry(retry_state) | ||
| def _run_wait(self, retry_state: "RetryCallState") -> None: | ||
| if self.wait: | ||
| sleep = self.wait(retry_state) | ||
| else: | ||
| sleep = 0.0 | ||
| retry_state.upcoming_sleep = sleep | ||
| def _run_stop(self, retry_state: "RetryCallState") -> None: | ||
| self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start | ||
| self.iter_state.stop_run_result = self.stop(retry_state) | ||
| def iter(self, retry_state: "RetryCallState") -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa | ||
| self._begin_iter(retry_state) | ||
| result = None | ||
| for action in self.iter_state.actions: | ||
| result = action(retry_state) | ||
| return result | ||
| def _begin_iter(self, retry_state: "RetryCallState") -> None: # noqa | ||
| self.iter_state.reset() | ||
| fut = retry_state.outcome | ||
| if fut is None: | ||
| if self.before is not None: | ||
| self.before(retry_state) | ||
| return DoAttempt() | ||
| self._add_action_func(self.before) | ||
| self._add_action_func(lambda rs: DoAttempt()) | ||
| return | ||
| is_explicit_retry = fut.failed and isinstance(fut.exception(), TryAgain) | ||
| if not (is_explicit_retry or self.retry(retry_state)): | ||
| return fut.result() | ||
| self.iter_state.is_explicit_retry = fut.failed and isinstance( | ||
| fut.exception(), TryAgain | ||
| ) | ||
| if not self.iter_state.is_explicit_retry: | ||
| self._add_action_func(self._run_retry) | ||
| self._add_action_func(self._post_retry_check_actions) | ||
| def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None: | ||
| if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result): | ||
| self._add_action_func(lambda rs: rs.outcome.result()) | ||
| return | ||
| if self.after is not None: | ||
| self.after(retry_state) | ||
| self._add_action_func(self.after) | ||
| self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start | ||
| if self.stop(retry_state): | ||
| self._add_action_func(self._run_wait) | ||
| self._add_action_func(self._run_stop) | ||
| self._add_action_func(self._post_stop_check_actions) | ||
| def _post_stop_check_actions(self, retry_state: "RetryCallState") -> None: | ||
| if self.iter_state.stop_run_result: | ||
| if self.retry_error_callback: | ||
| return self.retry_error_callback(retry_state) | ||
| retry_exc = self.retry_error_cls(fut) | ||
| if self.reraise: | ||
| raise retry_exc.reraise() | ||
| raise retry_exc from fut.exception() | ||
| self._add_action_func(self.retry_error_callback) | ||
| return | ||
| if self.wait: | ||
| sleep = self.wait(retry_state) | ||
| else: | ||
| sleep = 0.0 | ||
| retry_state.next_action = RetryAction(sleep) | ||
| retry_state.idle_for += sleep | ||
| self.statistics["idle_for"] += sleep | ||
| self.statistics["attempt_number"] += 1 | ||
| def exc_check(rs: "RetryCallState") -> None: | ||
| fut = t.cast(Future, rs.outcome) | ||
| retry_exc = self.retry_error_cls(fut) | ||
| if self.reraise: | ||
| raise retry_exc.reraise() | ||
| raise retry_exc from fut.exception() | ||
| self._add_action_func(exc_check) | ||
| return | ||
| def next_action(rs: "RetryCallState") -> None: | ||
| sleep = rs.upcoming_sleep | ||
| rs.next_action = RetryAction(sleep) | ||
| rs.idle_for += sleep | ||
| self.statistics["idle_for"] += sleep | ||
| self.statistics["attempt_number"] += 1 | ||
| self._add_action_func(next_action) | ||
| if self.before_sleep is not None: | ||
| self.before_sleep(retry_state) | ||
| self._add_action_func(self.before_sleep) | ||
| return DoSleep(sleep) | ||
| self._add_action_func(lambda rs: DoSleep(rs.upcoming_sleep)) | ||
@@ -403,3 +491,3 @@ def __iter__(self) -> t.Generator[AttemptManager, None, None]: | ||
| if sys.version_info[1] >= 9: | ||
| if sys.version_info >= (3, 9): | ||
| FutureGenericT = futures.Future[t.Any] | ||
@@ -423,3 +511,5 @@ else: | ||
| @classmethod | ||
| def construct(cls, attempt_number: int, value: t.Any, has_exception: bool) -> "Future": | ||
| def construct( | ||
| cls, attempt_number: int, value: t.Any, has_exception: bool | ||
| ) -> "Future": | ||
| """Construct a new Future object.""" | ||
@@ -465,2 +555,4 @@ fut = cls(attempt_number) | ||
| self.next_action: t.Optional[RetryAction] = None | ||
| #: Next sleep time as decided by the retry manager. | ||
| self.upcoming_sleep: float = 0.0 | ||
@@ -486,3 +578,6 @@ @property | ||
| def set_exception( | ||
| self, exc_info: t.Tuple[t.Type[BaseException], BaseException, "types.TracebackType| None"] | ||
| self, | ||
| exc_info: t.Tuple[ | ||
| t.Type[BaseException], BaseException, "types.TracebackType| None" | ||
| ], | ||
| ) -> None: | ||
@@ -509,4 +604,3 @@ ts = time.monotonic() | ||
| @t.overload | ||
| def retry(func: WrappedFn) -> WrappedFn: | ||
| ... | ||
| def retry(func: WrappedFn) -> WrappedFn: ... | ||
@@ -526,4 +620,3 @@ | ||
| retry_error_callback: t.Optional[t.Callable[["RetryCallState"], t.Any]] = None, | ||
| ) -> t.Callable[[WrappedFn], WrappedFn]: | ||
| ... | ||
| ) -> t.Callable[[WrappedFn], WrappedFn]: ... | ||
@@ -551,3 +644,7 @@ | ||
| r = AsyncRetrying(*dargs, **dkw) | ||
| elif tornado and hasattr(tornado.gen, "is_coroutine_function") and tornado.gen.is_coroutine_function(f): | ||
| elif ( | ||
| tornado | ||
| and hasattr(tornado.gen, "is_coroutine_function") | ||
| and tornado.gen.is_coroutine_function(f) | ||
| ): | ||
| r = TornadoRetrying(*dargs, **dkw) | ||
@@ -587,2 +684,3 @@ else: | ||
| "stop_after_delay", | ||
| "stop_before_delay", | ||
| "stop_all", | ||
@@ -589,0 +687,0 @@ "stop_any", |
+59
-5
@@ -21,3 +21,2 @@ # Copyright 2016 Étienne Bersac | ||
| import typing as t | ||
| from asyncio import sleep | ||
@@ -29,2 +28,3 @@ from tenacity import AttemptManager | ||
| from tenacity import RetryCallState | ||
| from tenacity import _utils | ||
@@ -35,6 +35,17 @@ WrappedFnReturnT = t.TypeVar("WrappedFnReturnT") | ||
| def asyncio_sleep(duration: float) -> t.Awaitable[None]: | ||
| # Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead). | ||
| import asyncio | ||
| return asyncio.sleep(duration) | ||
| class AsyncRetrying(BaseRetrying): | ||
| sleep: t.Callable[[float], t.Awaitable[t.Any]] | ||
| def __init__(self, sleep: t.Callable[[float], t.Awaitable[t.Any]] = sleep, **kwargs: t.Any) -> None: | ||
| def __init__( | ||
| self, | ||
| sleep: t.Callable[[float], t.Awaitable[t.Any]] = asyncio_sleep, | ||
| **kwargs: t.Any, | ||
| ) -> None: | ||
| super().__init__(**kwargs) | ||
@@ -50,3 +61,3 @@ self.sleep = sleep | ||
| while True: | ||
| do = self.iter(retry_state=retry_state) | ||
| do = await self.iter(retry_state=retry_state) | ||
| if isinstance(do, DoAttempt): | ||
@@ -65,2 +76,43 @@ try: | ||
| @classmethod | ||
| def _wrap_action_func(cls, fn: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: | ||
| if _utils.is_coroutine_callable(fn): | ||
| return fn | ||
| async def inner(*args: t.Any, **kwargs: t.Any) -> t.Any: | ||
| return fn(*args, **kwargs) | ||
| return inner | ||
| def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None: | ||
| self.iter_state.actions.append(self._wrap_action_func(fn)) | ||
| async def _run_retry(self, retry_state: "RetryCallState") -> None: # type: ignore[override] | ||
| self.iter_state.retry_run_result = await self._wrap_action_func(self.retry)( | ||
| retry_state | ||
| ) | ||
| async def _run_wait(self, retry_state: "RetryCallState") -> None: # type: ignore[override] | ||
| if self.wait: | ||
| sleep = await self._wrap_action_func(self.wait)(retry_state) | ||
| else: | ||
| sleep = 0.0 | ||
| retry_state.upcoming_sleep = sleep | ||
| async def _run_stop(self, retry_state: "RetryCallState") -> None: # type: ignore[override] | ||
| self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start | ||
| self.iter_state.stop_run_result = await self._wrap_action_func(self.stop)( | ||
| retry_state | ||
| ) | ||
| async def iter( | ||
| self, retry_state: "RetryCallState" | ||
| ) -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa: A003 | ||
| self._begin_iter(retry_state) | ||
| result = None | ||
| for action in self.iter_state.actions: | ||
| result = await action(retry_state) | ||
| return result | ||
| def __iter__(self) -> t.Generator[AttemptManager, None, None]: | ||
@@ -76,3 +128,3 @@ raise TypeError("AsyncRetrying object is not iterable") | ||
| while True: | ||
| do = self.iter(retry_state=self._retry_state) | ||
| do = await self.iter(retry_state=self._retry_state) | ||
| if do is None: | ||
@@ -92,3 +144,5 @@ raise StopAsyncIteration | ||
| @functools.wraps(fn) | ||
| @functools.wraps( | ||
| fn, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__") | ||
| ) | ||
| async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any: | ||
@@ -95,0 +149,0 @@ return await fn(*args, **kwargs) |
+15
-2
@@ -16,3 +16,4 @@ # Copyright 2016 Julien Danjou | ||
| # limitations under the License. | ||
| import functools | ||
| import inspect | ||
| import sys | ||
@@ -77,2 +78,14 @@ import typing | ||
| def to_seconds(time_unit: time_unit_type) -> float: | ||
| return float(time_unit.total_seconds() if isinstance(time_unit, timedelta) else time_unit) | ||
| return float( | ||
| time_unit.total_seconds() if isinstance(time_unit, timedelta) else time_unit | ||
| ) | ||
| def is_coroutine_callable(call: typing.Callable[..., typing.Any]) -> bool: | ||
| if inspect.isclass(call): | ||
| return False | ||
| if inspect.iscoroutinefunction(call): | ||
| return True | ||
| partial_call = isinstance(call, functools.partial) and call.func | ||
| dunder_call = partial_call or getattr(call, "__call__", None) | ||
| return inspect.iscoroutinefunction(dunder_call) |
@@ -67,3 +67,4 @@ # Copyright 2016 Julien Danjou | ||
| log_level, | ||
| f"Retrying {fn_name} " f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.", | ||
| f"Retrying {fn_name} " | ||
| f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.", | ||
| exc_info=local_exc_info, | ||
@@ -70,0 +71,0 @@ ) |
@@ -31,3 +31,5 @@ # Copyright 2016 Julien Danjou | ||
| def before_log(logger: "logging.Logger", log_level: int) -> typing.Callable[["RetryCallState"], None]: | ||
| def before_log( | ||
| logger: "logging.Logger", log_level: int | ||
| ) -> typing.Callable[["RetryCallState"], None]: | ||
| """Before call strategy that logs to some logger the attempt.""" | ||
@@ -34,0 +36,0 @@ |
@@ -207,3 +207,5 @@ # Copyright 2016–2021 Julien Danjou | ||
| if message and match: | ||
| raise TypeError(f"{self.__class__.__name__}() takes either 'message' or 'match', not both") | ||
| raise TypeError( | ||
| f"{self.__class__.__name__}() takes either 'message' or 'match', not both" | ||
| ) | ||
@@ -225,3 +227,5 @@ # set predicate | ||
| else: | ||
| raise TypeError(f"{self.__class__.__name__}() missing 1 required argument 'message' or 'match'") | ||
| raise TypeError( | ||
| f"{self.__class__.__name__}() missing 1 required argument 'message' or 'match'" | ||
| ) | ||
@@ -228,0 +232,0 @@ super().__init__(predicate) |
+28
-1
@@ -95,4 +95,11 @@ # Copyright 2016–2021 Julien Danjou | ||
| class stop_after_delay(stop_base): | ||
| """Stop when the time from the first attempt >= limit.""" | ||
| """ | ||
| Stop when the time from the first attempt >= limit. | ||
| Note: `max_delay` will be exceeded, so when used with a `wait`, the actual total delay will be greater | ||
| than `max_delay` by some of the final sleep period before `max_delay` is exceeded. | ||
| If you need stricter timing with waits, consider `stop_before_delay` instead. | ||
| """ | ||
| def __init__(self, max_delay: _utils.time_unit_type) -> None: | ||
@@ -105,1 +112,21 @@ self.max_delay = _utils.to_seconds(max_delay) | ||
| return retry_state.seconds_since_start >= self.max_delay | ||
| class stop_before_delay(stop_base): | ||
| """ | ||
| Stop right before the next attempt would take place after the time from the first attempt >= limit. | ||
| Most useful when you are using with a `wait` function like wait_random_exponential, but need to make | ||
| sure that the max_delay is not exceeded. | ||
| """ | ||
| def __init__(self, max_delay: _utils.time_unit_type) -> None: | ||
| self.max_delay = _utils.to_seconds(max_delay) | ||
| def __call__(self, retry_state: "RetryCallState") -> bool: | ||
| if retry_state.seconds_since_start is None: | ||
| raise RuntimeError("__call__() called but seconds_since_start is not set") | ||
| return ( | ||
| retry_state.seconds_since_start + retry_state.upcoming_sleep | ||
| >= self.max_delay | ||
| ) |
@@ -32,3 +32,7 @@ # Copyright 2017 Elisey Zanko | ||
| class TornadoRetrying(BaseRetrying): | ||
| def __init__(self, sleep: "typing.Callable[[float], Future[None]]" = gen.sleep, **kwargs: typing.Any) -> None: | ||
| def __init__( | ||
| self, | ||
| sleep: "typing.Callable[[float], Future[None]]" = gen.sleep, | ||
| **kwargs: typing.Any, | ||
| ) -> None: | ||
| super().__init__(**kwargs) | ||
@@ -35,0 +39,0 @@ self.sleep = sleep |
+9
-3
@@ -44,3 +44,5 @@ # Copyright 2016–2021 Julien Danjou | ||
| WaitBaseT = typing.Union[wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]]] | ||
| WaitBaseT = typing.Union[ | ||
| wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]] | ||
| ] | ||
@@ -68,3 +70,5 @@ | ||
| def __init__(self, min: _utils.time_unit_type = 0, max: _utils.time_unit_type = 1) -> None: # noqa | ||
| def __init__( | ||
| self, min: _utils.time_unit_type = 0, max: _utils.time_unit_type = 1 | ||
| ) -> None: # noqa | ||
| self.wait_random_min = _utils.to_seconds(min) | ||
@@ -74,3 +78,5 @@ self.wait_random_max = _utils.to_seconds(max) | ||
| def __call__(self, retry_state: "RetryCallState") -> float: | ||
| return self.wait_random_min + (random.random() * (self.wait_random_max - self.wait_random_min)) | ||
| return self.wait_random_min + ( | ||
| random.random() * (self.wait_random_max - self.wait_random_min) | ||
| ) | ||
@@ -77,0 +83,0 @@ |
+29
-6
@@ -0,1 +1,2 @@ | ||
| # mypy: disable-error-code="no-untyped-def,no-untyped-call" | ||
| import logging | ||
@@ -13,3 +14,11 @@ import random | ||
| def setUp(self) -> None: | ||
| self.log_level = random.choice((logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)) | ||
| self.log_level = random.choice( | ||
| ( | ||
| logging.DEBUG, | ||
| logging.INFO, | ||
| logging.WARNING, | ||
| logging.ERROR, | ||
| logging.CRITICAL, | ||
| ) | ||
| ) | ||
| self.previous_attempt_number = random.randint(1, 512) | ||
@@ -25,6 +34,14 @@ | ||
| retry_state = test_tenacity.make_retry_state(self.previous_attempt_number, delay_since_first_attempt) | ||
| fun = after_log(logger=logger, log_level=self.log_level) # use default sec_format | ||
| retry_state = test_tenacity.make_retry_state( | ||
| self.previous_attempt_number, delay_since_first_attempt | ||
| ) | ||
| fun = after_log( | ||
| logger=logger, log_level=self.log_level | ||
| ) # use default sec_format | ||
| fun(retry_state) | ||
| fn_name = "<unknown>" if retry_state.fn is None else _utils.get_callback_name(retry_state.fn) | ||
| fn_name = ( | ||
| "<unknown>" | ||
| if retry_state.fn is None | ||
| else _utils.get_callback_name(retry_state.fn) | ||
| ) | ||
| log.assert_called_once_with( | ||
@@ -45,6 +62,12 @@ self.log_level, | ||
| retry_state = test_tenacity.make_retry_state(self.previous_attempt_number, delay_since_first_attempt) | ||
| retry_state = test_tenacity.make_retry_state( | ||
| self.previous_attempt_number, delay_since_first_attempt | ||
| ) | ||
| fun = after_log(logger=logger, log_level=self.log_level, sec_format=sec_format) | ||
| fun(retry_state) | ||
| fn_name = "<unknown>" if retry_state.fn is None else _utils.get_callback_name(retry_state.fn) | ||
| fn_name = ( | ||
| "<unknown>" | ||
| if retry_state.fn is None | ||
| else _utils.get_callback_name(retry_state.fn) | ||
| ) | ||
| log.assert_called_once_with( | ||
@@ -51,0 +74,0 @@ self.log_level, |
@@ -1,2 +0,2 @@ | ||
| # coding: utf-8 | ||
| # mypy: disable-error-code="no-untyped-def,no-untyped-call" | ||
| # Copyright 2016 Étienne Bersac | ||
@@ -23,2 +23,3 @@ # | ||
| import tenacity | ||
| from tenacity import AsyncRetrying, RetryError | ||
@@ -92,2 +93,23 @@ from tenacity import _asyncio as tasyncio | ||
| def test_retry_preserves_argument_defaults(self): | ||
| async def function_with_defaults(a=1): | ||
| return a | ||
| async def function_with_kwdefaults(*, a=1): | ||
| return a | ||
| retrying = AsyncRetrying( | ||
| wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3) | ||
| ) | ||
| wrapped_defaults_function = retrying.wraps(function_with_defaults) | ||
| wrapped_kwdefaults_function = retrying.wraps(function_with_kwdefaults) | ||
| self.assertEqual( | ||
| function_with_defaults.__defaults__, wrapped_defaults_function.__defaults__ | ||
| ) | ||
| self.assertEqual( | ||
| function_with_kwdefaults.__kwdefaults__, | ||
| wrapped_kwdefaults_function.__kwdefaults__, | ||
| ) | ||
| @asynctest | ||
@@ -104,4 +126,4 @@ async def test_attempt_number_is_correct_for_interleaved_coroutines(self): | ||
| await asyncio.gather( | ||
| _retryable_coroutine.retry_with(after=after)(thing1), | ||
| _retryable_coroutine.retry_with(after=after)(thing2), | ||
| _retryable_coroutine.retry_with(after=after)(thing1), # type: ignore[attr-defined] | ||
| _retryable_coroutine.retry_with(after=after)(thing2), # type: ignore[attr-defined] | ||
| ) | ||
@@ -143,3 +165,5 @@ | ||
| try: | ||
| async for attempt in tasyncio.AsyncRetrying(stop=stop_after_attempt(1), reraise=True): | ||
| async for attempt in tasyncio.AsyncRetrying( | ||
| stop=stop_after_attempt(1), reraise=True | ||
| ): | ||
| with attempt: | ||
@@ -156,3 +180,5 @@ raise CustomError() | ||
| try: | ||
| async for attempt in tasyncio.AsyncRetrying(stop=stop_after_attempt(1), wait=wait_fixed(1)): | ||
| async for attempt in tasyncio.AsyncRetrying( | ||
| stop=stop_after_attempt(1), wait=wait_fixed(1) | ||
| ): | ||
| with attempt: | ||
@@ -169,3 +195,8 @@ raise Exception() | ||
| attempts = 0 | ||
| async for attempt in tasyncio.AsyncRetrying(retry=retry_if_result(lambda x: x < 3)): | ||
| # mypy doesn't have great lambda support | ||
| def lt_3(x: float) -> bool: | ||
| return x < 3 | ||
| async for attempt in tasyncio.AsyncRetrying(retry=retry_if_result(lt_3)): | ||
| with attempt: | ||
@@ -189,3 +220,14 @@ attempts += 1 | ||
| # make sure mypy accepts passing an async sleep function | ||
| # https://github.com/jd/tenacity/issues/399 | ||
| async def my_async_sleep(x: float) -> None: | ||
| await asyncio.sleep(x) | ||
| @retry(sleep=my_async_sleep) | ||
| async def foo(): | ||
| pass | ||
| if __name__ == "__main__": | ||
| unittest.main() |
+197
-49
@@ -0,1 +1,2 @@ | ||
| # mypy: disable_error_code="no-untyped-def,no-untyped-call,attr-defined,arg-type,no-any-return,list-item,var-annotated,import,call-overload" | ||
| # Copyright 2016–2021 Julien Danjou | ||
@@ -53,3 +54,8 @@ # Copyright 2016 Joshua Harlow | ||
| def make_retry_state(previous_attempt_number, delay_since_first_attempt, last_result=None): | ||
| def make_retry_state( | ||
| previous_attempt_number, | ||
| delay_since_first_attempt, | ||
| last_result=None, | ||
| upcoming_sleep=0, | ||
| ): | ||
| """Construct RetryCallState for given attempt number & delay. | ||
@@ -59,3 +65,5 @@ | ||
| """ | ||
| required_parameter_unset = previous_attempt_number is _unset or delay_since_first_attempt is _unset | ||
| required_parameter_unset = ( | ||
| previous_attempt_number is _unset or delay_since_first_attempt is _unset | ||
| ) | ||
| if required_parameter_unset: | ||
@@ -74,2 +82,5 @@ raise _make_unset_exception( | ||
| retry_state.set_result(None) | ||
| retry_state.upcoming_sleep = upcoming_sleep | ||
| _set_delay_since_start(retry_state, delay_since_first_attempt) | ||
@@ -92,5 +103,11 @@ return retry_state | ||
| rs = make_retry_state(2, 5) | ||
| assert repr(rs).endswith("attempt #2; slept for 0.0; last result: returned None>") | ||
| rs = make_retry_state(0, 0, last_result=tenacity.Future.construct(1, ValueError("aaa"), True)) | ||
| assert repr(rs).endswith("attempt #0; slept for 0.0; last result: failed (ValueError aaa)>") | ||
| assert repr(rs).endswith( | ||
| "attempt #2; slept for 0.0; last result: returned None>" | ||
| ) | ||
| rs = make_retry_state( | ||
| 0, 0, last_result=tenacity.Future.construct(1, ValueError("aaa"), True) | ||
| ) | ||
| assert repr(rs).endswith( | ||
| "attempt #0; slept for 0.0; last result: failed (ValueError aaa)>" | ||
| ) | ||
@@ -104,3 +121,5 @@ | ||
| def test_stop_any(self): | ||
| stop = tenacity.stop_any(tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)) | ||
| stop = tenacity.stop_any( | ||
| tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4) | ||
| ) | ||
@@ -118,3 +137,5 @@ def s(*args): | ||
| def test_stop_all(self): | ||
| stop = tenacity.stop_all(tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)) | ||
| stop = tenacity.stop_all( | ||
| tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4) | ||
| ) | ||
@@ -171,2 +192,17 @@ def s(*args): | ||
| def test_stop_before_delay(self): | ||
| for delay in (1, datetime.timedelta(seconds=1)): | ||
| with self.subTest(): | ||
| r = Retrying(stop=tenacity.stop_before_delay(delay)) | ||
| self.assertFalse( | ||
| r.stop(make_retry_state(2, 0.999, upcoming_sleep=0.0001)) | ||
| ) | ||
| self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=0.001))) | ||
| self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=1))) | ||
| # It should act the same as stop_after_delay if upcoming sleep is 0 | ||
| self.assertFalse(r.stop(make_retry_state(2, 0.999, upcoming_sleep=0))) | ||
| self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=0))) | ||
| self.assertTrue(r.stop(make_retry_state(2, 1.001, upcoming_sleep=0))) | ||
| def test_legacy_explicit_stop_type(self): | ||
@@ -198,5 +234,10 @@ Retrying(stop="stop_after_attempt") | ||
| def test_incrementing_sleep(self): | ||
| for start, increment in ((500, 100), (datetime.timedelta(seconds=500), datetime.timedelta(seconds=100))): | ||
| for start, increment in ( | ||
| (500, 100), | ||
| (datetime.timedelta(seconds=500), datetime.timedelta(seconds=100)), | ||
| ): | ||
| with self.subTest(): | ||
| r = Retrying(wait=tenacity.wait_incrementing(start=start, increment=increment)) | ||
| r = Retrying( | ||
| wait=tenacity.wait_incrementing(start=start, increment=increment) | ||
| ) | ||
| self.assertEqual(500, r.wait(make_retry_state(1, 6546))) | ||
@@ -207,3 +248,6 @@ self.assertEqual(600, r.wait(make_retry_state(2, 6546))) | ||
| def test_random_sleep(self): | ||
| for min_, max_ in ((1, 20), (datetime.timedelta(seconds=1), datetime.timedelta(seconds=20))): | ||
| for min_, max_ in ( | ||
| (1, 20), | ||
| (datetime.timedelta(seconds=1), datetime.timedelta(seconds=20)), | ||
| ): | ||
| with self.subTest(): | ||
@@ -295,3 +339,6 @@ r = Retrying(wait=tenacity.wait_random(min=min_, max=max_)) | ||
| def test_exponential_with_min_wait_andmax__wait(self): | ||
| for min_, max_ in ((10, 100), (datetime.timedelta(seconds=10), datetime.timedelta(seconds=100))): | ||
| for min_, max_ in ( | ||
| (10, 100), | ||
| (datetime.timedelta(seconds=10), datetime.timedelta(seconds=100)), | ||
| ): | ||
| with self.subTest(): | ||
@@ -323,3 +370,7 @@ r = Retrying(wait=tenacity.wait_exponential(min=min_, max=max_)) | ||
| def test_wait_combine(self): | ||
| r = Retrying(wait=tenacity.wait_combine(tenacity.wait_random(0, 3), tenacity.wait_fixed(5))) | ||
| r = Retrying( | ||
| wait=tenacity.wait_combine( | ||
| tenacity.wait_random(0, 3), tenacity.wait_fixed(5) | ||
| ) | ||
| ) | ||
| # Test it a few time since it's random | ||
@@ -340,3 +391,7 @@ for i in range(1000): | ||
| def test_wait_triple_sum(self): | ||
| r = Retrying(wait=tenacity.wait_fixed(1) + tenacity.wait_random(0, 3) + tenacity.wait_fixed(5)) | ||
| r = Retrying( | ||
| wait=tenacity.wait_fixed(1) | ||
| + tenacity.wait_random(0, 3) | ||
| + tenacity.wait_fixed(5) | ||
| ) | ||
| # Test it a few time since it's random | ||
@@ -493,3 +548,6 @@ for i in range(1000): | ||
| wait=waitfunc, | ||
| retry=(tenacity.retry_if_exception_type() | tenacity.retry_if_result(lambda result: result == 123)), | ||
| retry=( | ||
| tenacity.retry_if_exception_type() | ||
| | tenacity.retry_if_result(lambda result: result == 123) | ||
| ), | ||
| ) | ||
@@ -578,3 +636,5 @@ | ||
| def test_retry_and(self): | ||
| retry = tenacity.retry_if_result(lambda x: x == 1) & tenacity.retry_if_result(lambda x: isinstance(x, int)) | ||
| retry = tenacity.retry_if_result(lambda x: x == 1) & tenacity.retry_if_result( | ||
| lambda x: isinstance(x, int) | ||
| ) | ||
@@ -591,3 +651,5 @@ def r(fut): | ||
| def test_retry_or(self): | ||
| retry = tenacity.retry_if_result(lambda x: x == "foo") | tenacity.retry_if_result(lambda x: isinstance(x, int)) | ||
| retry = tenacity.retry_if_result( | ||
| lambda x: x == "foo" | ||
| ) | tenacity.retry_if_result(lambda x: isinstance(x, int)) | ||
@@ -610,3 +672,5 @@ def r(fut): | ||
| self._attempts = 0 | ||
| Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never)(self._raise_try_again) | ||
| Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never)( | ||
| self._raise_try_again | ||
| ) | ||
| self.assertEqual(3, self._attempts) | ||
@@ -675,3 +739,3 @@ | ||
| self.counter += 1 | ||
| raise IOError("Hi there, I'm an IOError") | ||
| raise OSError("Hi there, I'm an IOError") | ||
| return True | ||
@@ -718,3 +782,3 @@ | ||
| except NameError as e: | ||
| raise IOError() from e | ||
| raise OSError() from e | ||
@@ -732,3 +796,3 @@ return True | ||
| def go2(self): | ||
| raise IOError("Hi there, I'm an IOError") | ||
| raise OSError("Hi there, I'm an IOError") | ||
@@ -744,3 +808,3 @@ def go(self): | ||
| self.go2() | ||
| except IOError as e: | ||
| except OSError as e: | ||
| raise NameError() from e | ||
@@ -786,3 +850,3 @@ | ||
| return True | ||
| raise IOError("Hi there, I'm an IOError") | ||
| raise OSError("Hi there, I'm an IOError") | ||
@@ -874,3 +938,5 @@ | ||
| @retry(stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(IOError)) | ||
| @retry( | ||
| stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(IOError) | ||
| ) | ||
| def _retryable_test_with_exception_type_io_attempt_limit(thing): | ||
@@ -900,3 +966,5 @@ return thing.go() | ||
| stop=tenacity.stop_after_attempt(5), | ||
| retry=tenacity.retry_if_exception_message(message=NoCustomErrorAfterCount.derived_message), | ||
| retry=tenacity.retry_if_exception_message( | ||
| message=NoCustomErrorAfterCount.derived_message | ||
| ), | ||
| ) | ||
@@ -907,3 +975,7 @@ def _retryable_test_if_exception_message_message(thing): | ||
| @retry(retry=tenacity.retry_if_not_exception_message(message=NoCustomErrorAfterCount.derived_message)) | ||
| @retry( | ||
| retry=tenacity.retry_if_not_exception_message( | ||
| message=NoCustomErrorAfterCount.derived_message | ||
| ) | ||
| ) | ||
| def _retryable_test_if_not_exception_message_message(thing): | ||
@@ -913,3 +985,7 @@ return thing.go() | ||
| @retry(retry=tenacity.retry_if_exception_message(match=NoCustomErrorAfterCount.derived_message[:3] + ".*")) | ||
| @retry( | ||
| retry=tenacity.retry_if_exception_message( | ||
| match=NoCustomErrorAfterCount.derived_message[:3] + ".*" | ||
| ) | ||
| ) | ||
| def _retryable_test_if_exception_message_match(thing): | ||
@@ -919,3 +995,7 @@ return thing.go() | ||
| @retry(retry=tenacity.retry_if_not_exception_message(match=NoCustomErrorAfterCount.derived_message[:3] + ".*")) | ||
| @retry( | ||
| retry=tenacity.retry_if_not_exception_message( | ||
| match=NoCustomErrorAfterCount.derived_message[:3] + ".*" | ||
| ) | ||
| ) | ||
| def _retryable_test_if_not_exception_message_match(thing): | ||
@@ -925,3 +1005,7 @@ return thing.go() | ||
| @retry(retry=tenacity.retry_if_not_exception_message(message=NameErrorUntilCount.derived_message)) | ||
| @retry( | ||
| retry=tenacity.retry_if_not_exception_message( | ||
| message=NameErrorUntilCount.derived_message | ||
| ) | ||
| ) | ||
| def _retryable_test_not_exception_message_delay(thing): | ||
@@ -976,3 +1060,3 @@ return thing.go() | ||
| self.fail("Expected IOError") | ||
| except IOError as re: | ||
| except OSError as re: | ||
| self.assertTrue(isinstance(re, IOError)) | ||
@@ -991,3 +1075,5 @@ print(re) | ||
| self.assertTrue(_retryable_test_with_exception_type_custom(NoCustomErrorAfterCount(5))) | ||
| self.assertTrue( | ||
| _retryable_test_with_exception_type_custom(NoCustomErrorAfterCount(5)) | ||
| ) | ||
@@ -1002,3 +1088,5 @@ try: | ||
| def test_retry_except_exception_of_type(self): | ||
| self.assertTrue(_retryable_test_if_not_exception_type_io(NoNameErrorAfterCount(5))) | ||
| self.assertTrue( | ||
| _retryable_test_if_not_exception_type_io(NoNameErrorAfterCount(5)) | ||
| ) | ||
@@ -1008,3 +1096,3 @@ try: | ||
| self.fail("Expected IOError") | ||
| except IOError as err: | ||
| except OSError as err: | ||
| self.assertTrue(isinstance(err, IOError)) | ||
@@ -1015,3 +1103,5 @@ print(err) | ||
| try: | ||
| self.assertTrue(_retryable_test_with_unless_exception_type_name(NameErrorUntilCount(5))) | ||
| self.assertTrue( | ||
| _retryable_test_with_unless_exception_type_name(NameErrorUntilCount(5)) | ||
| ) | ||
| except NameError as e: | ||
@@ -1027,3 +1117,7 @@ s = _retryable_test_with_unless_exception_type_name.retry.statistics | ||
| # no input should catch all subclasses of Exception | ||
| self.assertTrue(_retryable_test_with_unless_exception_type_no_input(NameErrorUntilCount(5))) | ||
| self.assertTrue( | ||
| _retryable_test_with_unless_exception_type_no_input( | ||
| NameErrorUntilCount(5) | ||
| ) | ||
| ) | ||
| except NameError as e: | ||
@@ -1039,3 +1133,5 @@ s = _retryable_test_with_unless_exception_type_no_input.retry.statistics | ||
| # two iterations with IOError, one that returns True | ||
| _retryable_test_with_unless_exception_type_name_attempt_limit(IOErrorUntilCount(2)) | ||
| _retryable_test_with_unless_exception_type_name_attempt_limit( | ||
| IOErrorUntilCount(2) | ||
| ) | ||
| self.fail("Expected RetryError") | ||
@@ -1048,3 +1144,5 @@ except RetryError as e: | ||
| try: | ||
| self.assertTrue(_retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3))) | ||
| self.assertTrue( | ||
| _retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3)) | ||
| ) | ||
| except CustomError: | ||
@@ -1056,3 +1154,7 @@ print(_retryable_test_if_exception_message_message.retry.statistics) | ||
| try: | ||
| self.assertTrue(_retryable_test_if_not_exception_message_message(NoCustomErrorAfterCount(2))) | ||
| self.assertTrue( | ||
| _retryable_test_if_not_exception_message_message( | ||
| NoCustomErrorAfterCount(2) | ||
| ) | ||
| ) | ||
| except CustomError: | ||
@@ -1064,3 +1166,5 @@ s = _retryable_test_if_not_exception_message_message.retry.statistics | ||
| try: | ||
| self.assertTrue(_retryable_test_not_exception_message_delay(NameErrorUntilCount(3))) | ||
| self.assertTrue( | ||
| _retryable_test_not_exception_message_delay(NameErrorUntilCount(3)) | ||
| ) | ||
| except NameError: | ||
@@ -1073,3 +1177,5 @@ s = _retryable_test_not_exception_message_delay.retry.statistics | ||
| try: | ||
| self.assertTrue(_retryable_test_if_exception_message_match(NoCustomErrorAfterCount(3))) | ||
| self.assertTrue( | ||
| _retryable_test_if_exception_message_match(NoCustomErrorAfterCount(3)) | ||
| ) | ||
| except CustomError: | ||
@@ -1080,3 +1186,7 @@ self.fail("CustomError should've been retried from errormessage") | ||
| try: | ||
| self.assertTrue(_retryable_test_if_not_exception_message_message(NoCustomErrorAfterCount(2))) | ||
| self.assertTrue( | ||
| _retryable_test_if_not_exception_message_message( | ||
| NoCustomErrorAfterCount(2) | ||
| ) | ||
| ) | ||
| except CustomError: | ||
@@ -1087,3 +1197,5 @@ s = _retryable_test_if_not_exception_message_message.retry.statistics | ||
| def test_retry_if_exception_cause_type(self): | ||
| self.assertTrue(_retryable_test_with_exception_cause_type(NoNameErrorCauseAfterCount(5))) | ||
| self.assertTrue( | ||
| _retryable_test_with_exception_cause_type(NoNameErrorCauseAfterCount(5)) | ||
| ) | ||
@@ -1096,2 +1208,23 @@ try: | ||
| def test_retry_preserves_argument_defaults(self): | ||
| def function_with_defaults(a=1): | ||
| return a | ||
| def function_with_kwdefaults(*, a=1): | ||
| return a | ||
| retrying = Retrying( | ||
| wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3) | ||
| ) | ||
| wrapped_defaults_function = retrying.wraps(function_with_defaults) | ||
| wrapped_kwdefaults_function = retrying.wraps(function_with_kwdefaults) | ||
| self.assertEqual( | ||
| function_with_defaults.__defaults__, wrapped_defaults_function.__defaults__ | ||
| ) | ||
| self.assertEqual( | ||
| function_with_kwdefaults.__kwdefaults__, | ||
| wrapped_kwdefaults_function.__kwdefaults__, | ||
| ) | ||
| def test_defaults(self): | ||
@@ -1114,3 +1247,5 @@ self.assertTrue(_retryable_default(NoNameErrorAfterCount(5))) | ||
| retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3)) | ||
| retrying = Retrying( | ||
| wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3) | ||
| ) | ||
| h = retrying.wraps(Hello()) | ||
@@ -1123,3 +1258,5 @@ self.assertEqual(h(), "Hello") | ||
| start = current_time_ms() | ||
| result = _retryable_test_with_wait.retry_with(wait=tenacity.wait_fixed(0.1))(NoneReturnUntilAfterCount(5)) | ||
| result = _retryable_test_with_wait.retry_with(wait=tenacity.wait_fixed(0.1))( | ||
| NoneReturnUntilAfterCount(5) | ||
| ) | ||
| t = current_time_ms() - start | ||
@@ -1130,3 +1267,5 @@ assert t >= 500 | ||
| def test_redefine_stop(self): | ||
| result = _retryable_test_with_stop.retry_with(stop=tenacity.stop_after_attempt(5))(NoneReturnUntilAfterCount(4)) | ||
| result = _retryable_test_with_stop.retry_with( | ||
| stop=tenacity.stop_after_attempt(5) | ||
| )(NoneReturnUntilAfterCount(4)) | ||
| assert result is True | ||
@@ -1146,3 +1285,3 @@ | ||
| def return_text(retry_state): | ||
| return "Calling %s keeps raising errors after %s attempts" % ( | ||
| return "Calling {} keeps raising errors after {} attempts".format( | ||
| retry_state.fn.__name__, | ||
@@ -1237,3 +1376,6 @@ retry_state.attempt_number, | ||
| etalon_re = r"^Retrying .* in 0\.01 seconds as it raised " r"(IO|OS)Error: Hi there, I'm an IOError\.$" | ||
| etalon_re = ( | ||
| r"^Retrying .* in 0\.01 seconds as it raised " | ||
| r"(IO|OS)Error: Hi there, I'm an IOError\.$" | ||
| ) | ||
| self.assertEqual(len(handler.records), 2) | ||
@@ -1255,3 +1397,5 @@ fmt = logging.Formatter().format | ||
| try: | ||
| _before_sleep = tenacity.before_sleep_log(logger, logging.INFO, exc_info=True) | ||
| _before_sleep = tenacity.before_sleep_log( | ||
| logger, logging.INFO, exc_info=True | ||
| ) | ||
| retrying = Retrying( | ||
@@ -1286,3 +1430,5 @@ wait=tenacity.wait_fixed(0.01), | ||
| try: | ||
| _before_sleep = tenacity.before_sleep_log(logger, logging.INFO, exc_info=exc_info) | ||
| _before_sleep = tenacity.before_sleep_log( | ||
| logger, logging.INFO, exc_info=exc_info | ||
| ) | ||
| _retry = tenacity.retry_if_result(lambda result: result is None) | ||
@@ -1570,3 +1716,5 @@ retrying = Retrying( | ||
| class TestRetryTyping(unittest.TestCase): | ||
| @pytest.mark.skipif(sys.version_info < (3, 0), reason="typeguard not supported for python 2") | ||
| @pytest.mark.skipif( | ||
| sys.version_info < (3, 0), reason="typeguard not supported for python 2" | ||
| ) | ||
| def test_retry_type_annotations(self): | ||
@@ -1573,0 +1721,0 @@ """The decorator should maintain types of decorated functions.""" |
@@ -1,2 +0,2 @@ | ||
| # coding: utf-8 | ||
| # mypy: disable-error-code="no-untyped-def,no-untyped-call" | ||
| # Copyright 2017 Elisey Zanko | ||
@@ -41,3 +41,3 @@ # | ||
| class TestTornado(testing.AsyncTestCase): | ||
| class TestTornado(testing.AsyncTestCase): # type: ignore[misc] | ||
| @testing.gen_test | ||
@@ -44,0 +44,0 @@ def test_retry(self): |
+11
-35
| [tox] | ||
| envlist = py3{7,8,9,10,11}, pep8, pypy3 | ||
| envlist = py3{8,9,10,11,12}, pep8, pypy3 | ||
| skip_missing_interpreters = True | ||
@@ -9,26 +9,15 @@ | ||
| deps = | ||
| .[test] | ||
| .[doc] | ||
| pytest | ||
| typeguard | ||
| commands = | ||
| py3{7,8,9,10,11},pypy3: pytest {posargs} | ||
| py3{7,8,9,10,11},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build | ||
| py3{7,8,9,10,11},pypy3: sphinx-build -a -E -W -b html doc/source doc/build | ||
| py3{8,9,10,11,12},pypy3: pytest {posargs} | ||
| py3{8,9,10,11,12},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build | ||
| py3{8,9,10,11,12},pypy3: sphinx-build -a -E -W -b html doc/source doc/build | ||
| [testenv:pep8] | ||
| basepython = python3 | ||
| deps = flake8 | ||
| flake8-import-order | ||
| flake8-blind-except | ||
| flake8-builtins | ||
| flake8-docstrings | ||
| flake8-rst-docstrings | ||
| flake8-logging-format | ||
| commands = flake8 | ||
| [testenv:black] | ||
| deps = | ||
| black | ||
| deps = ruff | ||
| commands = | ||
| black . | ||
| ruff check . {posargs} | ||
| ruff format --check . {posargs} | ||
@@ -38,22 +27,9 @@ [testenv:mypy] | ||
| mypy>=1.0.0 | ||
| pytest # for stubs | ||
| commands = | ||
| mypy tenacity | ||
| mypy {posargs} | ||
| [testenv:black-ci] | ||
| deps = | ||
| black | ||
| {[testenv:black]deps} | ||
| commands = | ||
| black --check --diff . | ||
| [testenv:reno] | ||
| basepython = python3 | ||
| deps = reno | ||
| commands = reno {posargs} | ||
| [flake8] | ||
| exclude = .tox,.eggs | ||
| show-source = true | ||
| ignore = D100,D101,D102,D103,D104,D105,D107,G200,G201,W503,W504,E501 | ||
| enable-extensions=G | ||
| max-line-length = 120 | ||
| commands = reno {posargs} |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
197001
6.62%75
8.7%3245
14.02%