Repository: jd/tenacity Branch: main Commit: 1106b9a168c5 Files: 81 Total size: 221.6 KB Directory structure: gitextract_hx8yszl8/ ├── .editorconfig ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yaml │ └── release.yml ├── .gitignore ├── .mergify.yml ├── .readthedocs.yml ├── LICENSE ├── doc/ │ └── source/ │ ├── api.rst │ ├── changelog.rst │ ├── conf.py │ └── index.rst ├── pyproject.toml ├── releasenotes/ │ └── notes/ │ ├── Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml │ ├── Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml │ ├── add-async-actions-b249c527d99723bb.yaml │ ├── add-re-pattern-to-match-types-6a4c1d9e64e2a5e1.yaml │ ├── add-reno-d1ab5710f272650a.yaml │ ├── add-retry_except_exception_type-31b31da1924d55f4.yaml │ ├── add-stop-before-delay-a775f88ac872c923.yaml │ ├── add-test-extra-55e869261b03e56d.yaml │ ├── add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml │ ├── add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml │ ├── added_a_link_to_documentation-eefaf8f074b539f8.yaml │ ├── after_log-50f4d73b24ce9203.yaml │ ├── allow-mocking-of-nap-sleep-6679c50e702446f1.yaml │ ├── annotate_code-197b93130df14042.yaml │ ├── async-sleep-retrying-32de5866f5d041.yaml │ ├── before_sleep_log-improvements-d8149274dfb37d7c.yaml │ ├── clarify-reraise-option-6829667eacf4f599.yaml │ ├── dependabot-for-github-actions-4d2464f3c0928463.yaml │ ├── deprecate-initial-for-multiplier-c7b4e2d9f1a83065.yaml │ ├── do_not_package_tests-fe5ac61940b0a5ed.yaml │ ├── drop-deprecated-python-versions-69a05cb2e0f1034c.yaml │ ├── drop-python-3.9-ecfa2d7db9773e96.yaml │ ├── drop_deprecated-7ea90b212509b082.yaml │ ├── export-convenience-symbols-981d9611c8b754f3.yaml │ ├── fix-async-loop-with-result-f68e913ccb425aca.yaml │ ├── fix-async-retry-type-overloads-27f3e0c239ed6b.yaml │ ├── fix-local-context-overwrite-94190ba06a481631.yaml │ ├── fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml │ ├── fix-setuptools-config-3af71aa3592b6948.yaml │ ├── fix-wait-typing-b26eecdb6cc0a1de.yaml │ ├── fix_async-52b6594c8e75c4bc.yaml │ ├── logging-protocol-a4cf0f786f21e4ee.yaml │ ├── make-logger-more-compatible-5da1ddf1bab77047.yaml │ ├── no-async-iter-6132a42e52348a75.yaml │ ├── pr320-py3-only-wheel-tag.yaml │ ├── py36_plus-c425fb3aa17c6682.yaml │ ├── remove-py36-876c0416cf279d15.yaml │ ├── retrycallstate-repr-94947f7b00ee15e1.yaml │ ├── some-slug-for-preserve-defaults-86682846dfa18005.yaml │ ├── sphinx_define_error-642c9cd5c165d39a.yaml │ ├── support-py3.14-14928188cab53b99.yaml │ ├── support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml │ ├── timedelta-for-stop-ef6bf71b88ce9988.yaml │ ├── trio-support-retry-22bd544800cd1f36.yaml │ ├── wait-exponential-jitter-min-timedelta-a8e3c1f4b7d29e50.yaml │ ├── wait-random-exponential-min-2a4b7eed9f002436.yaml │ └── wait_exponential_jitter-6ffc81dddcbaa6d3.yaml ├── reno.yaml ├── tenacity/ │ ├── __init__.py │ ├── _utils.py │ ├── after.py │ ├── asyncio/ │ │ ├── __init__.py │ │ └── retry.py │ ├── before.py │ ├── before_sleep.py │ ├── nap.py │ ├── py.typed │ ├── retry.py │ ├── stop.py │ ├── tornadoweb.py │ └── wait.py └── tests/ ├── __init__.py ├── test_after.py ├── test_asyncio.py ├── test_issue_478.py ├── test_tenacity.py ├── test_tornado.py └── test_utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.{py,pyx,pxd,pyi}] indent_size = 4 max_line_length = 120 [*.ini] indent_size = 4 [*.rst] max_line_length = 150 [Makefile] indent_style = tab ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'monthly' groups: github-actions: patterns: - '*' ================================================ FILE: .github/workflows/ci.yaml ================================================ name: Continuous Integration permissions: read-all on: pull_request: branches: - main concurrency: # yamllint disable-line rule:line-length group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}" cancel-in-progress: true jobs: test: timeout-minutes: 20 runs-on: ubuntu-24.04 strategy: matrix: include: - python: "3.10" task: check - python: "3.11" task: check - python: "3.12" task: check - python: "3.13" task: check - python: "3.14" task: check - python: "3.14" task: lint - python: "3.14" task: mypy steps: - name: Checkout 🛎️ uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Setup uv 🔧 uses: astral-sh/setup-uv@v7 - name: Build 🔧 & Test 🔍 run: uv run --python ${{ matrix.python }} poe ${{ matrix.task }} ================================================ FILE: .github/workflows/release.yml ================================================ name: upload release to PyPI on: release: types: - published jobs: pypi-publish: name: upload release to PyPI runs-on: ubuntu-24.04 environment: release permissions: id-token: write steps: - uses: actions/checkout@v6.0.2 with: fetch-depth: 0 fetch-tags: true - uses: astral-sh/setup-uv@v7 - name: Build run: uv build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 ================================================ FILE: .gitignore ================================================ .idea dist *.pyc *.egg-info build .venv/ uv.lock AUTHORS ChangeLog doc/_build tenacity/_version.py /.pytest_cache ================================================ FILE: .mergify.yml ================================================ queue_rules: - name: default merge_method: squash autoqueue: true queue_conditions: - or: - author = jd - "#approved-reviews-by >= 1" - author = dependabot[bot] - "check-success=test (3.10, check)" - "check-success=test (3.11, check)" - "check-success=test (3.12, check)" - "check-success=test (3.13, check)" - "check-success=test (3.14, check)" - "check-success=test (3.14, lint)" - "check-success=test (3.14, mypy)" pull_request_rules: - name: dismiss reviews conditions: [] actions: dismiss_reviews: {} ================================================ FILE: .readthedocs.yml ================================================ version: 2 build: os: ubuntu-24.04 tools: python: "3.13" python: install: - method: pip path: . extra_requirements: - doc ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: doc/source/api.rst ================================================ =============== API Reference =============== Retry Main API -------------- .. autofunction:: tenacity.retry :noindex: .. autoclass:: tenacity.Retrying :members: .. autoclass:: tenacity.AsyncRetrying :members: .. autoclass:: tenacity.tornadoweb.TornadoRetrying :members: .. autoclass:: tenacity.RetryCallState :members: After Functions --------------- Those functions can be used as the `after` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.after :members: Before Functions ---------------- Those functions can be used as the `before` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.before :members: Before Sleep Functions ---------------------- Those functions can be used as the `before_sleep` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.before_sleep :members: Nap Functions ------------- Those functions can be used as the `sleep` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.nap :members: Retry Functions --------------- Those functions can be used as the `retry` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.retry :members: Stop Functions -------------- Those functions can be used as the `stop` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.stop :members: Wait Functions -------------- Those functions can be used as the `wait` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.wait :members: ================================================ FILE: doc/source/changelog.rst ================================================ Changelog ========= .. release-notes:: ================================================ FILE: doc/source/conf.py ================================================ # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from importlib.metadata import version as pkg_version master_doc = "index" project = "Tenacity" release = pkg_version("tenacity") version = ".".join(release.split(".")[:2]) extensions = [ "sphinx.ext.doctest", "sphinx.ext.autodoc", "reno.sphinxext", ] ================================================ FILE: doc/source/index.rst ================================================ Tenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.org/project/tenacity .. image:: https://img.shields.io/pypi/pyversions/tenacity.svg :target: https://pypi.org/project/tenacity .. image:: https://github.com/jd/tenacity/actions/workflows/ci.yaml/badge.svg?branch=main :target: https://github.com/jd/tenacity/actions/workflows/ci.yaml .. image:: https://img.shields.io/endpoint.svg?url=https://api.mergify.com/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status **Please refer to the** `tenacity documentation `_ **for a better experience.** Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything. It originates from `a fork of retrying `_ which is sadly no longer `maintained `_. Tenacity isn't api compatible with retrying but adds significant new functionality and fixes a number of longstanding bugs. The simplest use case is retrying a flaky function whenever an `Exception` occurs until a value is returned. .. testcode:: import random from tenacity import retry @retry def do_something_unreliable(): if random.randint(0, 10) > 1: raise IOError("Broken sauce, everything is hosed!!!111one") else: return "Awesome sauce!" print(do_something_unreliable()) .. testoutput:: :hide: Awesome sauce! .. toctree:: :hidden: :maxdepth: 2 changelog api Features -------- - Generic Decorator API - Specify stop condition (i.e. limit by number of attempts) - Specify wait condition (i.e. exponential backoff sleeping between attempts) - Customize retrying on Exceptions - Customize retrying on expected returned result - Retry on coroutines - Retry code block with context manager Installation ------------ To install *tenacity*, simply: .. code-block:: bash $ pip install tenacity Examples ---------- Basic Retry ~~~~~~~~~~~ .. testsetup:: import logging # # Note the following import is used for demonstration convenience only. # Production code should always explicitly import the names it needs. # from tenacity import * class MyException(Exception): pass As you saw above, the default behavior is to retry forever without waiting when an exception is raised. .. testcode:: @retry def never_gonna_give_you_up(): print("Retry forever ignoring Exceptions, don't wait between retries") raise Exception Stopping ~~~~~~~~ Let's be a little less persistent and set some boundaries, such as the number of attempts before giving up. .. testcode:: @retry(stop=stop_after_attempt(7)) def stop_after_7_attempts(): print("Stopping after 7 attempts") raise Exception We don't have all day, so let's set a boundary for how long we should be retrying stuff. .. testcode:: @retry(stop=stop_after_delay(10)) def stop_after_10_s(): print("Stopping after 10 seconds") raise Exception 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: .. testcode:: @retry(stop=(stop_after_delay(10) | stop_after_attempt(5))) def stop_after_10_s_or_5_retries(): print("Stopping after 10 seconds or 5 retries") raise Exception Waiting before retrying ~~~~~~~~~~~~~~~~~~~~~~~ Most things don't like to be polled as fast as possible, so let's just wait 2 seconds between retries. .. testcode:: @retry(wait=wait_fixed(2)) def wait_2_s(): print("Wait 2 second between retries") raise Exception Some things perform best with a bit of randomness injected. .. testcode:: @retry(wait=wait_random(min=1, max=2)) def wait_random_1_to_2_s(): print("Randomly wait 1 to 2 seconds between retries") raise Exception Then again, it's hard to beat exponential backoff when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_exponential(multiplier=1, min=4, max=10)) def wait_exponential_1(): print("Wait 2^x * 1 second between each retry starting with 4 seconds, then up to 10 seconds, then 10 seconds afterwards") raise Exception Then again, it's also hard to beat combining fixed waits and jitter (to help avoid thundering herds) when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_fixed(3) + wait_random(0, 2)) def wait_fixed_jitter(): print("Wait at least 3 seconds, and add up to 2 seconds of random delay") raise Exception When multiple processes are in contention for a shared resource, exponentially increasing jitter helps minimise collisions. .. testcode:: @retry(wait=wait_random_exponential(multiplier=1, max=60)) def wait_exponential_jitter(): print("Randomly wait up to 2^x * 1 seconds between each retry until the range reaches 60 seconds, then randomly up to 60 seconds afterwards") raise Exception Sometimes it's necessary to build a chain of backoffs. .. testcode:: @retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] + [wait_fixed(7) for i in range(2)] + [wait_fixed(9)])) def wait_fixed_chained(): print("Wait 3s for 3 attempts, 7s for the next 2 attempts and 9s for all attempts thereafter") raise Exception Whether to retry ~~~~~~~~~~~~~~~~ We have a few options for dealing with retries that raise specific or general exceptions, as in the cases here. .. testcode:: class ClientError(Exception): """Some type of client error.""" @retry(retry=retry_if_exception_type(IOError)) def might_io_error(): print("Retry forever with no wait if an IOError occurs, raise any other errors") raise Exception @retry(retry=retry_if_not_exception_type(ClientError)) def might_client_error(): print("Retry forever with no wait if any error other than ClientError occurs. Immediately raise ClientError.") raise Exception We can also use the result of the function to alter the behavior of retrying. .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=retry_if_result(is_none_p)) def might_return_none(): print("Retry with no wait if return value is None") See also these methods: .. testcode:: retry_if_exception retry_if_exception_type retry_if_not_exception_type retry_unless_exception_type retry_if_result retry_if_not_result retry_if_exception_message retry_if_not_exception_message retry_any retry_all We can also combine several conditions: .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=(retry_if_result(is_none_p) | retry_if_exception_type())) def might_return_none(): print("Retry forever ignoring Exceptions with no wait if return value is None") Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. It's also possible to retry explicitly at any time by raising the `TryAgain` exception: .. testcode:: @retry def do_something(): result = something_else() if result == 23: raise TryAgain Error Handling ~~~~~~~~~~~~~~ Normally when your function fails its final time (and will not be retried again based on your settings), a `RetryError` is raised. The exception your code encountered will be shown somewhere in the *middle* of the stack trace. If you would rather see the exception your code encountered at the *end* of the stack trace (where it is most visible), you can set `reraise=True`. .. testcode:: @retry(reraise=True, stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except MyException: # timed out retrying pass Before and After Retry, and Logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's possible to execute an action before any attempt of calling the function by using the before callback function: .. testcode:: import logging import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before=before_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") In the same spirit, It's possible to execute after a call that failed: .. testcode:: import logging import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), after=after_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") It's also possible to only log failures that are going to be retried. Normally retries happen after a wait interval, so the keyword argument is called ``before_sleep``: .. testcode:: import logging import sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before_sleep=before_sleep_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") Statistics ~~~~~~~~~~ You can access the statistics about the retry made over a function by using the `statistics` attribute attached to the function: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except Exception: pass print(raise_my_exception.statistics) .. testoutput:: :hide: ... Custom Callbacks ~~~~~~~~~~~~~~~~ You can also define your own callbacks. The callback should accept one parameter called ``retry_state`` that contains all information about current retry invocation. For example, you can call a custom callback function after all retries failed, without raising an exception (or you can re-raise or do anything really) .. testcode:: def return_last_value(retry_state): """return the result of the last call attempt""" return retry_state.outcome.result() def is_false(value): """Return True if value is False""" return value is False # will return False after trying 3 times to get a different result @retry(stop=stop_after_attempt(3), retry_error_callback=return_last_value, retry=retry_if_result(is_false)) def eventually_return_false(): return False RetryCallState ~~~~~~~~~~~~~~ ``retry_state`` argument is an object of :class:`~tenacity.RetryCallState` class. Its most useful attributes are: * ``attempt_number`` — number of the current attempt (starts at 1) * ``outcome`` — a :class:`concurrent.futures.Future` holding the last result or exception * ``seconds_since_start`` — total elapsed seconds from the first attempt to the last outcome (``None`` if no outcome yet) * ``idle_for`` — cumulative seconds spent sleeping between attempts * ``start_time`` — :func:`time.monotonic` timestamp of the first attempt For example, to log the total elapsed time after all retries: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) def log_elapsed(retry_state): logger.info('Finished after %.3fs', retry_state.seconds_since_start) @retry(stop=stop_after_attempt(3), after=log_elapsed) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except RetryError: pass Other Custom Callbacks ~~~~~~~~~~~~~~~~~~~~~~ It's also possible to define custom callbacks for other keyword arguments. .. function:: my_stop(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: whether or not retrying should stop :rtype: bool .. function:: my_wait(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: whether or not retrying should continue :rtype: bool .. function:: my_before(retry_state) :param RetryCallState retry_state: info about current retry invocation .. function:: my_after(retry_state) :param RetryCallState retry_state: info about current retry invocation .. function:: my_before_sleep(retry_state) :param RetryCallState retry_state: info about current retry invocation Here's an example with a custom ``before_sleep`` function: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) def my_before_sleep(retry_state): if retry_state.attempt_number < 1: loglevel = logging.INFO else: loglevel = logging.WARNING logger.log( loglevel, 'Retrying %s: attempt %s ended with: %s', retry_state.fn, retry_state.attempt_number, retry_state.outcome) @retry(stop=stop_after_attempt(3), before_sleep=my_before_sleep) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except RetryError: pass Common Patterns ~~~~~~~~~~~~~~~ **Running setup code between retries** (e.g. reconnecting): .. testcode:: def reconnect(retry_state): print("Reconnecting before next attempt...") @retry(stop=stop_after_attempt(3), before_sleep=reconnect) def send_data(): raise MyException("connection lost") try: send_data() except RetryError: pass .. testoutput:: :hide: ... The ``before_sleep`` callback runs after a failed attempt and before sleeping, making it ideal for re-establishing connections, refreshing tokens, or any other setup that needs to happen before the next attempt. **Accessing the attempt number inside the function** using the iterator API: .. testcode:: from tenacity import Retrying for attempt in Retrying(stop=stop_after_attempt(3)): with attempt: print(f"Attempt {attempt.retry_state.attempt_number}") if attempt.retry_state.attempt_number < 3: raise MyException("not yet") .. testoutput:: :hide: ... Changing Arguments at Run Time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can change the arguments of a retry decorator as needed when calling it by using the `retry_with` function attached to the wrapped function: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception.retry_with(stop=stop_after_attempt(4))() except Exception: pass print(raise_my_exception.statistics) .. testoutput:: :hide: ... If you want to use variables to set up the retry parameters, you don't have to use the `retry` decorator - you can instead use `Retrying` directly: .. testcode:: def never_good_enough(arg1): raise Exception('Invalid argument: {}'.format(arg1)) def try_never_good_enough(max_attempts=3): retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True) retryer(never_good_enough, 'I really do try') You may also want to change the behaviour of a decorated function temporarily, like in tests to avoid unnecessary wait times. You can modify/patch the `retry` attribute attached to the function. Bear in mind this is a write-only attribute, statistics should be read from the function `statistics` attribute. .. testcode:: @retry(stop=stop_after_attempt(3), wait=wait_fixed(3)) def raise_my_exception(): raise MyException("Fail") from unittest import mock with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)): try: raise_my_exception() except Exception: pass print(raise_my_exception.statistics) .. testoutput:: :hide: ... Disabling Retries ~~~~~~~~~~~~~~~~~ You can disable retrying entirely by passing ``enabled=False``. When disabled, the decorated function is called directly without any retry logic. This is useful during development or testing when you want fast feedback on failures: .. testcode:: import os @retry( enabled=os.getenv("ENABLE_RETRIES", "1") != "0", stop=stop_after_attempt(5), wait=wait_fixed(1), ) def call_api(): pass # your code here call_api() You can also use ``retry_with`` to disable retries on a per-call basis: .. testcode:: @retry(stop=stop_after_attempt(5)) def call_api(): pass # your code here # In tests: call_api.retry_with(enabled=False)() Retrying code block ~~~~~~~~~~~~~~~~~~~ Tenacity allows you to retry a code block without the need to wraps it in an isolated function. This makes it easy to isolate failing block while sharing context. The trick is to combine a for loop and a context manager. .. testcode:: from tenacity import Retrying, RetryError, stop_after_attempt try: for attempt in Retrying(stop=stop_after_attempt(3)): with attempt: raise Exception('My code is failing!') except RetryError: pass You can configure every details of retry policy by configuring the Retrying object. With async code you can use AsyncRetrying. .. testcode:: from tenacity import AsyncRetrying, RetryError, stop_after_attempt async def function(): try: async for attempt in AsyncRetrying(stop=stop_after_attempt(3)): with attempt: raise Exception('My code is failing!') except RetryError: pass In both cases, you may want to set the result to the attempt so it's available in retry strategies like ``retry_if_result``. This can be done accessing the ``retry_state`` property: .. testcode:: from tenacity import AsyncRetrying, retry_if_result async def function(): async for attempt in AsyncRetrying(retry=retry_if_result(lambda x: x < 3)): with attempt: result = 1 # Some complex calculation, function call, etc. if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(result) return result Async and retry ~~~~~~~~~~~~~~~ Finally, ``retry`` works also on asyncio, Trio, and Tornado coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_asyncio_function(): await asyncio.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry async def my_async_trio_function(): await trio.socket.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry async def my_async_tornado_function(http_client, url): await http_client.fetch(url) You can use alternative event loops by passing the correct sleep function: .. code-block:: python @retry(sleep=trio.sleep) async def my_async_trio_function_with_sleep(): ... Generators ~~~~~~~~~~ ``retry`` does not support generator or async generator functions. Decorating a generator with ``@retry`` will not retry on exceptions raised during iteration — the decorator wraps the function call itself, which for generators simply returns a generator object without executing any of the body. Also note that generators passed *as arguments* to a retried function will be exhausted after the first attempt and will not be rewound automatically on retry. If you need to pass a generator as an argument, consider passing a factory function instead: .. code-block:: python # Bad: generator will be exhausted after the first attempt @retry def process(items): for item in items: do_work(item) process(my_generator()) # retries will see an empty generator # Good: pass a factory so a fresh generator is created on each attempt @retry def process(items_factory): for item in items_factory(): do_work(item) process(my_generator) # each retry gets a fresh generator Contribute ---------- #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. #. Fork `the repository`_ on GitHub to start making your changes to the **main** branch (or branch off of it). #. Write a test which shows that the bug was fixed or that the feature works as expected. #. Add a `changelog <#Changelogs>`_ #. Make the docs better (or more detailed, or more easier to read, or ...) Running the test suite locally:: uv run poe check # run tests + build docs uv run poe lint # run ruff linter uv run poe mypy # run type checker uv run poe all # run everything .. _`the repository`: https://github.com/jd/tenacity ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] version-file = "tenacity/_version.py" [project] name = "tenacity" dynamic = ["version"] description = "Retry code until it succeeds" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.10" authors = [ { name = "Julien Danjou", email = "julien@danjou.info" }, ] classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Utilities", ] [project.urls] Homepage = "https://github.com/jd/tenacity" Documentation = "https://tenacity.readthedocs.io" Source = "https://github.com/jd/tenacity" Issues = "https://github.com/jd/tenacity/issues" Changelog = "https://tenacity.readthedocs.io/en/latest/changelog.html" [project.optional-dependencies] doc = [ "reno", "sphinx", ] test = [ "pytest", "tornado>=6.0", "typeguard", ] [dependency-groups] dev = [ "poethepoet", "pytest", "tornado>=6.0", "typeguard", "ruff", "mypy", "sphinx", "reno", "trio", ] [tool.poe.tasks] test = "pytest" docs-doctest = "sphinx-build -a -E -W -b doctest doc/source doc/build" docs-html = "sphinx-build -a -E -W -b html doc/source doc/build" docs = ["docs-doctest", "docs-html"] check = ["test", "docs"] mypy = "mypy" reno = "reno" [tool.poe.tasks.fmt] sequence = [ { cmd = "ruff check --fix ." }, { cmd = "ruff format ." }, ] [tool.poe.tasks.lint] sequence = [ { cmd = "ruff check ." }, { cmd = "ruff format --check ." }, ] [tool.poe.tasks.all] sequence = [ { ref = "lint" }, { ref = "mypy" }, { ref = "check" }, ] [tool.pytest.ini_options] filterwarnings = [ "once::DeprecationWarning", ] [tool.ruff] line-length = 88 indent-width = 4 target-version = "py310" exclude = ["tenacity/_version.py"] [tool.ruff.lint] select = ["ASYNC", "B", "C4", "DTZ", "E", "EXE", "F", "FLY", "FURB", "I", "ICN", "ISC", "LOG", "PERF", "PGH", "PIE", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "SLOT", "T10", "TC", "UP", "W"] ignore = [ "B008", # function calls in default arguments (intentional API design) "B905", # zip() without strict= (not needed in existing code) "E501", # line too long (formatter handles what it can) "PYI036", # false positive on string-quoted __exit__ annotations "RUF003", # ambiguous unicode characters in comments (copyright names) "RUF005", # iterable unpacking vs concatenation (less readable for tuple +) "RUF012", # mutable class default (test constant, never mutated) "SIM108", # ternary instead of if-else (less readable in context) ] [tool.mypy] strict = true files = ["tenacity", "tests"] show_error_codes = true exclude = ["tenacity/_version\\.py"] [[tool.mypy.overrides]] module = "tornado.*" ignore_missing_imports = true ================================================ FILE: releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml ================================================ --- fixes: - "Fixes test failures with typeguard 3.x" ================================================ FILE: releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml ================================================ --- other: - "Use `black` for code formatting and validate using `black --check`. Code compatibility: py26-py39." - "Enforce maximal line length to 120 symbols" ================================================ FILE: releasenotes/notes/add-async-actions-b249c527d99723bb.yaml ================================================ --- features: - | Added the ability to use async functions for retries. This way, you can now use asyncio coroutines for retry strategy predicates. ================================================ FILE: releasenotes/notes/add-re-pattern-to-match-types-6a4c1d9e64e2a5e1.yaml ================================================ --- fixes: - | Added `re.Pattern` to allowed match types. ================================================ FILE: releasenotes/notes/add-reno-d1ab5710f272650a.yaml ================================================ --- features: - Add reno (changelog system) ================================================ FILE: releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml ================================================ --- features: - Add ``retry_if_not_exception_type()`` that allows to retry if a raised exception doesn't match given exceptions. ================================================ FILE: releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml ================================================ --- 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. ================================================ FILE: releasenotes/notes/add-test-extra-55e869261b03e56d.yaml ================================================ --- other: - Add a \"test\" extra ================================================ FILE: releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml ================================================ --- other: - Add `retry_if_exception_cause_type`and `wait_exponential_jitter` to __all__ of init.py ================================================ FILE: releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml ================================================ --- features: - | Add a new `retry_base` class called `retry_if_exception_cause_type` that checks, recursively, if any of the causes of the raised exception is of a certain type. ================================================ FILE: releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml ================================================ --- other: - | Added a link to the documentation, as code snippets are not being rendered properly Changed branch name to main in index.rst ================================================ FILE: releasenotes/notes/after_log-50f4d73b24ce9203.yaml ================================================ --- fixes: - "Fix after_log logger format: function name was used with delay formatting." ================================================ FILE: releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml ================================================ --- other: - Unit tests can now mock ``nap.sleep()`` for testing in all tenacity usage styles ================================================ FILE: releasenotes/notes/annotate_code-197b93130df14042.yaml ================================================ --- other: - Add type annotations to cover all public API. ================================================ FILE: releasenotes/notes/async-sleep-retrying-32de5866f5d041.yaml ================================================ --- fixes: - | Passing an async ``sleep`` callable (e.g. ``trio.sleep``) to ``@retry`` now correctly uses ``AsyncRetrying``, even when the decorated function is synchronous. Previously, the async sleep would silently not be awaited, resulting in no delay between retries. ================================================ FILE: releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml ================================================ --- features: - Add an ``exc_info`` option to the ``before_sleep_log()`` strategy. ================================================ FILE: releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml ================================================ --- prelude: > Clarify usage of `reraise` keyword argument ================================================ FILE: releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml ================================================ --- other: - | Add a Dependabot configuration submit PRs monthly (as needed) to keep GitHub action versions updated. ================================================ FILE: releasenotes/notes/deprecate-initial-for-multiplier-c7b4e2d9f1a83065.yaml ================================================ --- deprecations: - | The ``initial`` parameter of ``wait_exponential_jitter`` is deprecated in favor of ``multiplier``, for consistency with ``wait_exponential``. Passing ``initial`` still works but emits a ``DeprecationWarning``. features: - | Add ``multiplier`` parameter to ``wait_exponential_jitter``, consistent with ``wait_exponential``. ================================================ FILE: releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml ================================================ --- other: - Do not package tests with tenacity. ================================================ FILE: releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml ================================================ --- other: - | Drop support for deprecated Python versions (2.7 and 3.5) ================================================ FILE: releasenotes/notes/drop-python-3.9-ecfa2d7db9773e96.yaml ================================================ --- upgrade: - | Python 3.9 has reached end-of-life and is no longer supported. The minimum supported version is now Python 3.10. ================================================ FILE: releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml ================================================ --- upgrade: - "Removed `BaseRetrying.call`: was long time deprecated and produced `DeprecationWarning`" - "Removed `BaseRetrying.fn`: was noted as deprecated" - "API change: `BaseRetrying.begin()` do not require arguments anymore as it not setting `BaseRetrying.fn`" ================================================ FILE: releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml ================================================ --- features: - Explicitly export convenience symbols from tenacity root module ================================================ FILE: releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml ================================================ --- fixes: - | Fix async loop with retrying code block when result is available. ================================================ FILE: releasenotes/notes/fix-async-retry-type-overloads-27f3e0c239ed6b.yaml ================================================ --- fixes: - | The ``@retry`` decorator's type overloads for the ``sleep=`` parameter (e.g. ``sleep=trio.sleep``) have been improved. Previously, the async-sleep overload used ``R | Awaitable[R]`` as the return type bound, which was ambiguous: for ``async def f() -> T``, pyright could infer ``R = Coroutine[Any, Any, T]`` instead of ``R = T``, producing false-positive type errors in downstream code. ================================================ FILE: releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml ================================================ --- fixes: - | Avoid overwriting local contexts when applying the retry decorator. ================================================ FILE: releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml ================================================ --- fixes: - | Restore the value of the `retry` attribute for wrapped functions. Also, clarify that those attributes are write-only and statistics should be read from the function attribute directly. ================================================ FILE: releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml ================================================ --- fixes: - Fix setuptools config to include tenacity.asyncio package in release distributions. ================================================ FILE: releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml ================================================ --- fixes: - | Argument `wait` was improperly annotated, making mypy checks fail. Now it's annotated as `typing.Union[wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]]]` ================================================ FILE: releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml ================================================ --- fixes: - "Fix issue #288 : __name__ and other attributes for async functions" ================================================ FILE: releasenotes/notes/logging-protocol-a4cf0f786f21e4ee.yaml ================================================ --- other: - | Accept non-standard logger in helpers logging something (eg: structlog, loguru...) ================================================ FILE: releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml ================================================ --- fixes: - | Use str.format to format the logs internally to make logging compatible with other logger such as loguru. ================================================ FILE: releasenotes/notes/no-async-iter-6132a42e52348a75.yaml ================================================ --- fixes: - | `AsyncRetrying` was erroneously implementing `__iter__()`, making tenacity retrying mechanism working but in a synchronous fashion and not waiting as expected. This interface has been removed, `__aiter__()` should be used instead. ================================================ FILE: releasenotes/notes/pr320-py3-only-wheel-tag.yaml ================================================ --- other: >- Corrected the PyPI-published wheel tag to match the metadata saying that the release is Python 3 only. ... ================================================ FILE: releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml ================================================ --- features: - Most part of the code is type annotated. - Python 3.10 support has been added. ================================================ FILE: releasenotes/notes/remove-py36-876c0416cf279d15.yaml ================================================ --- upgrade: - | Support for Python 3.6 has been removed. ================================================ FILE: releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml ================================================ --- features: - Add a ``__repr__`` method to ``RetryCallState`` objects for easier debugging. ================================================ FILE: releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml ================================================ --- fixes: - | Preserve __defaults__ and __kwdefaults__ through retry decorator ================================================ FILE: releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml ================================================ --- fixes: Sphinx build error where Sphinx complains about an undefined class. ================================================ FILE: releasenotes/notes/support-py3.14-14928188cab53b99.yaml ================================================ --- features: - Python 3.14 support has been added. ================================================ FILE: releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml ================================================ --- features: - Add ``datetime.timedelta`` as accepted wait unit type. ================================================ FILE: releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml ================================================ --- features: - | - accept ``datetime.timedelta`` instances as argument to ``tenacity.stop.stop_after_delay`` ================================================ FILE: releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml ================================================ --- features: - | If you're using `Trio `__, then ``@retry`` now works automatically. It's no longer necessary to pass ``sleep=trio.sleep``. ================================================ FILE: releasenotes/notes/wait-exponential-jitter-min-timedelta-a8e3c1f4b7d29e50.yaml ================================================ --- features: - | Add ``min`` parameter to ``wait_exponential_jitter`` to set a minimum wait time floor, consistent with ``wait_exponential``. Also accept ``timedelta`` for ``max``, ``jitter``, and ``min`` parameters. ================================================ FILE: releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml ================================================ --- fixes: - | Respects `min` arg for `wait_random_exponential` ================================================ FILE: releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml ================================================ --- features: - | Implement a wait.wait_exponential_jitter per Google's storage retry guide. See https://cloud.google.com/storage/docs/retry-strategy ================================================ FILE: reno.yaml ================================================ --- unreleased_version_title: Unreleased ================================================ FILE: tenacity/__init__.py ================================================ # Copyright 2016-2018 Julien Danjou # Copyright 2017 Elisey Zanko # Copyright 2016 Étienne Bersac # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import dataclasses import functools import sys import threading import time import typing as t import warnings from abc import ABC, abstractmethod from concurrent import futures from . import _utils # Import all built-in after strategies for easier usage. from .after import after_log, after_nothing # Import all built-in before strategies for easier usage. from .before import before_log, before_nothing # Import all built-in before sleep strategies for easier usage. from .before_sleep import before_sleep_log, before_sleep_nothing # Import all nap strategies for easier usage. from .nap import sleep, sleep_using_event # Import all built-in retry strategies for easier usage. from .retry import ( retry_all, retry_always, retry_any, retry_base, retry_if_exception, retry_if_exception_cause_type, retry_if_exception_message, retry_if_exception_type, retry_if_not_exception_message, retry_if_not_exception_type, retry_if_not_result, retry_if_result, retry_never, retry_unless_exception_type, ) # Import all built-in stop strategies for easier usage. from .stop import ( stop_after_attempt, stop_after_delay, stop_all, stop_any, stop_before_delay, stop_never, stop_when_event_set, ) # Import all built-in wait strategies for easier usage. from .wait import ( wait_chain, wait_combine, wait_exception, wait_exponential, wait_exponential_jitter, wait_fixed, wait_incrementing, wait_none, wait_random, wait_random_exponential, ) from .wait import wait_random_exponential as wait_full_jitter try: import tornado except ImportError: tornado = None # type: ignore[assignment] if t.TYPE_CHECKING: if sys.version_info >= (3, 11): from typing import Self else: from typing_extensions import Self import types from . import asyncio as tasyncio from .retry import RetryBaseT from .stop import StopBaseT from .wait import WaitBaseT WrappedFnReturnT = t.TypeVar("WrappedFnReturnT") WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any]) P = t.ParamSpec("P") R = t.TypeVar("R") @dataclasses.dataclass(slots=True) class IterState: actions: list[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field( default_factory=list ) retry_run_result: bool = False stop_run_result: bool = False is_explicit_retry: bool = False def reset(self) -> None: self.actions = [] self.retry_run_result = False self.stop_run_result = False self.is_explicit_retry = False class TryAgain(Exception): """Always retry the executed function when raised.""" NO_RESULT = object() class DoAttempt: pass class DoSleep(float): pass class BaseAction: """Base class for representing actions to take by retry object. Concrete implementations must define: - __init__: to initialize all necessary fields - REPR_FIELDS: class variable specifying attributes to include in repr(self) - NAME: for identification in retry object methods and callbacks """ REPR_FIELDS: t.Sequence[str] = () NAME: str | None = None def __repr__(self) -> str: state_str = ", ".join( f"{field}={getattr(self, field)!r}" for field in self.REPR_FIELDS ) return f"{self.__class__.__name__}({state_str})" def __str__(self) -> str: return repr(self) class RetryAction(BaseAction): REPR_FIELDS = ("sleep",) NAME = "retry" def __init__(self, sleep: t.SupportsFloat) -> None: self.sleep = float(sleep) _unset = object() def _first_set(first: t.Any | object, second: t.Any) -> t.Any: return second if first is _unset else first class RetryError(Exception): """Encapsulates the last attempt instance right before giving up.""" def __init__(self, last_attempt: "Future") -> None: self.last_attempt = last_attempt super().__init__(last_attempt) def reraise(self) -> t.NoReturn: if self.last_attempt.failed: raise self.last_attempt.result() raise self def __str__(self) -> str: return f"{self.__class__.__name__}[{self.last_attempt}]" class AttemptManager: """Manage attempt context.""" def __init__(self, retry_state: "RetryCallState"): self.retry_state = retry_state def __enter__(self) -> None: pass def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: "types.TracebackType | None", ) -> bool | None: if exc_type is not None and exc_value is not None: self.retry_state.set_exception((exc_type, exc_value, traceback)) return True # Swallow exception. # We don't have the result, actually. self.retry_state.set_result(None) return None async def __aenter__(self) -> None: pass async def __aexit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: "types.TracebackType | None", ) -> bool | None: return self.__exit__(exc_type, exc_value, traceback) class BaseRetrying(ABC): def __init__( self, sleep: t.Callable[[int | float], None] = sleep, stop: "StopBaseT" = stop_never, wait: "WaitBaseT" = wait_none(), retry: "RetryBaseT" = retry_if_exception_type(), before: t.Callable[["RetryCallState"], None] = before_nothing, after: t.Callable[["RetryCallState"], None] = after_nothing, before_sleep: t.Callable[["RetryCallState"], None] | None = None, reraise: bool = False, retry_error_cls: type[RetryError] = RetryError, retry_error_callback: t.Callable[["RetryCallState"], t.Any] | None = None, name: str | None = None, enabled: bool = True, ): self.sleep = sleep self.stop = stop self.wait = wait self.retry = retry self.before = before self.after = after self.before_sleep = before_sleep self.reraise = reraise self._local = threading.local() self.retry_error_cls = retry_error_cls self.retry_error_callback = retry_error_callback self._name = name self.enabled = enabled def copy( self, sleep: t.Callable[[int | float], None] | object = _unset, stop: "StopBaseT | object" = _unset, wait: "WaitBaseT | object" = _unset, retry: retry_base | object = _unset, before: t.Callable[["RetryCallState"], None] | object = _unset, after: t.Callable[["RetryCallState"], None] | object = _unset, before_sleep: t.Callable[["RetryCallState"], None] | None | object = _unset, reraise: bool | object = _unset, retry_error_cls: type[RetryError] | object = _unset, retry_error_callback: t.Callable[["RetryCallState"], t.Any] | None | object = _unset, name: str | None | object = _unset, enabled: bool | object = _unset, ) -> "Self": """Copy this object with some parameters changed if needed.""" return self.__class__( sleep=_first_set(sleep, self.sleep), stop=_first_set(stop, self.stop), wait=_first_set(wait, self.wait), retry=_first_set(retry, self.retry), before=_first_set(before, self.before), after=_first_set(after, self.after), before_sleep=_first_set(before_sleep, self.before_sleep), reraise=_first_set(reraise, self.reraise), retry_error_cls=_first_set(retry_error_cls, self.retry_error_cls), retry_error_callback=_first_set( retry_error_callback, self.retry_error_callback ), name=_first_set(name, self._name), enabled=_first_set(enabled, self.enabled), ) def __getstate__(self) -> dict[str, t.Any]: # Exclude threading.local which cannot be pickled return {k: v for k, v in self.__dict__.items() if k != "_local"} def __setstate__(self, state: dict[str, t.Any]) -> None: self.__dict__.update(state) self._local = threading.local() def __str__(self) -> str: return self._name if self._name is not None else "" def __repr__(self) -> str: return ( f"<{self.__class__.__name__} object at 0x{id(self):x} (" f"stop={self.stop}, " f"wait={self.wait}, " f"sleep={self.sleep}, " f"retry={self.retry}, " f"before={self.before}, " f"after={self.after}, " f"name={self._name!r})>" ) @property def statistics(self) -> dict[str, t.Any]: """Return a dictionary of runtime statistics. This dictionary will be empty when the controller has never been ran. When it is running or has ran previously it should have (but may not) have useful and/or informational keys and values when running is underway and/or completed. .. warning:: The keys in this dictionary **should** be somewhat stable (not changing), but their existence **may** change between major releases as new statistics are gathered or removed so before accessing keys ensure that they actually exist and handle when they do not. .. note:: The values in this dictionary are local to the thread running call (so if multiple threads share the same retrying object - either directly or indirectly) they will each have their own view of statistics they have collected (in the future we may provide a way to aggregate the various statistics from each thread). """ if not hasattr(self._local, "statistics"): self._local.statistics = t.cast("dict[str, t.Any]", {}) return self._local.statistics # type: ignore[no-any-return] @property def iter_state(self) -> IterState: if not hasattr(self._local, "iter_state"): self._local.iter_state = IterState() return self._local.iter_state # type: ignore[no-any-return] def wraps(self, f: t.Callable[P, R]) -> "_RetryDecorated[P, R]": """Wrap a function for retrying. :param f: A function to wrap for retrying. """ @functools.wraps( f, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__") ) def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any: if not self.enabled: return f(*args, **kw) # Always create a copy to prevent overwriting the local contexts when # calling the same wrapped functions multiple times in the same stack copy = self.copy() wrapped_f.statistics = copy.statistics # type: ignore[attr-defined] self._local.statistics = copy.statistics return copy(f, *args, **kw) def retry_with(*args: t.Any, **kwargs: t.Any) -> "_RetryDecorated[P, R]": return self.copy(*args, **kwargs).wraps(f) # Preserve attributes wrapped_f.retry = self # type: ignore[attr-defined] wrapped_f.retry_with = retry_with # type: ignore[attr-defined] wrapped_f.statistics = {} # type: ignore[attr-defined] return t.cast("_RetryDecorated[P, R]", wrapped_f) def begin(self) -> None: self.statistics.clear() self.statistics["start_time"] = time.monotonic() self.statistics["attempt_number"] = 1 self.statistics["idle_for"] = 0 self.statistics["delay_since_first_attempt"] = 0 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") -> DoAttempt | DoSleep | t.Any: 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: self.iter_state.reset() fut = retry_state.outcome if fut is None: if self.before is not None: self._add_action_func(self.before) self._add_action_func(lambda rs: DoAttempt()) return 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._add_action_func(self.after) 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: self._add_action_func(self.retry_error_callback) return def exc_check(rs: "RetryCallState") -> None: fut = t.cast("Future", rs.outcome) retry_exc = self.retry_error_cls(fut) if self.reraise: 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._add_action_func(self.before_sleep) self._add_action_func(lambda rs: DoSleep(rs.upcoming_sleep)) def __iter__(self) -> t.Generator[AttemptManager, None, None]: self.begin() retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): yield AttemptManager(retry_state=retry_state) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() self.sleep(do) else: break @abstractmethod def __call__( self, fn: t.Callable[..., WrappedFnReturnT], *args: t.Any, **kwargs: t.Any, ) -> WrappedFnReturnT: pass class Retrying(BaseRetrying): """Retrying controller.""" def __call__( self, fn: t.Callable[..., WrappedFnReturnT], *args: t.Any, **kwargs: t.Any, ) -> WrappedFnReturnT: self.begin() retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = fn(*args, **kwargs) except BaseException: retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() self.sleep(do) else: return do # type: ignore[no-any-return] class Future(futures.Future[t.Any]): """Encapsulates a (future or past) attempted call to a target function.""" def __init__(self, attempt_number: int) -> None: super().__init__() self.attempt_number = attempt_number @property def failed(self) -> bool: """Return whether a exception is being held in this future.""" return self.exception() is not None @classmethod def construct( cls, attempt_number: int, value: t.Any, has_exception: bool ) -> "Future": """Construct a new Future object.""" fut = cls(attempt_number) if has_exception: fut.set_exception(value) else: fut.set_result(value) return fut class RetryCallState: """State related to a single call wrapped with Retrying.""" def __init__( self, retry_object: BaseRetrying, fn: WrappedFn | None, args: t.Any, kwargs: t.Any, ) -> None: #: Retry call start timestamp self.start_time = time.monotonic() #: Retry manager object self.retry_object = retry_object #: Function wrapped by this retry call self.fn = fn #: Arguments of the function wrapped by this retry call self.args = args #: Keyword arguments of the function wrapped by this retry call self.kwargs = kwargs #: The number of the current attempt self.attempt_number: int = 1 #: Last outcome (result or exception) produced by the function self.outcome: Future | None = None #: Timestamp of the last outcome self.outcome_timestamp: float | None = None #: Time spent sleeping in retries self.idle_for: float = 0.0 #: Next action as decided by the retry manager self.next_action: RetryAction | None = None #: Next sleep time as decided by the retry manager. self.upcoming_sleep: float = 0.0 def get_fn_name(self) -> str: """Get the name of the function being retried. Returns the fully-qualified name of the wrapped function when used as a decorator, the ``name`` passed to the retrying object when used as a context manager, or ``""`` if neither is available. """ if self.fn is not None: return _utils.get_callback_name(self.fn) return str(self.retry_object) @property def seconds_since_start(self) -> float | None: if self.outcome_timestamp is None: return None return self.outcome_timestamp - self.start_time def prepare_for_next_attempt(self) -> None: self.outcome = None self.outcome_timestamp = None self.attempt_number += 1 self.next_action = None def set_result(self, val: t.Any) -> None: ts = time.monotonic() fut = Future(self.attempt_number) fut.set_result(val) self.outcome, self.outcome_timestamp = fut, ts def set_exception( self, exc_info: tuple[ type[BaseException], BaseException, "types.TracebackType | None" ], ) -> None: ts = time.monotonic() fut = Future(self.attempt_number) fut.set_exception(exc_info[1]) self.outcome, self.outcome_timestamp = fut, ts def __repr__(self) -> str: if self.outcome is None: result = "none yet" elif self.outcome.failed: exception = self.outcome.exception() result = f"failed ({exception.__class__.__name__} {exception})" else: result = f"returned {self.outcome.result()}" slept = float(round(self.idle_for, 2)) clsname = self.__class__.__name__ return f"<{clsname} {id(self)}: attempt #{self.attempt_number}; slept for {slept}; last result: {result}>" class _RetryDecorated(t.Protocol[P, R]): """Protocol for functions decorated with @retry. Provides the original callable signature plus retry control attributes. """ retry: "BaseRetrying" statistics: dict[str, t.Any] def retry_with(self, *args: t.Any, **kwargs: t.Any) -> "_RetryDecorated[P, R]": ... def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... class _AsyncRetryDecorator(t.Protocol): @t.overload def __call__( self, fn: "t.Callable[P, types.CoroutineType[t.Any, t.Any, R]]" ) -> "_RetryDecorated[P, types.CoroutineType[t.Any, t.Any, R]]": ... @t.overload def __call__( self, fn: t.Callable[P, t.Coroutine[t.Any, t.Any, R]] ) -> "_RetryDecorated[P, t.Coroutine[t.Any, t.Any, R]]": ... @t.overload def __call__( self, fn: t.Callable[P, t.Awaitable[R]] ) -> "_RetryDecorated[P, t.Awaitable[R]]": ... @t.overload def __call__( self, fn: t.Callable[P, R] ) -> "_RetryDecorated[P, t.Awaitable[R]]": ... @t.overload def retry(func: t.Callable[P, R]) -> _RetryDecorated[P, R]: ... @t.overload def retry( *, sleep: t.Callable[[int | float], t.Awaitable[None]], stop: "StopBaseT" = ..., wait: "WaitBaseT" = ..., retry: "RetryBaseT | tasyncio.retry.RetryBaseT" = ..., before: t.Callable[["RetryCallState"], None | t.Awaitable[None]] = ..., after: t.Callable[["RetryCallState"], None | t.Awaitable[None]] = ..., before_sleep: t.Callable[["RetryCallState"], None | t.Awaitable[None]] | None = ..., reraise: bool = ..., retry_error_cls: type["RetryError"] = ..., retry_error_callback: t.Callable[["RetryCallState"], t.Any | t.Awaitable[t.Any]] | None = ..., enabled: bool = ..., ) -> _AsyncRetryDecorator: ... @t.overload def retry( sleep: t.Callable[[int | float], None] = sleep, stop: "StopBaseT" = stop_never, wait: "WaitBaseT" = wait_none(), retry: "RetryBaseT | tasyncio.retry.RetryBaseT" = retry_if_exception_type(), before: t.Callable[["RetryCallState"], None | t.Awaitable[None]] = before_nothing, after: t.Callable[["RetryCallState"], None | t.Awaitable[None]] = after_nothing, before_sleep: t.Callable[["RetryCallState"], None | t.Awaitable[None]] | None = None, reraise: bool = False, retry_error_cls: type["RetryError"] = RetryError, retry_error_callback: t.Callable[["RetryCallState"], t.Any | t.Awaitable[t.Any]] | None = None, enabled: bool = True, ) -> t.Callable[[t.Callable[P, R]], _RetryDecorated[P, R]]: ... def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any: """Wrap a function with a new `Retrying` object. :param dargs: positional arguments passed to Retrying object :param dkw: keyword arguments passed to the Retrying object """ # support both @retry and @retry() as valid syntax if len(dargs) == 1 and callable(dargs[0]): return retry()(dargs[0]) def wrap(f: t.Callable[P, R]) -> _RetryDecorated[P, R]: if isinstance(f, retry_base): warnings.warn( f"Got retry_base instance ({f.__class__.__name__}) as callable argument, " f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)", stacklevel=2, ) r: BaseRetrying sleep = dkw.get("sleep") if _utils.is_coroutine_callable(f) or ( sleep is not None and _utils.is_coroutine_callable(sleep) ): r = AsyncRetrying(*dargs, **dkw) elif ( tornado and hasattr(tornado.gen, "is_coroutine_function") and tornado.gen.is_coroutine_function(f) ): r = TornadoRetrying(*dargs, **dkw) else: r = Retrying(*dargs, **dkw) return r.wraps(f) return wrap from tenacity.asyncio import AsyncRetrying # noqa: E402 if tornado: from tenacity.tornadoweb import TornadoRetrying __all__ = [ "NO_RESULT", "AsyncRetrying", "AttemptManager", "BaseAction", "BaseRetrying", "DoAttempt", "DoSleep", "Future", "RetryAction", "RetryCallState", "RetryError", "Retrying", "TryAgain", "WrappedFn", "after_log", "after_nothing", "before_log", "before_nothing", "before_sleep_log", "before_sleep_nothing", "retry", "retry_all", "retry_always", "retry_any", "retry_base", "retry_if_exception", "retry_if_exception_cause_type", "retry_if_exception_message", "retry_if_exception_type", "retry_if_not_exception_message", "retry_if_not_exception_type", "retry_if_not_result", "retry_if_result", "retry_never", "retry_unless_exception_type", "sleep", "sleep_using_event", "stop_after_attempt", "stop_after_delay", "stop_all", "stop_any", "stop_before_delay", "stop_never", "stop_when_event_set", "wait_chain", "wait_combine", "wait_exception", "wait_exponential", "wait_exponential_jitter", "wait_fixed", "wait_full_jitter", "wait_incrementing", "wait_none", "wait_random", "wait_random_exponential", ] ================================================ FILE: tenacity/_utils.py ================================================ # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import contextlib import functools import inspect import sys import typing from datetime import timedelta # sys.maxsize: # An integer giving the maximum value a variable of type Py_ssize_t can take. MAX_WAIT = sys.maxsize / 2 class LoggerProtocol(typing.Protocol): """ Protocol used by utils expecting a logger (eg: before_log). Compatible with logging, structlog, loguru, etc... """ def log(self, level: int, msg: str, *args: typing.Any) -> typing.Any: ... def find_ordinal(pos_num: int) -> str: # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers if 11 <= (pos_num % 100) <= 13: return "th" if pos_num == 0: return "th" if pos_num == 1: return "st" if pos_num == 2: return "nd" if pos_num == 3: return "rd" if 4 <= pos_num <= 20: return "th" return find_ordinal(pos_num % 10) def to_ordinal(pos_num: int) -> str: return f"{pos_num}{find_ordinal(pos_num)}" def get_callback_name(cb: typing.Callable[..., typing.Any]) -> str: """Get a callback fully-qualified name. If no name can be produced ``repr(cb)`` is called and returned. """ segments = [] try: segments.append(cb.__qualname__) except AttributeError: with contextlib.suppress(AttributeError): segments.append(cb.__name__) if not segments: return repr(cb) with contextlib.suppress(AttributeError): # When running under sphinx it appears this can be none? if cb.__module__: segments.insert(0, cb.__module__) return ".".join(segments) time_unit_type = int | float | timedelta def to_seconds(time_unit: time_unit_type) -> float: 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) # noqa: B004 return inspect.iscoroutinefunction(dunder_call) def wrap_to_async_func( call: typing.Callable[..., typing.Any], ) -> typing.Callable[..., typing.Awaitable[typing.Any]]: if is_coroutine_callable(call): return call async def inner(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: return call(*args, **kwargs) return inner ================================================ FILE: tenacity/after.py ================================================ # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import typing from tenacity import _utils if typing.TYPE_CHECKING: from tenacity import RetryCallState def after_nothing(retry_state: "RetryCallState") -> None: """After call strategy that does nothing.""" def after_log( logger: _utils.LoggerProtocol, log_level: int, sec_format: str = "%.3g", ) -> typing.Callable[["RetryCallState"], None]: """After call strategy that logs to some logger the finished attempt.""" def log_it(retry_state: "RetryCallState") -> None: fn_name = retry_state.get_fn_name() secs = retry_state.seconds_since_start logger.log( log_level, f"Finished call to '{fn_name}' " f"after {sec_format % secs if secs is not None else '?'}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) return log_it ================================================ FILE: tenacity/asyncio/__init__.py ================================================ # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools import sys import typing as t import tenacity from tenacity import ( AttemptManager, BaseRetrying, DoAttempt, DoSleep, RetryCallState, RetryError, _RetryDecorated, _utils, after_nothing, before_nothing, ) # Import all built-in retry strategies for easier usage. from .retry import ( RetryBaseT, retry_all, retry_any, retry_if_exception, retry_if_result, ) if t.TYPE_CHECKING: from tenacity.retry import RetryBaseT as SyncRetryBaseT from tenacity.stop import StopBaseT from tenacity.wait import WaitBaseT WrappedFnReturnT = t.TypeVar("WrappedFnReturnT") WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]]) P = t.ParamSpec("P") R = t.TypeVar("R") def _portable_async_sleep(seconds: float) -> t.Awaitable[None]: # If trio is already imported, then importing it is cheap. # If trio isn't already imported, then it's definitely not running, so we # can skip further checks. if "trio" in sys.modules: # If trio is available, then sniffio is too import sniffio import trio if sniffio.current_async_library() == "trio": return trio.sleep(seconds) # noqa: ASYNC105 # Otherwise, assume asyncio # Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead). import asyncio return asyncio.sleep(seconds) class AsyncRetrying(BaseRetrying): def __init__( self, sleep: t.Callable[ [int | float], None | t.Awaitable[None] ] = _portable_async_sleep, stop: "StopBaseT" = tenacity.stop.stop_never, wait: "WaitBaseT" = tenacity.wait.wait_none(), retry: "SyncRetryBaseT | RetryBaseT" = tenacity.retry_if_exception_type(), before: t.Callable[ ["RetryCallState"], None | t.Awaitable[None] ] = before_nothing, after: t.Callable[["RetryCallState"], None | t.Awaitable[None]] = after_nothing, before_sleep: t.Callable[["RetryCallState"], None | t.Awaitable[None]] | None = None, reraise: bool = False, retry_error_cls: type["RetryError"] = RetryError, retry_error_callback: t.Callable[["RetryCallState"], t.Any | t.Awaitable[t.Any]] | None = None, name: str | None = None, enabled: bool = True, ) -> None: super().__init__( sleep=sleep, # type: ignore[arg-type] stop=stop, wait=wait, retry=retry, # type: ignore[arg-type] before=before, # type: ignore[arg-type] after=after, # type: ignore[arg-type] before_sleep=before_sleep, # type: ignore[arg-type] reraise=reraise, retry_error_cls=retry_error_cls, retry_error_callback=retry_error_callback, name=name, enabled=enabled, ) async def __call__( # type: ignore[override] self, fn: WrappedFn, *args: t.Any, **kwargs: t.Any ) -> WrappedFnReturnT: self.begin() retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) is_async = _utils.is_coroutine_callable(fn) while True: do = await self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: if is_async: result = await fn(*args, **kwargs) else: result = fn(*args, **kwargs) except BaseException: retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() await self.sleep(do) # type: ignore[misc] else: return do # type: ignore[no-any-return] def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None: self.iter_state.actions.append(_utils.wrap_to_async_func(fn)) async def _run_retry(self, retry_state: "RetryCallState") -> None: # type: ignore[override] self.iter_state.retry_run_result = await _utils.wrap_to_async_func(self.retry)( retry_state ) async def _run_wait(self, retry_state: "RetryCallState") -> None: # type: ignore[override] if self.wait: sleep = await _utils.wrap_to_async_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 _utils.wrap_to_async_func(self.stop)( retry_state ) async def iter(self, retry_state: "RetryCallState") -> DoAttempt | DoSleep | t.Any: 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]: raise TypeError("AsyncRetrying object is not iterable") def __aiter__(self) -> "AsyncRetrying": self.begin() self._retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) return self async def __anext__(self) -> AttemptManager: while True: do = await self.iter(retry_state=self._retry_state) if do is None: raise StopAsyncIteration if isinstance(do, DoAttempt): return AttemptManager(retry_state=self._retry_state) if isinstance(do, DoSleep): self._retry_state.prepare_for_next_attempt() await self.sleep(do) # type: ignore[misc] else: raise StopAsyncIteration def wraps(self, fn: t.Callable[P, R]) -> _RetryDecorated[P, R]: wrapped = super().wraps(fn) # Ensure wrapper is recognized as a coroutine function. @functools.wraps( fn, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__") ) async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any: if not self.enabled: return await fn(*args, **kwargs) # type: ignore[misc] # Always create a copy to prevent overwriting the local contexts when # calling the same wrapped functions multiple times in the same stack copy = self.copy() async_wrapped.statistics = copy.statistics # type: ignore[attr-defined] self._local.statistics = copy.statistics return await copy(fn, *args, **kwargs) # type: ignore[type-var] # Preserve attributes async_wrapped.retry = self # type: ignore[attr-defined] async_wrapped.retry_with = wrapped.retry_with # type: ignore[attr-defined] async_wrapped.statistics = {} # type: ignore[attr-defined] return t.cast("_RetryDecorated[P, R]", async_wrapped) __all__ = [ "AsyncRetrying", "WrappedFn", "retry_all", "retry_any", "retry_if_exception", "retry_if_result", ] ================================================ FILE: tenacity/asyncio/retry.py ================================================ # Copyright 2016–2021 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import typing from tenacity import _utils, retry_base if typing.TYPE_CHECKING: from tenacity import RetryCallState class async_retry_base(retry_base): """Abstract base class for async retry strategies.""" @abc.abstractmethod async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] pass def __and__( # type: ignore[override] self, other: "retry_base | async_retry_base" ) -> "retry_all": return retry_all(self, other) def __rand__( # type: ignore[misc,override] self, other: "retry_base | async_retry_base" ) -> "retry_all": return retry_all(other, self) def __or__( # type: ignore[override] self, other: "retry_base | async_retry_base" ) -> "retry_any": return retry_any(self, other) def __ror__( # type: ignore[misc,override] self, other: "retry_base | async_retry_base" ) -> "retry_any": return retry_any(other, self) RetryBaseT = ( async_retry_base | typing.Callable[["RetryCallState"], typing.Awaitable[bool]] ) class retry_if_exception(async_retry_base): """Retry strategy that retries if an exception verifies a predicate.""" def __init__( self, predicate: typing.Callable[[BaseException], typing.Awaitable[bool]] ) -> None: self.predicate = predicate async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if retry_state.outcome.failed: exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return await self.predicate(exception) return False class retry_if_result(async_retry_base): """Retries if the result verifies a predicate.""" def __init__( self, predicate: typing.Callable[[typing.Any], typing.Awaitable[bool]] ) -> None: self.predicate = predicate async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if not retry_state.outcome.failed: return await self.predicate(retry_state.outcome.result()) return False class retry_any(async_retry_base): """Retries if any of the retries condition is valid.""" def __init__(self, *retries: retry_base | async_retry_base) -> None: self.retries = retries async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] result = False for r in self.retries: result = result or await _utils.wrap_to_async_func(r)(retry_state) if result: break return result def __ror__( # type: ignore[misc,override] self, other: "retry_base | async_retry_base" ) -> "retry_any": if isinstance(other, retry_any): return retry_any(*other.retries, *self.retries) return retry_any(other, *self.retries) class retry_all(async_retry_base): """Retries if all the retries condition are valid.""" def __init__(self, *retries: retry_base | async_retry_base) -> None: self.retries = retries async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] result = True for r in self.retries: result = result and await _utils.wrap_to_async_func(r)(retry_state) if not result: break return result def __rand__( # type: ignore[misc,override] self, other: "retry_base | async_retry_base" ) -> "retry_all": if isinstance(other, retry_all): return retry_all(*other.retries, *self.retries) return retry_all(other, *self.retries) ================================================ FILE: tenacity/before.py ================================================ # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import typing from tenacity import _utils if typing.TYPE_CHECKING: from tenacity import RetryCallState def before_nothing(retry_state: "RetryCallState") -> None: """Before call strategy that does nothing.""" def before_log( logger: _utils.LoggerProtocol, log_level: int ) -> typing.Callable[["RetryCallState"], None]: """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state: "RetryCallState") -> None: fn_name = retry_state.get_fn_name() logger.log( log_level, f"Starting call to '{fn_name}', " f"this is the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) return log_it ================================================ FILE: tenacity/before_sleep.py ================================================ # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import traceback import typing from tenacity import _utils if typing.TYPE_CHECKING: from tenacity import RetryCallState def before_sleep_nothing(retry_state: "RetryCallState") -> None: """Before sleep strategy that does nothing.""" def before_sleep_log( logger: _utils.LoggerProtocol, log_level: int, exc_info: bool = False, sec_format: str = "%.3g", ) -> typing.Callable[["RetryCallState"], None]: """Before sleep strategy that logs to some logger the attempt.""" def log_it(retry_state: "RetryCallState") -> None: if retry_state.outcome is None: raise RuntimeError("log_it() called before outcome was set") if retry_state.next_action is None: raise RuntimeError("log_it() called before next_action was set") if retry_state.outcome.failed: ex = retry_state.outcome.exception() verb, value = "raised", f"{ex.__class__.__name__}: {ex}" else: verb, value = "returned", retry_state.outcome.result() fn_name = retry_state.get_fn_name() msg = ( f"Retrying {fn_name} " f"in {sec_format % retry_state.next_action.sleep} seconds as it {verb} {value}." ) if exc_info and retry_state.outcome.failed: ex = retry_state.outcome.exception() if ex is not None: tb = "".join(traceback.format_exception(type(ex), ex, ex.__traceback__)) msg = f"{msg}\n{tb.rstrip()}" logger.log(log_level, msg) return log_it ================================================ FILE: tenacity/nap.py ================================================ # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import time import typing if typing.TYPE_CHECKING: import threading def sleep(seconds: float) -> None: """ Sleep strategy that delays execution for a given number of seconds. This is the default strategy, and may be mocked out for unit testing. """ time.sleep(seconds) class sleep_using_event: """Sleep strategy that waits on an event to be set.""" def __init__(self, event: "threading.Event") -> None: self.event = event def __call__(self, timeout: float | None) -> None: # NOTE(harlowja): this may *not* actually wait for timeout # seconds if the event is set (ie this may eject out early). self.event.wait(timeout=timeout) ================================================ FILE: tenacity/py.typed ================================================ ================================================ FILE: tenacity/retry.py ================================================ # Copyright 2016–2021 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import re import typing if typing.TYPE_CHECKING: from tenacity import RetryCallState class retry_base(abc.ABC): """Abstract base class for retry strategies.""" @abc.abstractmethod def __call__(self, retry_state: "RetryCallState") -> bool: pass def __and__(self, other: "RetryBaseT") -> "retry_all": if isinstance(other, retry_base): return other.__rand__(self) # Plain callable: flatten if self is already a retry_all if isinstance(self, retry_all): return retry_all(*self.retries, other) return retry_all(self, other) def __rand__(self, other: "RetryBaseT") -> "retry_all": # Flatten if other is already a retry_all if isinstance(other, retry_all): return retry_all(*other.retries, self) return retry_all(other, self) def __or__(self, other: "RetryBaseT") -> "retry_any": if isinstance(other, retry_base): return other.__ror__(self) # Plain callable: flatten if self is already a retry_any if isinstance(self, retry_any): return retry_any(*self.retries, other) return retry_any(self, other) def __ror__(self, other: "RetryBaseT") -> "retry_any": # Flatten if other is already a retry_any if isinstance(other, retry_any): return retry_any(*other.retries, self) return retry_any(other, self) RetryBaseT = retry_base | typing.Callable[["RetryCallState"], bool] class _retry_never(retry_base): """Retry strategy that never rejects any result.""" def __call__(self, retry_state: "RetryCallState") -> bool: return False retry_never = _retry_never() class _retry_always(retry_base): """Retry strategy that always rejects any result.""" def __call__(self, retry_state: "RetryCallState") -> bool: return True retry_always = _retry_always() class retry_if_exception(retry_base): """Retry strategy that retries if an exception verifies a predicate.""" def __init__(self, predicate: typing.Callable[[BaseException], bool]) -> None: self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if retry_state.outcome.failed: exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return self.predicate(exception) return False class retry_if_exception_type(retry_if_exception): """Retries if an exception has been raised of one or more types.""" def __init__( self, exception_types: type[BaseException] | tuple[type[BaseException], ...] = Exception, ) -> None: self.exception_types = exception_types super().__init__(self._check) def _check(self, e: BaseException) -> bool: return isinstance(e, self.exception_types) class retry_if_not_exception_type(retry_if_exception): """Retries except an exception has been raised of one or more types.""" def __init__( self, exception_types: type[BaseException] | tuple[type[BaseException], ...] = Exception, ) -> None: self.exception_types = exception_types super().__init__(self._check) def _check(self, e: BaseException) -> bool: return not isinstance(e, self.exception_types) class retry_unless_exception_type(retry_if_exception): """Retries until an exception is raised of one or more types.""" def __init__( self, exception_types: type[BaseException] | tuple[type[BaseException], ...] = Exception, ) -> None: self.exception_types = exception_types super().__init__(self._check) def _check(self, e: BaseException) -> bool: return not isinstance(e, self.exception_types) def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") # always retry if no exception was raised if not retry_state.outcome.failed: return True exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return self.predicate(exception) class retry_if_exception_cause_type(retry_base): """Retries if any of the causes of the raised exception is of one or more types. The check on the type of the cause of the exception is done recursively (until finding an exception in the chain that has no `__cause__`) """ def __init__( self, exception_types: type[BaseException] | tuple[type[BaseException], ...] = Exception, ) -> None: self.exception_cause_types = exception_types def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__ called before outcome was set") if retry_state.outcome.failed: exc = retry_state.outcome.exception() while exc is not None: if isinstance(exc.__cause__, self.exception_cause_types): return True exc = exc.__cause__ return False class retry_if_result(retry_base): """Retries if the result verifies a predicate.""" def __init__(self, predicate: typing.Callable[[typing.Any], bool]) -> None: self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if not retry_state.outcome.failed: return self.predicate(retry_state.outcome.result()) return False class retry_if_not_result(retry_base): """Retries if the result refutes a predicate.""" def __init__(self, predicate: typing.Callable[[typing.Any], bool]) -> None: self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if not retry_state.outcome.failed: return not self.predicate(retry_state.outcome.result()) return False class retry_if_exception_message(retry_if_exception): """Retries if an exception message equals or matches.""" def __init__( self, message: str | None = None, match: None | str | re.Pattern[str] = None, ) -> None: if message and match: raise TypeError( f"{self.__class__.__name__}() takes either 'message' or 'match', not both" ) if not message and not match: raise TypeError( f"{self.__class__.__name__}() missing 1 required argument 'message' or 'match'" ) self.message = message self.match = re.compile(match) if match else None super().__init__(self._check) def _check(self, exception: BaseException) -> bool: if self.message: return self.message == str(exception) assert self.match is not None return bool(self.match.match(str(exception))) class retry_if_not_exception_message(retry_if_exception_message): """Retries until an exception message equals or matches.""" def _check(self, exception: BaseException) -> bool: return not super()._check(exception) def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if not retry_state.outcome.failed: return True exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return self.predicate(exception) class retry_any(retry_base): """Retries if any of the retries condition is valid.""" def __init__(self, *retries: "RetryBaseT") -> None: self.retries = retries def __call__(self, retry_state: "RetryCallState") -> bool: return any(r(retry_state) for r in self.retries) def __ror__(self, other: "RetryBaseT") -> "retry_any": if isinstance(other, retry_any): return retry_any(*other.retries, *self.retries) return retry_any(other, *self.retries) class retry_all(retry_base): """Retries if all the retries condition are valid.""" def __init__(self, *retries: "RetryBaseT") -> None: self.retries = retries def __call__(self, retry_state: "RetryCallState") -> bool: return all(r(retry_state) for r in self.retries) def __rand__(self, other: "RetryBaseT") -> "retry_all": if isinstance(other, retry_all): return retry_all(*other.retries, *self.retries) return retry_all(other, *self.retries) ================================================ FILE: tenacity/stop.py ================================================ # Copyright 2016–2021 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import typing from tenacity import _utils if typing.TYPE_CHECKING: import threading from tenacity import RetryCallState class stop_base(abc.ABC): """Abstract base class for stop strategies.""" @abc.abstractmethod def __call__(self, retry_state: "RetryCallState") -> bool: pass def __and__(self, other: "stop_base") -> "stop_all": return stop_all(self, other) def __or__(self, other: "stop_base") -> "stop_any": return stop_any(self, other) StopBaseT = stop_base | typing.Callable[["RetryCallState"], bool] class stop_any(stop_base): """Stop if any of the stop condition is valid.""" def __init__(self, *stops: stop_base) -> None: self.stops = stops def __call__(self, retry_state: "RetryCallState") -> bool: return any(x(retry_state) for x in self.stops) class stop_all(stop_base): """Stop if all the stop conditions are valid.""" def __init__(self, *stops: stop_base) -> None: self.stops = stops def __call__(self, retry_state: "RetryCallState") -> bool: return all(x(retry_state) for x in self.stops) class _stop_never(stop_base): """Never stop.""" def __call__(self, retry_state: "RetryCallState") -> bool: return False stop_never = _stop_never() class stop_when_event_set(stop_base): """Stop when the given event is set.""" def __init__(self, event: "threading.Event") -> None: self.event = event def __call__(self, retry_state: "RetryCallState") -> bool: return self.event.is_set() class stop_after_attempt(stop_base): """Stop when the previous attempt >= max_attempt.""" def __init__(self, max_attempt_number: int) -> None: self.max_attempt_number = max_attempt_number def __call__(self, retry_state: "RetryCallState") -> bool: return retry_state.attempt_number >= self.max_attempt_number class stop_after_delay(stop_base): """ 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: 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 >= 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 ) ================================================ FILE: tenacity/tornadoweb.py ================================================ # Copyright 2017 Elisey Zanko # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys import typing from tornado import gen from tenacity import BaseRetrying, DoAttempt, DoSleep, RetryCallState if typing.TYPE_CHECKING: from tornado.concurrent import Future _RetValT = typing.TypeVar("_RetValT") class TornadoRetrying(BaseRetrying): sleep: typing.Callable[..., "Future[None]"] def __init__( self, sleep: "typing.Callable[[float], Future[None]]" = gen.sleep, **kwargs: typing.Any, ) -> None: super().__init__(**kwargs) self.sleep = sleep @gen.coroutine def __call__( # type: ignore[override] self, fn: "typing.Callable[..., typing.Generator[typing.Any, typing.Any, _RetValT] | Future[_RetValT]]", *args: typing.Any, **kwargs: typing.Any, ) -> "typing.Generator[typing.Any, typing.Any, _RetValT]": self.begin() retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = yield fn(*args, **kwargs) except BaseException: retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() yield self.sleep(do) else: raise gen.Return(do) ================================================ FILE: tenacity/wait.py ================================================ # Copyright 2016–2021 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import random import typing import warnings from tenacity import _utils if typing.TYPE_CHECKING: from tenacity import RetryCallState class wait_base(abc.ABC): """Abstract base class for wait strategies.""" @abc.abstractmethod def __call__(self, retry_state: "RetryCallState") -> float: pass def __add__(self, other: "wait_base") -> "wait_combine": return wait_combine(self, other) def __radd__(self, other: "wait_base") -> "wait_combine | wait_base": # make it possible to use multiple waits with the built-in sum function if other == 0: # type: ignore[comparison-overlap] return self return self.__add__(other) WaitBaseT = wait_base | typing.Callable[["RetryCallState"], float | int] class wait_fixed(wait_base): """Wait strategy that waits a fixed amount of time between each retry.""" def __init__(self, wait: _utils.time_unit_type) -> None: self.wait_fixed = _utils.to_seconds(wait) def __call__(self, retry_state: "RetryCallState") -> float: return self.wait_fixed class wait_none(wait_fixed): """Wait strategy that doesn't wait at all before retrying.""" def __init__(self) -> None: super().__init__(0) class wait_random(wait_base): """Wait strategy that waits a random amount of time between min/max.""" def __init__( self, min: _utils.time_unit_type = 0, max: _utils.time_unit_type = 1 ) -> None: self.wait_random_min = _utils.to_seconds(min) 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) ) class wait_combine(wait_base): """Combine several waiting strategies.""" def __init__(self, *strategies: wait_base) -> None: self.wait_funcs = strategies def __call__(self, retry_state: "RetryCallState") -> float: return sum(x(retry_state=retry_state) for x in self.wait_funcs) class wait_chain(wait_base): """Chain two or more waiting strategies. If all strategies are exhausted, the very last strategy is used thereafter. For example:: @retry(wait=wait_chain(*[wait_fixed(1) for i in range(3)] + [wait_fixed(2) for j in range(5)] + [wait_fixed(5) for k in range(4)])) def wait_chained(): print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s " "thereafter.") """ def __init__(self, *strategies: wait_base) -> None: self.strategies = strategies def __call__(self, retry_state: "RetryCallState") -> float: wait_func_no = min(max(retry_state.attempt_number, 1), len(self.strategies)) wait_func = self.strategies[wait_func_no - 1] return wait_func(retry_state=retry_state) class wait_exception(wait_base): """Wait strategy that waits the amount of time returned by the predicate. The predicate is passed the exception object. Based on the exception, the user can decide how much time to wait before retrying. For example:: def http_error(exception: BaseException) -> float: if ( isinstance(exception, requests.HTTPError) and exception.response.status_code == requests.codes.too_many_requests ): return float(exception.response.headers.get("Retry-After", "1")) return 60.0 @retry( stop=stop_after_attempt(3), wait=wait_exception(http_error), ) def http_get_request(url: str) -> None: response = requests.get(url) response.raise_for_status() """ def __init__(self, predicate: typing.Callable[[BaseException], float]) -> None: self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> float: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return self.predicate(exception) class wait_incrementing(wait_base): """Wait an incremental amount of time after each attempt. Starting at a starting value and incrementing by a value for each attempt (and restricting the upper limit to some maximum value). """ def __init__( self, start: _utils.time_unit_type = 0, increment: _utils.time_unit_type = 100, max: _utils.time_unit_type = _utils.MAX_WAIT, ) -> None: self.start = _utils.to_seconds(start) self.increment = _utils.to_seconds(increment) self.max = _utils.to_seconds(max) def __call__(self, retry_state: "RetryCallState") -> float: result = self.start + (self.increment * (retry_state.attempt_number - 1)) return max(0, min(result, self.max)) class wait_exponential(wait_base): """Wait strategy that applies exponential backoff. It allows for a customized multiplier and an ability to restrict the upper and lower limits to some maximum and minimum value. The intervals are fixed (i.e. there is no jitter), so this strategy is suitable for balancing retries against latency when a required resource is unavailable for an unknown duration, but *not* suitable for resolving contention between multiple processes for a shared resource. Use wait_random_exponential for the latter case. """ def __init__( self, multiplier: float = 1, max: _utils.time_unit_type = _utils.MAX_WAIT, exp_base: float = 2, min: _utils.time_unit_type = 0, ) -> None: self.multiplier = multiplier self.min = _utils.to_seconds(min) self.max = _utils.to_seconds(max) self.exp_base = exp_base def __call__(self, retry_state: "RetryCallState") -> float: try: exp = self.exp_base ** (retry_state.attempt_number - 1) result = self.multiplier * exp except OverflowError: return self.max return max(max(0, self.min), min(result, self.max)) class wait_random_exponential(wait_exponential): """Random wait with exponentially widening window. An exponential backoff strategy used to mediate contention between multiple uncoordinated processes for a shared resource in distributed systems. This is the sense in which "exponential backoff" is meant in e.g. Ethernet networking, and corresponds to the "Full Jitter" algorithm described in this blog post: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ Each retry occurs at a random time in a geometrically expanding interval. It allows for a custom multiplier and an ability to restrict the upper limit of the random interval to some maximum value. Example:: wait_random_exponential(multiplier=0.5, # initial window 0.5s max=60) # max 60s timeout When waiting for an unavailable resource to become available again, as opposed to trying to resolve contention for a shared resource, the wait_exponential strategy (which uses a fixed interval) may be preferable. """ def __call__(self, retry_state: "RetryCallState") -> float: high = super().__call__(retry_state=retry_state) return random.uniform(self.min, high) class wait_exponential_jitter(wait_base): """Wait strategy that applies exponential backoff and jitter. It allows for a customized multiplier, maximum wait, jitter and minimum. This implements the strategy described here: https://cloud.google.com/storage/docs/retry-strategy The wait time is max(min, min(multiplier * 2**n + random.uniform(0, jitter), maximum)) where n is the retry count. """ def __init__( self, initial: float = 1, max: _utils.time_unit_type = _utils.MAX_WAIT, exp_base: float = 2, jitter: _utils.time_unit_type = 1, min: _utils.time_unit_type = 0, multiplier: float = 1, ) -> None: if initial != 1 and multiplier != 1: raise ValueError( "Cannot specify both 'initial' and 'multiplier' — use 'multiplier' only" ) if initial != 1: warnings.warn( "The 'initial' parameter is deprecated, use 'multiplier' instead", DeprecationWarning, stacklevel=2, ) multiplier = initial self.multiplier = multiplier self.max = _utils.to_seconds(max) self.exp_base = exp_base self.jitter = _utils.to_seconds(jitter) self.min = _utils.to_seconds(min) def __call__(self, retry_state: "RetryCallState") -> float: jitter = random.uniform(0, self.jitter) try: exp = self.exp_base ** (retry_state.attempt_number - 1) result = self.multiplier * exp + jitter except OverflowError: result = self.max return max(max(0, self.min), min(result, self.max)) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/test_after.py ================================================ import logging import random import unittest.mock from tenacity import ( _utils, after_log, ) from . import test_tenacity class TestAfterLogFormat(unittest.TestCase): def setUp(self) -> None: self.log_level = random.choice( ( logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL, ) ) self.previous_attempt_number = random.randint(1, 512) def test_01_default(self) -> None: """Test log formatting.""" log = unittest.mock.MagicMock(spec="logging.Logger.log") logger = unittest.mock.MagicMock(spec="logging.Logger", log=log) sec_format = "%.3g" delay_since_first_attempt = 0.1 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) log.assert_called_once_with( self.log_level, f"Finished call to '{retry_state.get_fn_name()}' " f"after {sec_format % retry_state.seconds_since_start}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) def test_02_none_seconds_since_start(self) -> None: """Test log formatting when seconds_since_start is None.""" log = unittest.mock.MagicMock(spec="logging.Logger.log") logger = unittest.mock.MagicMock(spec="logging.Logger", log=log) retry_state = test_tenacity.make_retry_state(self.previous_attempt_number, 0.1) retry_state.outcome_timestamp = None assert retry_state.seconds_since_start is None fun = after_log(logger=logger, log_level=self.log_level) fun(retry_state) log.assert_called_once_with( self.log_level, f"Finished call to '{retry_state.get_fn_name()}' " f"after ?(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) def test_02_custom_sec_format(self) -> None: """Test log formatting with custom int format..""" log = unittest.mock.MagicMock(spec="logging.Logger.log") logger = unittest.mock.MagicMock(spec="logging.Logger", log=log) sec_format = "%.1f" delay_since_first_attempt = 0.1 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) log.assert_called_once_with( self.log_level, f"Finished call to '{retry_state.get_fn_name()}' " f"after {sec_format % retry_state.seconds_since_start}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) ================================================ FILE: tests/test_asyncio.py ================================================ # Copyright 2016 Étienne Bersac # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import inspect import unittest from collections.abc import Callable, Coroutine from functools import wraps from typing import Any, TypeVar from unittest import mock try: import trio except ImportError: have_trio = False else: have_trio = True import pytest import tenacity from tenacity import ( AsyncRetrying, RetryCallState, RetryError, retry, retry_if_exception, retry_if_result, stop_after_attempt, ) from tenacity import asyncio as tasyncio from tenacity.wait import wait_fixed from .test_tenacity import ( NoIOErrorAfterCount, NoneReturnUntilAfterCount, current_time_ms, ) _F = TypeVar("_F", bound=Callable[..., Coroutine[Any, Any, Any]]) def asynctest(callable_: _F) -> Callable[..., Any]: @wraps(callable_) def wrapper(*a: Any, **kw: Any) -> Any: return asyncio.run(callable_(*a, **kw)) return wrapper async def _async_function(thing: NoIOErrorAfterCount) -> Any: await asyncio.sleep(0.00001) return thing.go() @retry async def _retryable_coroutine(thing: NoIOErrorAfterCount) -> Any: await asyncio.sleep(0.00001) return thing.go() @retry(stop=stop_after_attempt(2)) async def _retryable_coroutine_with_2_attempts(thing: NoIOErrorAfterCount) -> Any: await asyncio.sleep(0.00001) return thing.go() class TestAsyncio(unittest.TestCase): @asynctest async def test_retry(self) -> None: thing = NoIOErrorAfterCount(5) await _retryable_coroutine(thing) assert thing.counter == thing.count @asynctest async def test_iscoroutinefunction(self) -> None: assert asyncio.iscoroutinefunction(_retryable_coroutine) assert inspect.iscoroutinefunction(_retryable_coroutine) @asynctest async def test_retry_using_async_retying(self) -> None: thing = NoIOErrorAfterCount(5) retrying = AsyncRetrying() await retrying(_async_function, thing) assert thing.counter == thing.count @asynctest async def test_stop_after_attempt(self) -> None: thing = NoIOErrorAfterCount(2) try: await _retryable_coroutine_with_2_attempts(thing) except RetryError: assert thing.counter == 2 def test_repr(self) -> None: repr(tasyncio.AsyncRetrying()) def test_retry_attributes(self) -> None: assert hasattr(_retryable_coroutine, "retry") assert hasattr(_retryable_coroutine, "retry_with") def test_retry_preserves_argument_defaults(self) -> None: async def function_with_defaults(a: int = 1) -> int: return a async def function_with_kwdefaults(*, a: int = 1) -> int: 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__, # type: ignore[attr-defined] ) self.assertEqual( function_with_kwdefaults.__kwdefaults__, wrapped_kwdefaults_function.__kwdefaults__, # type: ignore[attr-defined] ) @asynctest async def test_attempt_number_is_correct_for_interleaved_coroutines(self) -> None: attempts: list[Any] = [] def after(retry_state: RetryCallState) -> None: attempts.append((retry_state.args[0], retry_state.attempt_number)) thing1 = NoIOErrorAfterCount(3) thing2 = NoIOErrorAfterCount(3) await asyncio.gather( _retryable_coroutine.retry_with(after=after)(thing1), _retryable_coroutine.retry_with(after=after)(thing2), ) # There's no waiting on retry, only a wait in the coroutine, so the # executions should be interleaved. even_thing_attempts = attempts[::2] things, attempt_nos1 = zip(*even_thing_attempts) assert len(set(things)) == 1 assert list(attempt_nos1) == [1, 2, 3] odd_thing_attempts = attempts[1::2] things, attempt_nos2 = zip(*odd_thing_attempts) assert len(set(things)) == 1 assert list(attempt_nos2) == [1, 2, 3] class TestAsyncEnabled(unittest.TestCase): @asynctest async def test_enabled_false_skips_retry(self) -> None: """When enabled=False, async function is called directly without retrying.""" call_count = 0 @retry(enabled=False, stop=stop_after_attempt(3)) async def always_fails() -> None: nonlocal call_count call_count += 1 raise ValueError("fail") with pytest.raises(ValueError, match="fail"): await always_fails() assert call_count == 1 @unittest.skipIf(not have_trio, "trio not installed") class TestTrio(unittest.TestCase): def test_trio_basic(self) -> None: thing = NoIOErrorAfterCount(5) @retry async def trio_function() -> Any: await trio.sleep(0.00001) return thing.go() trio.run(trio_function) assert thing.counter == thing.count class TestContextManager(unittest.TestCase): @asynctest async def test_do_max_attempts(self) -> None: attempts = 0 retrying = tasyncio.AsyncRetrying(stop=stop_after_attempt(3)) try: async for attempt in retrying: with attempt: attempts += 1 raise Exception except RetryError: pass assert attempts == 3 @asynctest async def test_async_with_attempt_manager(self) -> None: """AttemptManager supports async with for use inside async for.""" attempts = 0 retrying = tasyncio.AsyncRetrying(stop=stop_after_attempt(3)) try: async for attempt in retrying: async with attempt: attempts += 1 raise Exception except RetryError: pass assert attempts == 3 @asynctest async def test_reraise(self) -> None: class CustomError(Exception): pass try: async for attempt in tasyncio.AsyncRetrying( stop=stop_after_attempt(1), reraise=True ): with attempt: raise CustomError except CustomError: pass else: raise Exception @asynctest async def test_sleeps(self) -> None: start = current_time_ms() try: async for attempt in tasyncio.AsyncRetrying( stop=stop_after_attempt(1), wait=wait_fixed(1) ): with attempt: raise Exception except RetryError: pass t = current_time_ms() - start self.assertLess(t, 1.1) @asynctest async def test_retry_with_result(self) -> None: async def test() -> int: attempts = 0 # 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: attempts += 1 attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_retry_with_async_result(self) -> None: async def test() -> int: attempts = 0 async def lt_3(x: float) -> bool: return x < 3 async for attempt in tasyncio.AsyncRetrying( retry=tasyncio.retry_if_result(lt_3) ): with attempt: attempts += 1 assert attempt.retry_state.outcome # help mypy if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_retry_with_async_exc(self) -> None: async def test() -> int: attempts = 0 class CustomException(Exception): pass async def is_exc(e: BaseException) -> bool: return isinstance(e, CustomException) async for attempt in tasyncio.AsyncRetrying( retry=tasyncio.retry_if_exception(is_exc) ): with attempt: attempts += 1 if attempts < 3: raise CustomException assert attempt.retry_state.outcome # help mypy if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_retry_with_async_result_or(self) -> None: async def test() -> int: attempts = 0 async def lt_3(x: float) -> bool: return x < 3 class CustomException(Exception): pass def is_exc(e: BaseException) -> bool: return isinstance(e, CustomException) retry_strategy = tasyncio.retry_if_result(lt_3) | retry_if_exception(is_exc) async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy): with attempt: attempts += 1 if 2 < attempts < 4: raise CustomException assert attempt.retry_state.outcome # help mypy if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(4, result) @asynctest async def test_retry_with_async_result_ror(self) -> None: async def test() -> int: attempts = 0 def lt_3(x: float) -> bool: return x < 3 class CustomException(Exception): pass async def is_exc(e: BaseException) -> bool: return isinstance(e, CustomException) retry_strategy = retry_if_result(lt_3) | tasyncio.retry_if_exception(is_exc) async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy): with attempt: attempts += 1 if 2 < attempts < 4: raise CustomException assert attempt.retry_state.outcome # help mypy if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(4, result) @asynctest async def test_retry_with_async_result_and(self) -> None: async def test() -> int: attempts = 0 async def lt_3(x: float) -> bool: return x < 3 def gt_0(x: float) -> bool: return x > 0 retry_strategy = tasyncio.retry_if_result(lt_3) & retry_if_result(gt_0) async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy): with attempt: attempts += 1 attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_retry_with_async_result_rand(self) -> None: async def test() -> int: attempts = 0 async def lt_3(x: float) -> bool: return x < 3 def gt_0(x: float) -> bool: return x > 0 retry_strategy = retry_if_result(gt_0) & tasyncio.retry_if_result(lt_3) async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy): with attempt: attempts += 1 attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_async_retying_iterator(self) -> None: thing = NoIOErrorAfterCount(5) with pytest.raises(TypeError): for attempts in AsyncRetrying(): with attempts: await _async_function(thing) class TestDecoratorWrapper(unittest.TestCase): @asynctest async def test_retry_function_attributes(self) -> None: """Test that the wrapped function attributes are exposed as intended. - statistics contains the value for the latest function run - retry object can be modified to change its behaviour (useful to patch in tests) - retry object statistics are synced with function statistics """ self.assertTrue( await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(1)) ) expected_stats = { "attempt_number": 2, "delay_since_first_attempt": mock.ANY, "idle_for": mock.ANY, "start_time": mock.ANY, } self.assertEqual( _retryable_coroutine_with_2_attempts.statistics, expected_stats, ) self.assertEqual( _retryable_coroutine_with_2_attempts.retry.statistics, expected_stats, ) with mock.patch.object( _retryable_coroutine_with_2_attempts.retry, "stop", tenacity.stop_after_attempt(1), ): try: self.assertTrue( await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(2)) ) except RetryError as exc: expected_stats = { "attempt_number": 1, "delay_since_first_attempt": mock.ANY, "idle_for": mock.ANY, "start_time": mock.ANY, } self.assertEqual( _retryable_coroutine_with_2_attempts.statistics, expected_stats, ) self.assertEqual(exc.last_attempt.attempt_number, 1) self.assertEqual( _retryable_coroutine_with_2_attempts.retry.statistics, expected_stats, ) else: self.fail("RetryError should have been raised after 1 attempt") # 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() -> None: pass class TestSyncFunctionWithAsyncSleep(unittest.TestCase): @asynctest async def test_sync_function_with_async_sleep(self) -> None: """A sync function with an async sleep callable uses AsyncRetrying.""" mock_sleep = mock.AsyncMock() thing = NoneReturnUntilAfterCount(2) @retry( sleep=mock_sleep, wait=wait_fixed(1), retry=retry_if_result(lambda x: x is None), ) def sync_function() -> Any: return thing.go() result = await sync_function() assert result is True assert mock_sleep.await_count == 2 if __name__ == "__main__": unittest.main() ================================================ FILE: tests/test_issue_478.py ================================================ import asyncio import typing import unittest from functools import wraps from tenacity import RetryCallState, retry def asynctest( callable_: typing.Callable[..., typing.Any], ) -> typing.Callable[..., typing.Any]: @wraps(callable_) def wrapper(*a: typing.Any, **kw: typing.Any) -> typing.Any: return asyncio.run(callable_(*a, **kw)) return wrapper MAX_RETRY_FIX_ATTEMPTS = 2 class TestIssue478(unittest.TestCase): def test_issue(self) -> None: results = [] def do_retry(retry_state: RetryCallState) -> bool: outcome = retry_state.outcome assert outcome ex = outcome.exception() _subject_: str = retry_state.args[0] if _subject_ == "Fix": # no retry on fix failure return False if retry_state.attempt_number >= MAX_RETRY_FIX_ATTEMPTS: return False if ex: do_fix_work() return True return False @retry(reraise=True, retry=do_retry) def _do_work(subject: str) -> None: if subject == "Error": results.append(f"{subject} is not working") raise Exception(f"{subject} is not working") results.append(f"{subject} is working") def do_any_work(subject: str) -> None: _do_work(subject) def do_fix_work() -> None: _do_work("Fix") try: do_any_work("Error") except Exception as exc: assert str(exc) == "Error is not working" else: raise AssertionError("No exception caught") assert results == [ "Error is not working", "Fix is working", "Error is not working", ] @asynctest async def test_async(self) -> None: results = [] async def do_retry(retry_state: RetryCallState) -> bool: outcome = retry_state.outcome assert outcome ex = outcome.exception() _subject_: str = retry_state.args[0] if _subject_ == "Fix": # no retry on fix failure return False if retry_state.attempt_number >= MAX_RETRY_FIX_ATTEMPTS: return False if ex: await do_fix_work() return True return False @retry(reraise=True, retry=do_retry) async def _do_work(subject: str) -> None: if subject == "Error": results.append(f"{subject} is not working") raise Exception(f"{subject} is not working") results.append(f"{subject} is working") async def do_any_work(subject: str) -> None: await _do_work(subject) async def do_fix_work() -> None: await _do_work("Fix") try: await do_any_work("Error") except Exception as exc: assert str(exc) == "Error is not working" else: raise AssertionError("No exception caught") assert results == [ "Error is not working", "Fix is working", "Error is not working", ] ================================================ FILE: tests/test_tenacity.py ================================================ # Copyright 2016–2021 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import contextlib import datetime import logging import pickle import re import time import typing import unittest from fractions import Fraction from unittest import mock import pytest from typeguard import check_type import tenacity from tenacity import RetryCallState, RetryError, Retrying, retry from tenacity.retry import retry_all, retry_any _unset = object() def _make_unset_exception(func_name: str, **kwargs: typing.Any) -> TypeError: missing = [] for k, v in kwargs.items(): if v is _unset: missing.append(k) missing_str = ", ".join(repr(s) for s in missing) return TypeError(func_name + " func missing parameters: " + missing_str) def _set_delay_since_start(retry_state: RetryCallState, delay: typing.Any) -> None: # Ensure outcome_timestamp - start_time is *exactly* equal to the delay to # avoid complexity in test code. retry_state.start_time = Fraction(retry_state.start_time) # type: ignore[assignment] retry_state.outcome_timestamp = retry_state.start_time + Fraction(delay) assert retry_state.seconds_since_start == delay def make_retry_state( previous_attempt_number: typing.Any, delay_since_first_attempt: typing.Any, last_result: typing.Any = None, upcoming_sleep: typing.Any = 0, ) -> RetryCallState: """Construct RetryCallState for given attempt number & delay. Only used in testing and thus is extra careful about timestamp arithmetics. """ required_parameter_unset = ( previous_attempt_number is _unset or delay_since_first_attempt is _unset ) if required_parameter_unset: raise _make_unset_exception( "wait/stop", previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt, ) retry_state = RetryCallState(None, None, (), {}) # type: ignore[arg-type] retry_state.attempt_number = previous_attempt_number if last_result is not None: retry_state.outcome = last_result else: retry_state.set_result(None) retry_state.upcoming_sleep = upcoming_sleep _set_delay_since_start(retry_state, delay_since_first_attempt) return retry_state class TestBase(unittest.TestCase): def test_retrying_repr(self) -> None: class ConcreteRetrying(tenacity.BaseRetrying): def __call__( self, fn: typing.Any, *args: typing.Any, **kwargs: typing.Any ) -> typing.Any: pass repr(ConcreteRetrying()) def test_callstate_repr(self) -> None: rs = RetryCallState(None, None, (), {}) # type: ignore[arg-type] rs.idle_for = 1.1111111 assert repr(rs).endswith("attempt #1; slept for 1.11; last result: none yet>") 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)>" ) class TestRetryingName(unittest.TestCase): def test_str_default(self) -> None: """Without a name, str() returns ''.""" assert str(Retrying()) == "" def test_str_with_name(self) -> None: """With a name, str() returns the given name.""" assert str(Retrying(name="my_block")) == "my_block" def test_str_preserved_by_copy(self) -> None: """copy() preserves the name.""" r = Retrying(name="my_block") assert str(r.copy()) == "my_block" def test_str_overridden_by_copy(self) -> None: """copy() allows overriding the name.""" r = Retrying(name="original") assert str(r.copy(name="overridden")) == "overridden" def test_get_fn_name_decorator(self) -> None: """get_fn_name() returns the function's qualified name when used as decorator.""" captured: list[RetryCallState] = [] @tenacity.retry( stop=tenacity.stop_after_attempt(1), after=lambda rs: captured.append(rs), ) def my_func() -> None: raise ValueError with contextlib.suppress(Exception): my_func() assert captured assert "my_func" in captured[0].get_fn_name() def test_get_fn_name_context_manager_no_name(self) -> None: """get_fn_name() returns '' in context manager mode without a name.""" r = Retrying(stop=tenacity.stop_after_attempt(1)) rs = RetryCallState(r, None, (), {}) assert rs.get_fn_name() == "" def test_get_fn_name_context_manager_with_name(self) -> None: """get_fn_name() returns the given name in context manager mode.""" r = Retrying(name="ws_listener", stop=tenacity.stop_after_attempt(1)) rs = RetryCallState(r, None, (), {}) assert rs.get_fn_name() == "ws_listener" def test_logging_uses_name(self) -> None: """before_log uses the name parameter in context manager mode.""" import unittest.mock log = unittest.mock.MagicMock() logger = unittest.mock.MagicMock(log=log) with contextlib.suppress(Exception): for attempt in Retrying( name="my_block", before=tenacity.before_log(logger, logging.INFO), stop=tenacity.stop_after_attempt(1), ): with attempt: raise ValueError args = log.call_args[0] assert "my_block" in args[1] class TestStopConditions(unittest.TestCase): def test_never_stop(self) -> None: r = Retrying() self.assertFalse(r.stop(make_retry_state(3, 6546))) def test_stop_any(self) -> None: stop = tenacity.stop_any( tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4) ) def s(*args: typing.Any) -> bool: return stop(make_retry_state(*args)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertTrue(s(4, 0.8)) self.assertTrue(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_all(self) -> None: stop = tenacity.stop_all( tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4) ) def s(*args: typing.Any) -> bool: return stop(make_retry_state(*args)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertFalse(s(4, 0.8)) self.assertFalse(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_or(self) -> None: stop = tenacity.stop_after_delay(1) | tenacity.stop_after_attempt(4) def s(*args: typing.Any) -> bool: return stop(make_retry_state(*args)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertTrue(s(4, 0.8)) self.assertTrue(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_and(self) -> None: stop = tenacity.stop_after_delay(1) & tenacity.stop_after_attempt(4) def s(*args: typing.Any) -> bool: return stop(make_retry_state(*args)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertFalse(s(4, 0.8)) self.assertFalse(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_after_attempt(self) -> None: r = Retrying(stop=tenacity.stop_after_attempt(3)) self.assertFalse(r.stop(make_retry_state(2, 6546))) self.assertTrue(r.stop(make_retry_state(3, 6546))) self.assertTrue(r.stop(make_retry_state(4, 6546))) def test_stop_after_delay(self) -> None: for delay in (1, datetime.timedelta(seconds=1)): with self.subTest(): r = Retrying(stop=tenacity.stop_after_delay(delay)) self.assertFalse(r.stop(make_retry_state(2, 0.999))) self.assertTrue(r.stop(make_retry_state(2, 1))) self.assertTrue(r.stop(make_retry_state(2, 1.001))) def test_stop_before_delay(self) -> None: 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) -> None: Retrying(stop="stop_after_attempt") # type: ignore[arg-type] def test_stop_func_with_retry_state(self) -> None: def stop_func(retry_state: RetryCallState) -> bool: rs = retry_state return rs.attempt_number == rs.seconds_since_start r = Retrying(stop=stop_func) self.assertFalse(r.stop(make_retry_state(1, 3))) self.assertFalse(r.stop(make_retry_state(100, 99))) self.assertTrue(r.stop(make_retry_state(101, 101))) class TestWaitConditions(unittest.TestCase): def test_no_sleep(self) -> None: r = Retrying() self.assertEqual(0, r.wait(make_retry_state(18, 9879))) def test_fixed_sleep(self) -> None: for wait in (1, datetime.timedelta(seconds=1)): with self.subTest(): r = Retrying(wait=tenacity.wait_fixed(wait)) self.assertEqual(1, r.wait(make_retry_state(12, 6546))) def test_incrementing_sleep(self) -> None: 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) ) self.assertEqual(500, r.wait(make_retry_state(1, 6546))) self.assertEqual(600, r.wait(make_retry_state(2, 6546))) self.assertEqual(700, r.wait(make_retry_state(3, 6546))) def test_random_sleep(self) -> None: for min_, max_ in ( (1, 20), (datetime.timedelta(seconds=1), datetime.timedelta(seconds=20)), ): with self.subTest(): r = Retrying(wait=tenacity.wait_random(min=min_, max=max_)) times = set() for _ in range(1000): times.add(r.wait(make_retry_state(1, 6546))) # this is kind of non-deterministic... self.assertTrue(len(times) > 1) for t in times: self.assertTrue(t >= 1) self.assertTrue(t < 20) def test_random_sleep_withoutmin_(self) -> None: r = Retrying(wait=tenacity.wait_random(max=2)) times = set() times.add(r.wait(make_retry_state(1, 6546))) times.add(r.wait(make_retry_state(1, 6546))) times.add(r.wait(make_retry_state(1, 6546))) times.add(r.wait(make_retry_state(1, 6546))) # this is kind of non-deterministic... self.assertTrue(len(times) > 1) for t in times: self.assertTrue(t >= 0) self.assertTrue(t <= 2) def test_exponential(self) -> None: r = Retrying(wait=tenacity.wait_exponential()) self.assertEqual(r.wait(make_retry_state(1, 0)), 1) self.assertEqual(r.wait(make_retry_state(2, 0)), 2) self.assertEqual(r.wait(make_retry_state(3, 0)), 4) self.assertEqual(r.wait(make_retry_state(4, 0)), 8) self.assertEqual(r.wait(make_retry_state(5, 0)), 16) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 64) self.assertEqual(r.wait(make_retry_state(8, 0)), 128) def test_exponential_with_max_wait(self) -> None: r = Retrying(wait=tenacity.wait_exponential(max=40)) self.assertEqual(r.wait(make_retry_state(1, 0)), 1) self.assertEqual(r.wait(make_retry_state(2, 0)), 2) self.assertEqual(r.wait(make_retry_state(3, 0)), 4) self.assertEqual(r.wait(make_retry_state(4, 0)), 8) self.assertEqual(r.wait(make_retry_state(5, 0)), 16) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 40) self.assertEqual(r.wait(make_retry_state(8, 0)), 40) self.assertEqual(r.wait(make_retry_state(50, 0)), 40) def test_exponential_with_min_wait(self) -> None: r = Retrying(wait=tenacity.wait_exponential(min=20)) self.assertEqual(r.wait(make_retry_state(1, 0)), 20) self.assertEqual(r.wait(make_retry_state(2, 0)), 20) self.assertEqual(r.wait(make_retry_state(3, 0)), 20) self.assertEqual(r.wait(make_retry_state(4, 0)), 20) self.assertEqual(r.wait(make_retry_state(5, 0)), 20) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 64) self.assertEqual(r.wait(make_retry_state(8, 0)), 128) self.assertEqual(r.wait(make_retry_state(20, 0)), 524288) def test_exponential_with_max_wait_and_multiplier(self) -> None: r = Retrying(wait=tenacity.wait_exponential(max=50, multiplier=1)) self.assertEqual(r.wait(make_retry_state(1, 0)), 1) self.assertEqual(r.wait(make_retry_state(2, 0)), 2) self.assertEqual(r.wait(make_retry_state(3, 0)), 4) self.assertEqual(r.wait(make_retry_state(4, 0)), 8) self.assertEqual(r.wait(make_retry_state(5, 0)), 16) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 50) self.assertEqual(r.wait(make_retry_state(8, 0)), 50) self.assertEqual(r.wait(make_retry_state(50, 0)), 50) def test_exponential_with_min_wait_and_multiplier(self) -> None: r = Retrying(wait=tenacity.wait_exponential(min=20, multiplier=2)) self.assertEqual(r.wait(make_retry_state(1, 0)), 20) self.assertEqual(r.wait(make_retry_state(2, 0)), 20) self.assertEqual(r.wait(make_retry_state(3, 0)), 20) self.assertEqual(r.wait(make_retry_state(4, 0)), 20) self.assertEqual(r.wait(make_retry_state(5, 0)), 32) self.assertEqual(r.wait(make_retry_state(6, 0)), 64) self.assertEqual(r.wait(make_retry_state(7, 0)), 128) self.assertEqual(r.wait(make_retry_state(8, 0)), 256) self.assertEqual(r.wait(make_retry_state(20, 0)), 1048576) def test_exponential_with_min_wait_andmax__wait(self) -> None: for min_, max_ in ( (10, 100), (datetime.timedelta(seconds=10), datetime.timedelta(seconds=100)), ): with self.subTest(): r = Retrying(wait=tenacity.wait_exponential(min=min_, max=max_)) self.assertEqual(r.wait(make_retry_state(1, 0)), 10) self.assertEqual(r.wait(make_retry_state(2, 0)), 10) self.assertEqual(r.wait(make_retry_state(3, 0)), 10) self.assertEqual(r.wait(make_retry_state(4, 0)), 10) self.assertEqual(r.wait(make_retry_state(5, 0)), 16) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 64) self.assertEqual(r.wait(make_retry_state(8, 0)), 100) self.assertEqual(r.wait(make_retry_state(9, 0)), 100) self.assertEqual(r.wait(make_retry_state(20, 0)), 100) def test_legacy_explicit_wait_type(self) -> None: Retrying(wait="exponential_sleep") # type: ignore[arg-type] def test_wait_func(self) -> None: def wait_func(retry_state: RetryCallState) -> typing.Any: return retry_state.attempt_number * retry_state.seconds_since_start # type: ignore[operator] r = Retrying(wait=wait_func) self.assertEqual(r.wait(make_retry_state(1, 5)), 5) self.assertEqual(r.wait(make_retry_state(2, 11)), 22) self.assertEqual(r.wait(make_retry_state(10, 100)), 1000) def test_wait_combine(self) -> None: r = Retrying( wait=tenacity.wait_combine( tenacity.wait_random(0, 3), tenacity.wait_fixed(5) ) ) # Test it a few time since it's random for _i in range(1000): w = r.wait(make_retry_state(1, 5)) self.assertLess(w, 8) self.assertGreaterEqual(w, 5) def test_wait_exception(self) -> None: def predicate(exc: BaseException) -> float: if isinstance(exc, ValueError): return 3.5 return 10.0 r = Retrying(wait=tenacity.wait_exception(predicate)) fut1 = tenacity.Future.construct(1, ValueError(), True) self.assertEqual(r.wait(make_retry_state(1, 0, last_result=fut1)), 3.5) fut2 = tenacity.Future.construct(1, KeyError(), True) self.assertEqual(r.wait(make_retry_state(1, 0, last_result=fut2)), 10.0) fut3 = tenacity.Future.construct(1, None, False) with self.assertRaises(RuntimeError): r.wait(make_retry_state(1, 0, last_result=fut3)) def test_wait_double_sum(self) -> None: r = Retrying(wait=tenacity.wait_random(0, 3) + tenacity.wait_fixed(5)) # Test it a few time since it's random for _i in range(1000): w = r.wait(make_retry_state(1, 5)) self.assertLess(w, 8) self.assertGreaterEqual(w, 5) def test_wait_triple_sum(self) -> None: 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 for _i in range(1000): w = r.wait(make_retry_state(1, 5)) self.assertLess(w, 9) self.assertGreaterEqual(w, 6) def test_wait_arbitrary_sum(self) -> None: r = Retrying( wait=sum( # type: ignore[arg-type] [ tenacity.wait_fixed(1), # type: ignore[list-item] tenacity.wait_random(0, 3), # type: ignore[list-item] tenacity.wait_fixed(5), # type: ignore[list-item] tenacity.wait_none(), # type: ignore[list-item] ] ) ) # Test it a few time since it's random for _ in range(1000): w = r.wait(make_retry_state(1, 5)) self.assertLess(w, 9) self.assertGreaterEqual(w, 6) def _assert_range(self, wait: float, min_: float, max_: float) -> None: self.assertLess(wait, max_) self.assertGreaterEqual(wait, min_) def _assert_inclusive_range(self, wait: float, low: float, high: float) -> None: self.assertLessEqual(wait, high) self.assertGreaterEqual(wait, low) def _assert_inclusive_epsilon( self, wait: float, target: float, epsilon: float ) -> None: self.assertLessEqual(wait, target + epsilon) self.assertGreaterEqual(wait, target - epsilon) def test_wait_chain(self) -> None: r = Retrying( wait=tenacity.wait_chain( *[tenacity.wait_fixed(1) for i in range(2)] + [tenacity.wait_fixed(4) for i in range(2)] + [tenacity.wait_fixed(8) for i in range(1)] ) ) for i in range(10): w = r.wait(make_retry_state(i + 1, 1)) if i < 2: self._assert_range(w, 1, 2) elif i < 4: self._assert_range(w, 4, 5) else: self._assert_range(w, 8, 9) def test_wait_chain_multiple_invocations(self) -> None: sleep_intervals: list[float] = [] r = Retrying( sleep=sleep_intervals.append, wait=tenacity.wait_chain(*[tenacity.wait_fixed(i + 1) for i in range(3)]), stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_if_result(lambda x: x == 1), ) @r.wraps def always_return_1() -> int: return 1 self.assertRaises(tenacity.RetryError, always_return_1) self.assertEqual(sleep_intervals, [1.0, 2.0, 3.0, 3.0]) sleep_intervals[:] = [] # Clear and restart retrying. self.assertRaises(tenacity.RetryError, always_return_1) self.assertEqual(sleep_intervals, [1.0, 2.0, 3.0, 3.0]) sleep_intervals[:] = [] def test_wait_random_exponential(self) -> None: fn = tenacity.wait_random_exponential(0.5, 60.0) for _ in range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0, 0.5) self._assert_inclusive_range(fn(make_retry_state(2, 0)), 0, 1.0) self._assert_inclusive_range(fn(make_retry_state(3, 0)), 0, 2.0) self._assert_inclusive_range(fn(make_retry_state(4, 0)), 0, 4.0) self._assert_inclusive_range(fn(make_retry_state(5, 0)), 0, 8.0) self._assert_inclusive_range(fn(make_retry_state(6, 0)), 0, 16.0) self._assert_inclusive_range(fn(make_retry_state(7, 0)), 0, 32.0) self._assert_inclusive_range(fn(make_retry_state(8, 0)), 0, 60.0) self._assert_inclusive_range(fn(make_retry_state(9, 0)), 0, 60.0) # max wait max_wait = 5 fn = tenacity.wait_random_exponential(10, max_wait) for _ in range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0.00, max_wait) # min wait min_wait = 5 fn = tenacity.wait_random_exponential(min=min_wait) for _ in range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), min_wait, 5) # Default arguments exist fn = tenacity.wait_random_exponential() fn(make_retry_state(0, 0)) def test_wait_random_exponential_statistically(self) -> None: fn = tenacity.wait_random_exponential(0.5, 60.0) attempt = [[fn(make_retry_state(i, 0)) for _ in range(4000)] for i in range(10)] def mean(lst: list[float]) -> float: return float(sum(lst)) / float(len(lst)) # skipping attempt 0 self._assert_inclusive_epsilon(mean(attempt[1]), 0.25, 0.02) self._assert_inclusive_epsilon(mean(attempt[2]), 0.50, 0.04) self._assert_inclusive_epsilon(mean(attempt[3]), 1, 0.08) self._assert_inclusive_epsilon(mean(attempt[4]), 2, 0.16) self._assert_inclusive_epsilon(mean(attempt[5]), 4, 0.32) self._assert_inclusive_epsilon(mean(attempt[6]), 8, 0.64) self._assert_inclusive_epsilon(mean(attempt[7]), 16, 1.28) self._assert_inclusive_epsilon(mean(attempt[8]), 30, 2.56) self._assert_inclusive_epsilon(mean(attempt[9]), 30, 2.56) def test_wait_exponential_jitter(self) -> None: fn = tenacity.wait_exponential_jitter(max=60) for _ in range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), 1, 2) self._assert_inclusive_range(fn(make_retry_state(2, 0)), 2, 3) self._assert_inclusive_range(fn(make_retry_state(3, 0)), 4, 5) self._assert_inclusive_range(fn(make_retry_state(4, 0)), 8, 9) self._assert_inclusive_range(fn(make_retry_state(5, 0)), 16, 17) self._assert_inclusive_range(fn(make_retry_state(6, 0)), 32, 33) self.assertEqual(fn(make_retry_state(7, 0)), 60) self.assertEqual(fn(make_retry_state(8, 0)), 60) self.assertEqual(fn(make_retry_state(9, 0)), 60) with self.assertWarns(DeprecationWarning): fn = tenacity.wait_exponential_jitter(10, 5) for _ in range(1000): self.assertEqual(fn(make_retry_state(1, 0)), 5) # Default arguments exist fn = tenacity.wait_exponential_jitter() fn(make_retry_state(0, 0)) def test_wait_exponential_jitter_min(self) -> None: fn = tenacity.wait_exponential_jitter(initial=1, max=60, jitter=1, min=5) for _ in range(1000): # Even for attempt 1 (base wait=1 + jitter 0..1 = 1..2), min=5 applies self._assert_inclusive_range(fn(make_retry_state(1, 0)), 5, 5) self._assert_inclusive_range(fn(make_retry_state(2, 0)), 5, 5) self._assert_inclusive_range(fn(make_retry_state(3, 0)), 5, 5) # For attempt 4, base wait=8 + jitter 0..1 = 8..9, above min self._assert_inclusive_range(fn(make_retry_state(4, 0)), 8, 9) def test_wait_exponential_jitter_timedelta(self) -> None: from datetime import timedelta fn = tenacity.wait_exponential_jitter( max=timedelta(seconds=60), jitter=timedelta(seconds=1), min=timedelta(seconds=5), ) for _ in range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), 5, 5) self._assert_inclusive_range(fn(make_retry_state(5, 0)), 16, 17) self.assertEqual(fn(make_retry_state(7, 0)), 60) def test_wait_exponential_jitter_multiplier(self) -> None: fn = tenacity.wait_exponential_jitter(multiplier=10, max=60, jitter=0) self.assertEqual(fn(make_retry_state(1, 0)), 10) self.assertEqual(fn(make_retry_state(2, 0)), 20) self.assertEqual(fn(make_retry_state(3, 0)), 40) self.assertEqual(fn(make_retry_state(4, 0)), 60) def test_wait_exponential_jitter_initial_deprecated(self) -> None: with self.assertWarns(DeprecationWarning): fn = tenacity.wait_exponential_jitter(initial=10, max=60, jitter=0) self.assertEqual(fn(make_retry_state(1, 0)), 10) self.assertEqual(fn(make_retry_state(2, 0)), 20) def test_wait_exponential_jitter_initial_and_multiplier_raises(self) -> None: with self.assertRaises(ValueError): tenacity.wait_exponential_jitter(initial=5, multiplier=10) def test_wait_retry_state_attributes(self) -> None: class ExtractCallState(Exception): pass # retry_state is mutable, so return it as an exception to extract the # exact values it has when wait is called and bypass any other logic. def waitfunc(retry_state: RetryCallState) -> float: raise ExtractCallState(retry_state) retrying = Retrying( wait=waitfunc, retry=( tenacity.retry_if_exception_type() | tenacity.retry_if_result(lambda result: result == 123) ), ) def returnval() -> int: return 123 try: retrying(returnval) except ExtractCallState as err: retry_state = err.args[0] self.assertIs(retry_state.fn, returnval) self.assertEqual(retry_state.args, ()) self.assertEqual(retry_state.kwargs, {}) self.assertEqual(retry_state.outcome.result(), 123) self.assertEqual(retry_state.attempt_number, 1) self.assertGreaterEqual(retry_state.outcome_timestamp, retry_state.start_time) def dying() -> None: raise Exception("Broken") try: retrying(dying) except ExtractCallState as err: retry_state = err.args[0] self.assertIs(retry_state.fn, dying) self.assertEqual(retry_state.args, ()) self.assertEqual(retry_state.kwargs, {}) self.assertEqual(str(retry_state.outcome.exception()), "Broken") self.assertEqual(retry_state.attempt_number, 1) self.assertGreaterEqual(retry_state.outcome_timestamp, retry_state.start_time) class TestRetryConditions(unittest.TestCase): def test_retry_if_result(self) -> None: retry = tenacity.retry_if_result(lambda x: x == 1) def r(fut: tenacity.Future) -> bool: retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) def test_retry_if_not_result(self) -> None: retry = tenacity.retry_if_not_result(lambda x: x == 1) def r(fut: tenacity.Future) -> bool: retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, False))) def test_retry_any(self) -> None: retry = tenacity.retry_any( tenacity.retry_if_result(lambda x: x == 1), tenacity.retry_if_result(lambda x: x == 2), ) def r(fut: tenacity.Future) -> bool: retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertTrue(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_all(self) -> None: retry = tenacity.retry_all( tenacity.retry_if_result(lambda x: x == 1), tenacity.retry_if_result(lambda x: isinstance(x, int)), ) def r(fut: tenacity.Future) -> bool: retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_and(self) -> None: retry = tenacity.retry_if_result(lambda x: x == 1) & tenacity.retry_if_result( lambda x: isinstance(x, int) ) def r(fut: tenacity.Future) -> bool: retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_or(self) -> None: retry = tenacity.retry_if_result( lambda x: x == "foo" ) | tenacity.retry_if_result(lambda x: isinstance(x, int)) def r(fut: tenacity.Future) -> bool: retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, "foo", False))) self.assertFalse(r(tenacity.Future.construct(1, "foobar", False))) self.assertFalse(r(tenacity.Future.construct(1, 2.2, False))) self.assertFalse(r(tenacity.Future.construct(1, 42, True))) def test_retry_or_with_plain_function(self) -> None: """Plain callables can be composed with retry_base via |.""" def my_retry(retry_state: tenacity.RetryCallState) -> bool: return retry_state.outcome is not None and not retry_state.outcome.failed # retry_base | plain_callable (exercises __or__ fallback) retry = tenacity.retry_if_exception_type(Exception) | my_retry retry_state = make_retry_state( 1, 1.0, last_result=tenacity.Future.construct(1, "ok", False) ) self.assertTrue(retry(retry_state)) # plain_callable | retry_base (exercises __ror__ via reflection) retry2 = my_retry | tenacity.retry_if_exception_type(Exception) self.assertTrue(retry2(retry_state)) def test_retry_and_with_plain_function(self) -> None: """Plain callables can be composed with retry_base via &.""" def my_retry(retry_state: tenacity.RetryCallState) -> bool: return True # retry_base & plain_callable (exercises __and__ fallback) retry = tenacity.retry_if_result(lambda x: x == 1) & my_retry retry_state = make_retry_state( 1, 1.0, last_result=tenacity.Future.construct(1, 1, False) ) self.assertTrue(retry(retry_state)) # plain_callable & retry_base (exercises __rand__ via reflection) retry2 = my_retry & tenacity.retry_if_result(lambda x: x == 1) self.assertTrue(retry2(retry_state)) def test_retry_or_coalesces(self) -> None: """Multiple | operations flatten into a single retry_any.""" a = tenacity.retry_if_exception_type(IOError) b = tenacity.retry_if_exception_type(OSError) c = tenacity.retry_if_exception_type(ValueError) combined = a | b | c self.assertIsInstance(combined, retry_any) self.assertEqual(len(combined.retries), 3) def test_retry_and_coalesces(self) -> None: """Multiple & operations flatten into a single retry_all.""" a = tenacity.retry_if_result(lambda x: x == 1) b = tenacity.retry_if_result(lambda x: x > 0) c = tenacity.retry_if_result(lambda x: x < 10) combined = a & b & c self.assertIsInstance(combined, retry_all) self.assertEqual(len(combined.retries), 3) def _raise_try_again(self) -> None: self._attempts += 1 if self._attempts < 3: raise tenacity.TryAgain def test_retry_try_again(self) -> None: self._attempts = 0 Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never)( self._raise_try_again ) self.assertEqual(3, self._attempts) def test_retry_try_again_forever(self) -> None: def _r() -> None: raise tenacity.TryAgain r = Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never) self.assertRaises(tenacity.RetryError, r, _r) self.assertEqual(5, r.statistics["attempt_number"]) def test_retry_try_again_forever_reraise(self) -> None: def _r() -> None: raise tenacity.TryAgain r = Retrying( stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never, reraise=True, ) self.assertRaises(tenacity.TryAgain, r, _r) self.assertEqual(5, r.statistics["attempt_number"]) def test_retry_if_exception_message_negative_no_inputs(self) -> None: with self.assertRaises(TypeError): tenacity.retry_if_exception_message() def test_retry_if_exception_message_negative_too_many_inputs(self) -> None: with self.assertRaises(TypeError): tenacity.retry_if_exception_message(message="negative", match="negative") class NoneReturnUntilAfterCount: """Holds counter state for invoking a method several times in a row.""" def __init__(self, count: int) -> None: self.counter = 0 self.count = count def go(self) -> typing.Any: """Return None until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 return None return True class NoIOErrorAfterCount: """Holds counter state for invoking a method several times in a row.""" def __init__(self, count: int) -> None: self.counter = 0 self.count = count def go(self) -> typing.Any: """Raise an IOError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise OSError("Hi there, I'm an IOError") return True class NoNameErrorAfterCount: """Holds counter state for invoking a method several times in a row.""" def __init__(self, count: int) -> None: self.counter = 0 self.count = count def go(self) -> typing.Any: """Raise a NameError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise NameError("Hi there, I'm a NameError") return True class NoNameErrorCauseAfterCount: """Holds counter state for invoking a method several times in a row.""" def __init__(self, count: int) -> None: self.counter = 0 self.count = count def go2(self) -> typing.Any: raise NameError("Hi there, I'm a NameError") def go(self) -> typing.Any: """Raise an IOError with a NameError as cause until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 try: self.go2() except NameError as e: raise OSError from e return True class NoIOErrorCauseAfterCount: """Holds counter state for invoking a method several times in a row.""" def __init__(self, count: int) -> None: self.counter = 0 self.count = count def go2(self) -> typing.Any: raise OSError("Hi there, I'm an IOError") def go(self) -> typing.Any: """Raise a NameError with an IOError as cause until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 try: self.go2() except OSError as e: raise NameError from e return True class NameErrorUntilCount: """Holds counter state for invoking a method several times in a row.""" derived_message = "Hi there, I'm a NameError" def __init__(self, count: int) -> None: self.counter = 0 self.count = count def go(self) -> typing.Any: """Return True until after count threshold has been crossed. Then raise a NameError. """ if self.counter < self.count: self.counter += 1 return True raise NameError(self.derived_message) class IOErrorUntilCount: """Holds counter state for invoking a method several times in a row.""" def __init__(self, count: int) -> None: self.counter = 0 self.count = count def go(self) -> typing.Any: """Return True until after count threshold has been crossed. Then raise an IOError. """ if self.counter < self.count: self.counter += 1 return True raise OSError("Hi there, I'm an IOError") class CustomError(Exception): """This is a custom exception class. Note that For Python 2.x, we don't strictly need to extend BaseException, however, Python 3.x will complain. While this test suite won't run correctly under Python 3.x without extending from the Python exception hierarchy, the actual module code is backwards compatible Python 2.x and will allow for cases where exception classes don't extend from the hierarchy. """ def __init__(self, value: str) -> None: self.value = value def __str__(self) -> str: return self.value class NoCustomErrorAfterCount: """Holds counter state for invoking a method several times in a row.""" derived_message = "This is a Custom exception class" def __init__(self, count: int) -> None: self.counter = 0 self.count = count def go(self) -> typing.Any: """Raise a CustomError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise CustomError(self.derived_message) return True class CapturingHandler(logging.Handler): """Captures log records for inspection.""" def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: super().__init__(*args, **kwargs) self.records: list[logging.LogRecord] = [] def emit(self, record: logging.LogRecord) -> None: self.records.append(record) def current_time_ms() -> int: return round(time.time() * 1000) @retry( wait=tenacity.wait_fixed(0.05), retry=tenacity.retry_if_result(lambda result: result is None), ) def _retryable_test_with_wait(thing: typing.Any) -> typing.Any: return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_result(lambda result: result is None), ) def _retryable_test_with_stop(thing: typing.Any) -> typing.Any: return thing.go() @retry(retry=tenacity.retry_if_exception_cause_type(NameError)) def _retryable_test_with_exception_cause_type(thing: typing.Any) -> typing.Any: return thing.go() @retry(retry=tenacity.retry_if_exception_type(IOError)) def _retryable_test_with_exception_type_io(thing: typing.Any) -> typing.Any: return thing.go() @retry(retry=tenacity.retry_if_not_exception_type(IOError)) def _retryable_test_if_not_exception_type_io(thing: typing.Any) -> typing.Any: return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(IOError) ) def _retryable_test_with_exception_type_io_attempt_limit( thing: typing.Any, ) -> typing.Any: return thing.go() @retry(retry=tenacity.retry_unless_exception_type(NameError)) def _retryable_test_with_unless_exception_type_name(thing: typing.Any) -> typing.Any: return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_unless_exception_type(NameError), ) def _retryable_test_with_unless_exception_type_name_attempt_limit( thing: typing.Any, ) -> typing.Any: return thing.go() @retry(retry=tenacity.retry_unless_exception_type()) def _retryable_test_with_unless_exception_type_no_input( thing: typing.Any, ) -> typing.Any: return thing.go() @retry( stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_if_exception_message( message=NoCustomErrorAfterCount.derived_message ), ) def _retryable_test_if_exception_message_message(thing: typing.Any) -> typing.Any: return thing.go() @retry( retry=tenacity.retry_if_not_exception_message( message=NoCustomErrorAfterCount.derived_message ) ) def _retryable_test_if_not_exception_message_message(thing: typing.Any) -> typing.Any: return thing.go() @retry( retry=tenacity.retry_if_exception_message( match=NoCustomErrorAfterCount.derived_message[:3] + ".*" ) ) def _retryable_test_if_exception_message_match(thing: typing.Any) -> typing.Any: return thing.go() @retry( retry=tenacity.retry_if_not_exception_message( match=NoCustomErrorAfterCount.derived_message[:3] + ".*" ) ) def _retryable_test_if_not_exception_message_match(thing: typing.Any) -> typing.Any: return thing.go() @retry( retry=tenacity.retry_if_not_exception_message( message=NameErrorUntilCount.derived_message ) ) def _retryable_test_not_exception_message_delay(thing: typing.Any) -> typing.Any: return thing.go() @retry def _retryable_default(thing: typing.Any) -> typing.Any: return thing.go() @retry() def _retryable_default_f(thing: typing.Any) -> typing.Any: return thing.go() @retry(retry=tenacity.retry_if_exception_type(CustomError)) def _retryable_test_with_exception_type_custom(thing: typing.Any) -> typing.Any: return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(CustomError), ) def _retryable_test_with_exception_type_custom_attempt_limit( thing: typing.Any, ) -> typing.Any: return thing.go() class TestDecoratorWrapper(unittest.TestCase): def test_with_wait(self) -> None: start = current_time_ms() result = _retryable_test_with_wait(NoneReturnUntilAfterCount(5)) t = current_time_ms() - start self.assertGreaterEqual(t, 250) self.assertTrue(result) def test_with_stop_on_return_value(self) -> None: try: _retryable_test_with_stop(NoneReturnUntilAfterCount(5)) self.fail("Expected RetryError after 3 attempts") except RetryError as re: self.assertFalse(re.last_attempt.failed) self.assertEqual(3, re.last_attempt.attempt_number) self.assertTrue(re.last_attempt.result() is None) print(re) def test_with_stop_on_exception(self) -> None: try: _retryable_test_with_stop(NoIOErrorAfterCount(5)) self.fail("Expected IOError") except OSError as re: self.assertTrue(isinstance(re, IOError)) print(re) def test_retry_if_exception_of_type(self) -> None: self.assertTrue(_retryable_test_with_exception_type_io(NoIOErrorAfterCount(5))) try: _retryable_test_with_exception_type_io(NoNameErrorAfterCount(5)) self.fail("Expected NameError") except NameError as n: self.assertTrue(isinstance(n, NameError)) print(n) self.assertTrue( _retryable_test_with_exception_type_custom(NoCustomErrorAfterCount(5)) ) try: _retryable_test_with_exception_type_custom(NoNameErrorAfterCount(5)) self.fail("Expected NameError") except NameError as n: self.assertTrue(isinstance(n, NameError)) print(n) def test_retry_except_exception_of_type(self) -> None: self.assertTrue( _retryable_test_if_not_exception_type_io(NoNameErrorAfterCount(5)) ) try: _retryable_test_if_not_exception_type_io(NoIOErrorAfterCount(5)) self.fail("Expected IOError") except OSError as err: self.assertTrue(isinstance(err, IOError)) print(err) def test_retry_until_exception_of_type_attempt_number(self) -> None: try: self.assertTrue( _retryable_test_with_unless_exception_type_name(NameErrorUntilCount(5)) ) except NameError as e: s = _retryable_test_with_unless_exception_type_name.statistics self.assertTrue(s["attempt_number"] == 6) print(e) else: self.fail("Expected NameError") def test_retry_until_exception_of_type_no_type(self) -> None: try: # no input should catch all subclasses of Exception self.assertTrue( _retryable_test_with_unless_exception_type_no_input( NameErrorUntilCount(5) ) ) except NameError as e: s = _retryable_test_with_unless_exception_type_no_input.statistics self.assertTrue(s["attempt_number"] == 6) print(e) else: self.fail("Expected NameError") def test_retry_until_exception_of_type_wrong_exception(self) -> None: try: # two iterations with IOError, one that returns True _retryable_test_with_unless_exception_type_name_attempt_limit( IOErrorUntilCount(2) ) self.fail("Expected RetryError") except RetryError as e: self.assertTrue(isinstance(e, RetryError)) print(e) def test_retry_if_exception_message(self) -> None: try: self.assertTrue( _retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3)) ) except CustomError: print(_retryable_test_if_exception_message_message.statistics) self.fail("CustomError should've been retried from errormessage") def test_retry_if_not_exception_message(self) -> None: try: self.assertTrue( _retryable_test_if_not_exception_message_message( NoCustomErrorAfterCount(2) ) ) except CustomError: s = _retryable_test_if_not_exception_message_message.statistics self.assertTrue(s["attempt_number"] == 1) def test_retry_if_not_exception_message_delay(self) -> None: try: self.assertTrue( _retryable_test_not_exception_message_delay(NameErrorUntilCount(3)) ) except NameError: s = _retryable_test_not_exception_message_delay.statistics print(s["attempt_number"]) self.assertTrue(s["attempt_number"] == 4) def test_retry_if_exception_message_match(self) -> None: try: self.assertTrue( _retryable_test_if_exception_message_match(NoCustomErrorAfterCount(3)) ) except CustomError: self.fail("CustomError should've been retried from errormessage") def test_retry_if_not_exception_message_match(self) -> None: try: self.assertTrue( _retryable_test_if_not_exception_message_message( NoCustomErrorAfterCount(2) ) ) except CustomError: s = _retryable_test_if_not_exception_message_message.statistics self.assertTrue(s["attempt_number"] == 1) def test_retry_if_exception_cause_type(self) -> None: self.assertTrue( _retryable_test_with_exception_cause_type(NoNameErrorCauseAfterCount(5)) ) try: _retryable_test_with_exception_cause_type(NoIOErrorCauseAfterCount(5)) self.fail("Expected exception without NameError as cause") except NameError: pass def test_retry_preserves_argument_defaults(self) -> None: def function_with_defaults(a: int = 1) -> int: return a def function_with_kwdefaults(*, a: int = 1) -> int: 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__, # type: ignore[attr-defined] ) self.assertEqual( function_with_kwdefaults.__kwdefaults__, wrapped_kwdefaults_function.__kwdefaults__, # type: ignore[attr-defined] ) def test_defaults(self) -> None: self.assertTrue(_retryable_default(NoNameErrorAfterCount(5))) self.assertTrue(_retryable_default_f(NoNameErrorAfterCount(5))) self.assertTrue(_retryable_default(NoCustomErrorAfterCount(5))) self.assertTrue(_retryable_default_f(NoCustomErrorAfterCount(5))) def test_retry_function_object(self) -> None: """Test that functools.wraps doesn't cause problems with callable objects. It raises an error upon trying to wrap it in Py2, because __name__ attribute is missing. It's fixed in Py3 but was never backported. """ class Hello: def __call__(self) -> str: return "Hello" retrying = Retrying( wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3) ) h = retrying.wraps(Hello()) self.assertEqual(h(), "Hello") def test_retry_function_attributes(self) -> None: """Test that the wrapped function attributes are exposed as intended. - statistics contains the value for the latest function run - retry object can be modified to change its behaviour (useful to patch in tests) - retry object statistics are synced with function statistics """ self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2))) expected_stats = { "attempt_number": 3, "delay_since_first_attempt": mock.ANY, "idle_for": mock.ANY, "start_time": mock.ANY, } self.assertEqual(_retryable_test_with_stop.statistics, expected_stats) self.assertEqual(_retryable_test_with_stop.retry.statistics, expected_stats) with mock.patch.object( _retryable_test_with_stop.retry, "stop", tenacity.stop_after_attempt(1), ): try: self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2))) except RetryError as exc: expected_stats = { "attempt_number": 1, "delay_since_first_attempt": mock.ANY, "idle_for": mock.ANY, "start_time": mock.ANY, } self.assertEqual(_retryable_test_with_stop.statistics, expected_stats) self.assertEqual(exc.last_attempt.attempt_number, 1) self.assertEqual( _retryable_test_with_stop.retry.statistics, expected_stats ) else: self.fail("RetryError should have been raised after 1 attempt") class TestStatisticsKeys: def test_delay_since_first_attempt_available_on_first_attempt(self) -> None: """delay_since_first_attempt should be in statistics from the start.""" @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_result(lambda x: x is None), ) def succeeds_first_try() -> bool: assert "delay_since_first_attempt" in succeeds_first_try.statistics assert succeeds_first_try.statistics["delay_since_first_attempt"] == 0 return True succeeds_first_try() assert succeeds_first_try.statistics["delay_since_first_attempt"] == 0 class TestEnabled: def test_enabled_false_skips_retry(self) -> None: """When enabled=False, the function is called directly without retrying.""" call_count = 0 @retry(enabled=False, stop=tenacity.stop_after_attempt(3)) def always_fails() -> None: nonlocal call_count call_count += 1 raise ValueError("fail") with pytest.raises(ValueError, match="fail"): always_fails() assert call_count == 1 def test_enabled_false_preserves_attributes(self) -> None: """When enabled=False, .retry, .retry_with, .statistics are still available.""" @retry(enabled=False, stop=tenacity.stop_after_attempt(3)) def my_func() -> str: return "ok" assert hasattr(my_func, "retry") assert hasattr(my_func, "retry_with") assert hasattr(my_func, "statistics") assert my_func() == "ok" def test_enabled_false_via_retry_with(self) -> None: """retry_with(enabled=False) disables retrying.""" call_count = 0 @retry(stop=tenacity.stop_after_attempt(3)) def always_fails() -> None: nonlocal call_count call_count += 1 raise ValueError("fail") disabled = always_fails.retry_with(enabled=False) with pytest.raises(ValueError, match="fail"): disabled() assert call_count == 1 def test_enabled_true_retries_normally(self) -> None: """When enabled=True (default), retrying works as usual.""" call_count = 0 @retry(enabled=True, stop=tenacity.stop_after_attempt(3), reraise=True) def fails_twice() -> bool: nonlocal call_count call_count += 1 if call_count < 3: raise ValueError("fail") return True assert fails_twice() is True assert call_count == 3 class TestRetryWith: def test_redefine_wait(self) -> None: start = current_time_ms() result = _retryable_test_with_wait.retry_with(wait=tenacity.wait_fixed(0.1))( NoneReturnUntilAfterCount(5) ) t = current_time_ms() - start assert t >= 500 assert result is True def test_redefine_stop(self) -> None: result = _retryable_test_with_stop.retry_with( stop=tenacity.stop_after_attempt(5) )(NoneReturnUntilAfterCount(4)) assert result is True def test_retry_error_cls_should_be_preserved(self) -> None: @retry(stop=tenacity.stop_after_attempt(10), retry_error_cls=ValueError) # type: ignore[arg-type] def _retryable() -> None: raise Exception("raised for test purposes") with pytest.raises(Exception) as exc_ctx: _retryable.retry_with(stop=tenacity.stop_after_attempt(2))() assert exc_ctx.type is ValueError, "Should remap to specific exception type" def test_retry_error_callback_should_be_preserved(self) -> None: def return_text(retry_state: RetryCallState) -> str: return f"Calling {retry_state.fn.__name__} keeps raising errors after {retry_state.attempt_number} attempts" # type: ignore[union-attr] @retry(stop=tenacity.stop_after_attempt(10), retry_error_callback=return_text) def _retryable() -> None: raise Exception("raised for test purposes") result = _retryable.retry_with(stop=tenacity.stop_after_attempt(5))() assert result == "Calling _retryable keeps raising errors after 5 attempts" class TestBeforeAfterAttempts(unittest.TestCase): _attempt_number = 0 def test_before_attempts(self) -> None: TestBeforeAfterAttempts._attempt_number = 0 def _before(retry_state: RetryCallState) -> None: TestBeforeAfterAttempts._attempt_number = retry_state.attempt_number @retry( wait=tenacity.wait_fixed(1), stop=tenacity.stop_after_attempt(1), before=_before, ) def _test_before() -> None: pass _test_before() self.assertTrue(TestBeforeAfterAttempts._attempt_number == 1) def test_after_attempts(self) -> None: TestBeforeAfterAttempts._attempt_number = 0 def _after(retry_state: RetryCallState) -> None: TestBeforeAfterAttempts._attempt_number = retry_state.attempt_number @retry( wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(3), after=_after, ) def _test_after() -> None: if TestBeforeAfterAttempts._attempt_number < 2: raise Exception("testing after_attempts handler") _test_after() self.assertTrue(TestBeforeAfterAttempts._attempt_number == 2) def test_before_sleep(self) -> None: def _before_sleep(retry_state: RetryCallState) -> None: self.assertGreater(retry_state.next_action.sleep, 0) # type: ignore[union-attr] _before_sleep.attempt_number = retry_state.attempt_number # type: ignore[attr-defined] @retry( wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep, ) def _test_before_sleep() -> None: if _before_sleep.attempt_number < 2: # type: ignore[attr-defined] raise Exception("testing before_sleep_attempts handler") _test_before_sleep() self.assertEqual(_before_sleep.attempt_number, 2) # type: ignore[attr-defined] def _before_sleep_log_raises( self, get_call_fn: typing.Callable[..., typing.Any] ) -> None: thing = NoIOErrorAfterCount(2) logger = logging.getLogger(self.id()) logger.propagate = False logger.setLevel(logging.INFO) handler = CapturingHandler() logger.addHandler(handler) try: _before_sleep = tenacity.before_sleep_log(logger, logging.INFO) retrying = Retrying( wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep, ) get_call_fn(retrying)(thing.go) finally: logger.removeHandler(handler) 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) fmt = logging.Formatter().format self.assertRegex(fmt(handler.records[0]), etalon_re) self.assertRegex(fmt(handler.records[1]), etalon_re) def test_before_sleep_log_raises(self) -> None: self._before_sleep_log_raises(lambda x: x) def test_before_sleep_log_raises_with_exc_info(self) -> None: thing = NoIOErrorAfterCount(2) logger = logging.getLogger(self.id()) logger.propagate = False logger.setLevel(logging.INFO) handler = CapturingHandler() logger.addHandler(handler) try: _before_sleep = tenacity.before_sleep_log( logger, logging.INFO, exc_info=True ) retrying = Retrying( wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep, ) retrying(thing.go) finally: logger.removeHandler(handler) etalon_re = re.compile( r"^Retrying .* in 0\.01 seconds as it raised " r"(IO|OS)Error: Hi there, I'm an IOError\.{0}" r"Traceback \(most recent call last\):{0}" r".*$".format("\n"), flags=re.MULTILINE, ) self.assertEqual(len(handler.records), 2) fmt = logging.Formatter().format self.assertRegex(fmt(handler.records[0]), etalon_re) self.assertRegex(fmt(handler.records[1]), etalon_re) def test_before_sleep_log_returns(self, exc_info: bool = False) -> None: thing = NoneReturnUntilAfterCount(2) logger = logging.getLogger(self.id()) logger.propagate = False logger.setLevel(logging.INFO) handler = CapturingHandler() logger.addHandler(handler) try: _before_sleep = tenacity.before_sleep_log( logger, logging.INFO, exc_info=exc_info ) _retry = tenacity.retry_if_result(lambda result: result is None) retrying = Retrying( wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), retry=_retry, before_sleep=_before_sleep, ) retrying(thing.go) finally: logger.removeHandler(handler) etalon_re = r"^Retrying .* in 0\.01 seconds as it returned None\.$" self.assertEqual(len(handler.records), 2) fmt = logging.Formatter().format self.assertRegex(fmt(handler.records[0]), etalon_re) self.assertRegex(fmt(handler.records[1]), etalon_re) def test_before_sleep_log_returns_with_exc_info(self) -> None: self.test_before_sleep_log_returns(exc_info=True) class TestReraiseExceptions(unittest.TestCase): def test_reraise_by_default(self) -> None: calls = [] @retry( wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), reraise=True, ) def _reraised_by_default() -> None: calls.append("x") raise KeyError("Bad key") self.assertRaises(KeyError, _reraised_by_default) self.assertEqual(2, len(calls)) def test_reraise_from_retry_error(self) -> None: calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2)) def _raise_key_error() -> None: calls.append("x") raise KeyError("Bad key") def _reraised_key_error() -> None: try: _raise_key_error() except tenacity.RetryError as retry_err: retry_err.reraise() self.assertRaises(KeyError, _reraised_key_error) self.assertEqual(2, len(calls)) def test_reraise_timeout_from_retry_error(self) -> None: calls = [] @retry( wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), retry=lambda retry_state: True, ) def _mock_fn() -> None: calls.append("x") def _reraised_mock_fn() -> None: try: _mock_fn() except tenacity.RetryError as retry_err: retry_err.reraise() self.assertRaises(tenacity.RetryError, _reraised_mock_fn) self.assertEqual(2, len(calls)) def test_reraise_no_exception(self) -> None: calls = [] @retry( wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), retry=lambda retry_state: True, reraise=True, ) def _mock_fn() -> None: calls.append("x") self.assertRaises(tenacity.RetryError, _mock_fn) self.assertEqual(2, len(calls)) class TestStatistics(unittest.TestCase): def test_stats(self) -> None: @retry() def _foobar() -> int: return 42 self.assertEqual({}, _foobar.statistics) _foobar() self.assertEqual(1, _foobar.statistics["attempt_number"]) def test_stats_failing(self) -> None: @retry(stop=tenacity.stop_after_attempt(2)) def _foobar() -> None: raise ValueError(42) self.assertEqual({}, _foobar.statistics) with contextlib.suppress(Exception): _foobar() self.assertEqual(2, _foobar.statistics["attempt_number"]) def test_retry_object_statistics_synced(self) -> None: """Test that func.retry.statistics is synced with func.statistics.""" @retry(stop=tenacity.stop_after_attempt(3)) def _foobar() -> int: return 42 _foobar() self.assertEqual( _foobar.retry.statistics["attempt_number"], _foobar.statistics["attempt_number"], ) def test_retry_object_statistics_during_execution(self) -> None: """Test that func.retry.statistics is accessible during execution.""" attempts: list[int] = [] @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(ValueError), reraise=True, ) def _foobar() -> int: attempts.append(_foobar.retry.statistics["attempt_number"]) if len(attempts) < 3: raise ValueError("retry") return 42 _foobar() self.assertEqual(attempts, [1, 2, 3]) class TestRetryErrorCallback(unittest.TestCase): def setUp(self) -> None: self._attempt_number = 0 self._callback_called = False def _callback(self, fut: tenacity.Future) -> tenacity.Future: self._callback_called = True return fut def test_retry_error_callback(self) -> None: num_attempts = 3 def retry_error_callback(retry_state: RetryCallState) -> typing.Any: retry_error_callback.called_times += 1 # type: ignore[attr-defined] return retry_state.outcome retry_error_callback.called_times = 0 # type: ignore[attr-defined] @retry( stop=tenacity.stop_after_attempt(num_attempts), retry_error_callback=retry_error_callback, ) def _foobar() -> None: self._attempt_number += 1 raise Exception("This exception should not be raised") result = _foobar() self.assertEqual(retry_error_callback.called_times, 1) # type: ignore[attr-defined] self.assertEqual(num_attempts, self._attempt_number) self.assertIsInstance(result, tenacity.Future) class TestContextManager(unittest.TestCase): def test_context_manager_retry_one(self) -> None: from tenacity import Retrying raise_ = True for attempt in Retrying(): with attempt: if raise_: raise_ = False raise Exception("Retry it!") def test_context_manager_on_error(self) -> None: from tenacity import Retrying class CustomError(Exception): pass retry = Retrying(retry=tenacity.retry_if_exception_type(IOError)) def test() -> None: for attempt in retry: with attempt: raise CustomError("Don't retry!") self.assertRaises(CustomError, test) def test_context_manager_retry_error(self) -> None: from tenacity import Retrying retry = Retrying(stop=tenacity.stop_after_attempt(2)) def test() -> None: for attempt in retry: with attempt: raise Exception("Retry it!") self.assertRaises(RetryError, test) def test_context_manager_reraise(self) -> None: from tenacity import Retrying class CustomError(Exception): pass retry = Retrying(reraise=True, stop=tenacity.stop_after_attempt(2)) def test() -> None: for attempt in retry: with attempt: raise CustomError("Don't retry!") self.assertRaises(CustomError, test) class TestInvokeAsCallable: """Test direct invocation of Retrying as a callable.""" @staticmethod def invoke(retry: Retrying, f: typing.Callable[..., typing.Any]) -> typing.Any: """ Invoke Retrying logic. Wrapper allows testing different call mechanisms in test sub-classes. """ return retry(f) def test_retry_one(self) -> None: def f() -> typing.Any: f.calls.append(len(f.calls) + 1) # type: ignore[attr-defined] if len(f.calls) <= 1: # type: ignore[attr-defined] raise Exception("Retry it!") return 42 f.calls = [] # type: ignore[attr-defined] retry = Retrying() assert self.invoke(retry, f) == 42 assert f.calls == [1, 2] # type: ignore[attr-defined] def test_on_error(self) -> None: class CustomError(Exception): pass def f() -> typing.Any: f.calls.append(len(f.calls) + 1) # type: ignore[attr-defined] if len(f.calls) <= 1: # type: ignore[attr-defined] raise CustomError("Don't retry!") return 42 f.calls = [] # type: ignore[attr-defined] retry = Retrying(retry=tenacity.retry_if_exception_type(IOError)) with pytest.raises(CustomError): self.invoke(retry, f) assert f.calls == [1] # type: ignore[attr-defined] def test_retry_error(self) -> None: def f() -> typing.Any: f.calls.append(len(f.calls) + 1) # type: ignore[attr-defined] raise Exception("Retry it!") f.calls = [] # type: ignore[attr-defined] retry = Retrying(stop=tenacity.stop_after_attempt(2)) with pytest.raises(RetryError): self.invoke(retry, f) assert f.calls == [1, 2] # type: ignore[attr-defined] def test_reraise(self) -> None: class CustomError(Exception): pass def f() -> typing.Any: f.calls.append(len(f.calls) + 1) # type: ignore[attr-defined] raise CustomError("Retry it!") f.calls = [] # type: ignore[attr-defined] retry = Retrying(reraise=True, stop=tenacity.stop_after_attempt(2)) with pytest.raises(CustomError): self.invoke(retry, f) assert f.calls == [1, 2] # type: ignore[attr-defined] class TestRetryException(unittest.TestCase): def test_retry_error_is_pickleable(self) -> None: import pickle expected = RetryError(last_attempt=123) # type: ignore[arg-type] pickled = pickle.dumps(expected) actual = pickle.loads(pickled) self.assertEqual(expected.last_attempt, actual.last_attempt) class TestRetryTyping(unittest.TestCase): def test_retry_type_annotations(self) -> None: """The decorator should maintain types of decorated functions.""" def num_to_str(number): # type: (int) -> str return str(number) # equivalent to a raw @retry decoration with_raw = retry(num_to_str) with_raw_result = with_raw(1) # equivalent to a @retry(...) decoration with_constructor = retry()(num_to_str) with_constructor_result = with_raw(1) # These raise TypeError exceptions if they fail check_type(with_raw, typing.Callable[[int], str]) check_type(with_raw_result, str) check_type(with_constructor, typing.Callable[[int], str]) check_type(with_constructor_result, str) class TestMockingSleep: RETRY_ARGS = { "wait": tenacity.wait_fixed(0.1), "stop": tenacity.stop_after_attempt(5), } def _fail(self) -> None: raise NotImplementedError @retry(**RETRY_ARGS) # type: ignore[call-overload, untyped-decorator] def _decorated_fail(self) -> None: self._fail() @pytest.fixture() def mock_sleep( self, monkeypatch: typing.Any ) -> typing.Generator[typing.Any, None, None]: class MockSleep: call_count = 0 def __call__(self, seconds: float) -> None: self.call_count += 1 sleep = MockSleep() monkeypatch.setattr(tenacity.nap.time, "sleep", sleep) # type: ignore[attr-defined] yield sleep def test_decorated(self, mock_sleep: typing.Any) -> None: with pytest.raises(RetryError): self._decorated_fail() assert mock_sleep.call_count == 4 def test_decorated_retry_with(self, mock_sleep: typing.Any) -> None: fail_faster = self._decorated_fail.retry_with( stop=tenacity.stop_after_attempt(2), ) with pytest.raises(RetryError): fail_faster() assert mock_sleep.call_count == 1 class TestPickle(unittest.TestCase): def test_retrying_picklable(self) -> None: """Retrying objects can be pickled for multiprocessing support.""" retrying = Retrying(stop=tenacity.stop_after_attempt(3)) pickled = pickle.dumps(retrying) restored = pickle.loads(pickled) assert isinstance(restored, Retrying) assert isinstance(restored.stop, tenacity.stop_after_attempt) def test_retrying_picklable_after_run(self) -> None: """Retrying objects can be pickled even after being used.""" retrying = Retrying(stop=tenacity.stop_after_attempt(3)) # Access statistics to populate _local _ = retrying.statistics pickled = pickle.dumps(retrying) restored = pickle.loads(pickled) assert isinstance(restored, Retrying) # Statistics should be reset on the restored object assert restored.statistics == {} def test_retry_strategies_picklable(self) -> None: """All built-in retry strategies can be pickled.""" strategies = [ tenacity.retry_if_exception_type(ValueError), tenacity.retry_if_not_exception_type(ValueError), tenacity.retry_if_exception_message(message="fail"), tenacity.retry_if_exception_message(match="fail.*"), tenacity.retry_if_not_exception_message(message="fail"), ] for strategy in strategies: restored = pickle.loads(pickle.dumps(strategy)) assert type(restored) is type(strategy) def test_retrying_pickle_round_trip_works(self) -> None: """A pickled-then-restored Retrying object retries correctly.""" retrying = Retrying( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(ValueError), reraise=True, ) restored = pickle.loads(pickle.dumps(retrying)) calls = 0 def succeed_on_third() -> str: nonlocal calls calls += 1 if calls < 3: raise ValueError("not yet") return "ok" result = restored(succeed_on_third) assert result == "ok" assert calls == 3 if __name__ == "__main__": unittest.main() ================================================ FILE: tests/test_tornado.py ================================================ # Copyright 2017 Elisey Zanko # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import unittest from collections.abc import Generator from typing import Any from tornado import gen, testing from tenacity import RetryError, retry, stop_after_attempt, tornadoweb from .test_tenacity import NoIOErrorAfterCount @retry @gen.coroutine def _retryable_coroutine(thing: NoIOErrorAfterCount) -> Generator[Any, Any, None]: yield gen.sleep(0.00001) thing.go() @retry(stop=stop_after_attempt(2)) @gen.coroutine def _retryable_coroutine_with_2_attempts( thing: NoIOErrorAfterCount, ) -> Generator[Any, Any, None]: yield gen.sleep(0.00001) thing.go() class TestTornado(testing.AsyncTestCase): @testing.gen_test def test_retry(self) -> Generator[Any, Any, None]: assert gen.is_coroutine_function(_retryable_coroutine) thing = NoIOErrorAfterCount(5) yield _retryable_coroutine(thing) assert thing.counter == thing.count @testing.gen_test def test_stop_after_attempt(self) -> Generator[Any, Any, None]: assert gen.is_coroutine_function(_retryable_coroutine) thing = NoIOErrorAfterCount(2) try: yield _retryable_coroutine_with_2_attempts(thing) except RetryError: assert thing.counter == 2 def test_repr(self) -> None: repr(tornadoweb.TornadoRetrying()) def test_old_tornado(self) -> None: old_attr = gen.is_coroutine_function try: del gen.is_coroutine_function # is_coroutine_function was introduced in tornado 4.5; # verify that we don't *completely* fall over on old versions @retry def retryable(thing: NoIOErrorAfterCount) -> None: pass finally: gen.is_coroutine_function = old_attr if __name__ == "__main__": unittest.main() ================================================ FILE: tests/test_utils.py ================================================ 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 def test_find_ordinal() -> None: assert _utils.find_ordinal(1) == "st" assert _utils.find_ordinal(2) == "nd" assert _utils.find_ordinal(3) == "rd" assert _utils.find_ordinal(4) == "th" assert _utils.find_ordinal(11) == "th" assert _utils.find_ordinal(12) == "th" assert _utils.find_ordinal(13) == "th" assert _utils.find_ordinal(21) == "st" assert _utils.find_ordinal(22) == "nd" assert _utils.find_ordinal(23) == "rd" assert _utils.find_ordinal(111) == "th" assert _utils.find_ordinal(112) == "th" assert _utils.find_ordinal(113) == "th"