././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1573042360.5683148 tenacity-6.0.0/0000755000655200065520000000000000000000000014724 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1573042360.556315 tenacity-6.0.0/.circleci/0000755000655200065520000000000000000000000016557 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/.circleci/config.yml0000644000655200065520000000365000000000000020553 0ustar00circlecicircleci00000000000000version: 2 jobs: pep8: docker: - image: circleci/python:3.8 steps: - checkout - run: command: | sudo pip install tox tox -e pep8 py27: docker: - image: circleci/python:2.7 steps: - checkout - run: command: | sudo pip install tox tox -e py27 py35: docker: - image: circleci/python:3.5 steps: - checkout - run: command: | sudo pip install tox tox -e py35 py36: docker: - image: circleci/python:3.6 steps: - checkout - run: command: | sudo pip install tox tox -e py36 py37: docker: - image: circleci/python:3.7 steps: - checkout - run: command: | sudo pip install tox tox -e py37 py38: docker: - image: circleci/python:3.8 steps: - checkout - run: command: | sudo pip install tox tox -e py38 deploy: docker: - image: circleci/python:3.8 steps: - checkout - run: | python -m venv venv - run: | venv/bin/pip install twine wheel - run: name: init .pypirc command: | echo -e "[pypi]" >> ~/.pypirc echo -e "username = __token__" >> ~/.pypirc echo -e "password = $PYPI_TOKEN" >> ~/.pypirc - run: name: create packages command: | venv/bin/python setup.py sdist bdist_wheel - run: name: upload to PyPI command: | venv/bin/twine upload dist/* workflows: version: 2 test: jobs: - pep8 - py27 - py35 - py36 - py37 - py38 - deploy: filters: tags: only: /[0-9]+(\.[0-9]+)*/ branches: ignore: /.*/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/.gitignore0000644000655200065520000000013400000000000016712 0ustar00circlecicircleci00000000000000.idea dist *.pyc *.egg-info build .tox/ AUTHORS ChangeLog .eggs/ doc/_build /.pytest_cache ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/.mergify.yml0000644000655200065520000000103600000000000017167 0ustar00circlecicircleci00000000000000pull_request_rules: - name: automatic merge conditions: - "status-success=ci/circleci: pep8" - "status-success=ci/circleci: py27" - "status-success=ci/circleci: py35" - "status-success=ci/circleci: py36" - "status-success=ci/circleci: py37" - "status-success=ci/circleci: py38" - "#approved-reviews-by>=1" - label!=work-in-progress actions: merge: strict: "smart" method: merge - name: dismiss reviews conditions: [] actions: dismiss_reviews: {} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/.readthedocs.yml0000644000655200065520000000014600000000000020013 0ustar00circlecicircleci00000000000000version: 2 python: install: - method: pip path: . extra_requirements: - doc ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/LICENSE0000644000655200065520000002613500000000000015740 0ustar00circlecicircleci00000000000000 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.././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1573042360.5683148 tenacity-6.0.0/PKG-INFO0000644000655200065520000000160600000000000016024 0ustar00circlecicircleci00000000000000Metadata-Version: 2.1 Name: tenacity Version: 6.0.0 Summary: Retry code until it succeeds Home-page: https://github.com/jd/tenacity Author: Julien Danjou Author-email: julien@danjou.info License: Apache 2.0 Description: Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Utilities Provides-Extra: doc ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/README.rst0000644000655200065520000004363000000000000016421 0ustar00circlecicircleci00000000000000Tenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.python.org/pypi/tenacity .. image:: https://circleci.com/gh/jd/tenacity.svg?style=svg :target: https://circleci.com/gh/jd/tenacity .. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/jd .. image:: https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status 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 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_give_up_never_surrender(): 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 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:: @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 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") 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 ~~~~~~~~~~~~~~ While callables that "timeout" retrying raise a `RetryError` by default, we can reraise the last attempt's exception if needed: .. 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 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 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 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 `retry` attribute attached to the function and its `statistics` attribute: .. 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.retry.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 .. note:: Calling the parameter ``retry_state`` is important, because this is how *tenacity* internally distinguishes callbacks from their :ref:`deprecated counterparts `. RetryCallState ~~~~~~~~~~~~~~ ``retry_state`` argument is an object of `RetryCallState` class: .. autoclass:: tenacity.RetryCallState Constant attributes: .. autoinstanceattribute:: start_time(float) :annotation: .. autoinstanceattribute:: retry_object(BaseRetrying) :annotation: .. autoinstanceattribute:: fn(callable) :annotation: .. autoinstanceattribute:: args(tuple) :annotation: .. autoinstanceattribute:: kwargs(dict) :annotation: Variable attributes: .. autoinstanceattribute:: attempt_number(int) :annotation: .. autoinstanceattribute:: outcome(tenacity.Future or None) :annotation: .. autoinstanceattribute:: outcome_timestamp(float or None) :annotation: .. autoinstanceattribute:: idle_for(float) :annotation: .. autoinstanceattribute:: next_action(tenacity.RetryAction or None) :annotation: Other Custom Callbacks ~~~~~~~~~~~~~~~~~~~~~~ It's also possible to define custom callbacks for other keyword arguments. .. function:: my_stop(retry_state) :param RetryState retry_state: info about current retry invocation :return: whether or not retrying should stop :rtype: bool .. function:: my_wait(retry_state) :param RetryState retry_state: info about current retry invocation :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(retry_state) :param RetryState retry_state: info about current retry invocation :return: whether or not retrying should continue :rtype: bool .. function:: my_before(retry_state) :param RetryState retry_state: info about current retry invocation .. function:: my_after(retry_state) :param RetryState retry_state: info about current retry invocation .. function:: my_before_sleep(retry_state) :param RetryState 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 .. _deprecated-callbacks: .. note:: It was also possible to define custom callbacks before, but they accepted varying parameter sets and none of those provided full state. The old way is deprecated, but kept for backward compatibility. .. function:: my_stop(previous_attempt_number, delay_since_first_attempt) *deprecated* :param previous_attempt_number: the number of current attempt :type previous_attempt_number: int :param delay_since_first_attempt: interval in seconds between the beginning of first attempt and current time :type delay_since_first_attempt: float :rtype: bool .. function:: my_wait(previous_attempt_number, delay_since_first_attempt [, last_result]) *deprecated* :param previous_attempt_number: the number of current attempt :type previous_attempt_number: int :param delay_since_first_attempt: interval in seconds between the beginning of first attempt and current time :type delay_since_first_attempt: float :param tenacity.Future last_result: current outcome :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(attempt) *deprecated* :param tenacity.Future attempt: current outcome :return: whether or not retrying should continue :rtype: bool .. function:: my_before(func, trial_number) *deprecated* :param callable func: function whose outcome is to be retried :param int trial_number: the number of current attempt .. function:: my_after(func, trial_number, trial_time_taken) *deprecated* :param callable func: function whose outcome is to be retried :param int trial_number: the number of current attempt :param float trial_time_taken: interval in seconds between the beginning of first attempt and current time .. function:: my_before_sleep(func, sleep, last_result) *deprecated* :param callable func: function whose outcome is to be retried :param float sleep: number of seconds to wait until next retry :param tenacity.Future last_result: current outcome .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) def my_before_sleep(retry_object, sleep, last_result): logger.warning( 'Retrying %s: last_result=%s, retrying in %s seconds...', retry_object.fn, last_result, sleep) @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 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.retry.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') 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. Async and retry ~~~~~~~~~~~~~~~ Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_async_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_function(http_client, url): yield http_client.fetch(url) You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function: .. code-block:: python @retry(sleep=trio.sleep) async def my_async_function(loop): await asks.get('https://example.org') 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 **master** 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 ...) .. _`the repository`: https://github.com/jd/tenacity Changelogs ~~~~~~~~~~ `reno`_ is used for managing changelogs. Take a look at their usage docs. The doc generation will automatically compile the changelogs. You just need to add them. .. code-block:: sh # Opens a template file in an editor tox -e reno -- new some-slug-for-my-change --edit .. _`reno`: https://docs.openstack.org/reno/latest/user/usage.html ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1573042360.5483148 tenacity-6.0.0/doc/0000755000655200065520000000000000000000000015471 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1573042360.556315 tenacity-6.0.0/doc/source/0000755000655200065520000000000000000000000016771 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/doc/source/api.rst0000644000655200065520000000270700000000000020302 0ustar00circlecicircleci00000000000000=============== API Reference =============== Retry Main API -------------- .. autofunction:: tenacity.retry :noindex: .. autoclass:: tenacity.Retrying :members: .. autoclass:: tenacity.AsyncRetrying :members: .. autoclass:: tenacity.TornadoRetrying :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: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/doc/source/changelog.rst0000644000655200065520000000005000000000000021445 0ustar00circlecicircleci00000000000000Changelog ========= .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/doc/source/conf.py0000644000655200065520000000221700000000000020272 0ustar00circlecicircleci00000000000000# -*- coding: utf-8 -*- # 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 os master_doc = 'index' project = "Tenacity" extensions = [ 'sphinx.ext.doctest', 'sphinx.ext.autodoc', 'reno.sphinxext', ] # -- Options for sphinx.ext.doctest ----------------------------------------- # doctest_default_flags = cwd = os.path.abspath(os.path.dirname(__file__)) tenacity_path = os.path.join(cwd, os.pardir, os.pardir) doctest_path = [tenacity_path] # doctest_global_setup = # doctest_global_cleanup = # doctest_test_doctest_blocks = ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/doc/source/index.rst0000644000655200065520000004363000000000000020640 0ustar00circlecicircleci00000000000000Tenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.python.org/pypi/tenacity .. image:: https://circleci.com/gh/jd/tenacity.svg?style=svg :target: https://circleci.com/gh/jd/tenacity .. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/jd .. image:: https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status 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 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_give_up_never_surrender(): 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 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:: @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 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") 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 ~~~~~~~~~~~~~~ While callables that "timeout" retrying raise a `RetryError` by default, we can reraise the last attempt's exception if needed: .. 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 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 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 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 `retry` attribute attached to the function and its `statistics` attribute: .. 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.retry.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 .. note:: Calling the parameter ``retry_state`` is important, because this is how *tenacity* internally distinguishes callbacks from their :ref:`deprecated counterparts `. RetryCallState ~~~~~~~~~~~~~~ ``retry_state`` argument is an object of `RetryCallState` class: .. autoclass:: tenacity.RetryCallState Constant attributes: .. autoinstanceattribute:: start_time(float) :annotation: .. autoinstanceattribute:: retry_object(BaseRetrying) :annotation: .. autoinstanceattribute:: fn(callable) :annotation: .. autoinstanceattribute:: args(tuple) :annotation: .. autoinstanceattribute:: kwargs(dict) :annotation: Variable attributes: .. autoinstanceattribute:: attempt_number(int) :annotation: .. autoinstanceattribute:: outcome(tenacity.Future or None) :annotation: .. autoinstanceattribute:: outcome_timestamp(float or None) :annotation: .. autoinstanceattribute:: idle_for(float) :annotation: .. autoinstanceattribute:: next_action(tenacity.RetryAction or None) :annotation: Other Custom Callbacks ~~~~~~~~~~~~~~~~~~~~~~ It's also possible to define custom callbacks for other keyword arguments. .. function:: my_stop(retry_state) :param RetryState retry_state: info about current retry invocation :return: whether or not retrying should stop :rtype: bool .. function:: my_wait(retry_state) :param RetryState retry_state: info about current retry invocation :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(retry_state) :param RetryState retry_state: info about current retry invocation :return: whether or not retrying should continue :rtype: bool .. function:: my_before(retry_state) :param RetryState retry_state: info about current retry invocation .. function:: my_after(retry_state) :param RetryState retry_state: info about current retry invocation .. function:: my_before_sleep(retry_state) :param RetryState 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 .. _deprecated-callbacks: .. note:: It was also possible to define custom callbacks before, but they accepted varying parameter sets and none of those provided full state. The old way is deprecated, but kept for backward compatibility. .. function:: my_stop(previous_attempt_number, delay_since_first_attempt) *deprecated* :param previous_attempt_number: the number of current attempt :type previous_attempt_number: int :param delay_since_first_attempt: interval in seconds between the beginning of first attempt and current time :type delay_since_first_attempt: float :rtype: bool .. function:: my_wait(previous_attempt_number, delay_since_first_attempt [, last_result]) *deprecated* :param previous_attempt_number: the number of current attempt :type previous_attempt_number: int :param delay_since_first_attempt: interval in seconds between the beginning of first attempt and current time :type delay_since_first_attempt: float :param tenacity.Future last_result: current outcome :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(attempt) *deprecated* :param tenacity.Future attempt: current outcome :return: whether or not retrying should continue :rtype: bool .. function:: my_before(func, trial_number) *deprecated* :param callable func: function whose outcome is to be retried :param int trial_number: the number of current attempt .. function:: my_after(func, trial_number, trial_time_taken) *deprecated* :param callable func: function whose outcome is to be retried :param int trial_number: the number of current attempt :param float trial_time_taken: interval in seconds between the beginning of first attempt and current time .. function:: my_before_sleep(func, sleep, last_result) *deprecated* :param callable func: function whose outcome is to be retried :param float sleep: number of seconds to wait until next retry :param tenacity.Future last_result: current outcome .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) def my_before_sleep(retry_object, sleep, last_result): logger.warning( 'Retrying %s: last_result=%s, retrying in %s seconds...', retry_object.fn, last_result, sleep) @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 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.retry.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') 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. Async and retry ~~~~~~~~~~~~~~~ Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_async_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_function(http_client, url): yield http_client.fetch(url) You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function: .. code-block:: python @retry(sleep=trio.sleep) async def my_async_function(loop): await asks.get('https://example.org') 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 **master** 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 ...) .. _`the repository`: https://github.com/jd/tenacity Changelogs ~~~~~~~~~~ `reno`_ is used for managing changelogs. Take a look at their usage docs. The doc generation will automatically compile the changelogs. You just need to add them. .. code-block:: sh # Opens a template file in an editor tox -e reno -- new some-slug-for-my-change --edit .. _`reno`: https://docs.openstack.org/reno/latest/user/usage.html ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1573042360.5483148 tenacity-6.0.0/releasenotes/0000755000655200065520000000000000000000000017415 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1573042360.556315 tenacity-6.0.0/releasenotes/notes/0000755000655200065520000000000000000000000020545 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/releasenotes/notes/add-reno-d1ab5710f272650a.yaml0000644000655200065520000000005600000000000025242 0ustar00circlecicircleci00000000000000--- features: - Add reno (changelog system) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/reno.yaml0000644000655200065520000000005100000000000016547 0ustar00circlecicircleci00000000000000--- unreleased_version_title: Unreleased ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1573042360.5683148 tenacity-6.0.0/setup.cfg0000644000655200065520000000211300000000000016542 0ustar00circlecicircleci00000000000000[metadata] name = tenacity license = Apache 2.0 url = https://github.com/jd/tenacity summary = Retry code until it succeeds long_description = Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. author = Julien Danjou author-email = julien@danjou.info home-page = https://github.com/jd/tenacity classifier = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Topic :: Utilities [options] install_requires = six>=1.9.0 futures>=3.0;python_version=='2.7' monotonic>=0.6;python_version=='2.7' packages = tenacity [options.extras_require] doc = reno sphinx tornado>=4.5 [wheel] universal = 1 [tool:pytest] filterwarnings = once::DeprecationWarning [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/setup.py0000644000655200065520000000124200000000000016435 0ustar00circlecicircleci00000000000000#!/usr/bin/env python # # 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 setuptools setuptools.setup( setup_requires=['setuptools_scm'], use_scm_version=True, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1573042360.5643148 tenacity-6.0.0/tenacity/0000755000655200065520000000000000000000000016544 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/__init__.py0000644000655200065520000003672500000000000020672 0ustar00circlecicircleci00000000000000# -*- coding: utf-8 -*- # 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. try: import asyncio except ImportError: asyncio = None try: import tornado except ImportError: tornado = None import sys import threading from concurrent import futures import six from tenacity import _utils from tenacity import compat as _compat # Import all built-in retry strategies for easier usage. from .retry import retry_all # noqa from .retry import retry_always # noqa from .retry import retry_any # noqa from .retry import retry_if_exception # noqa from .retry import retry_if_exception_type # noqa from .retry import retry_if_not_result # noqa from .retry import retry_if_result # noqa from .retry import retry_never # noqa from .retry import retry_unless_exception_type # noqa from .retry import retry_if_exception_message # noqa from .retry import retry_if_not_exception_message # noqa # Import all nap strategies for easier usage. from .nap import sleep # noqa from .nap import sleep_using_event # noqa # Import all built-in stop strategies for easier usage. from .stop import stop_after_attempt # noqa from .stop import stop_after_delay # noqa from .stop import stop_all # noqa from .stop import stop_any # noqa from .stop import stop_never # noqa from .stop import stop_when_event_set # noqa # Import all built-in wait strategies for easier usage. from .wait import wait_chain # noqa from .wait import wait_combine # noqa from .wait import wait_exponential # noqa from .wait import wait_fixed # noqa from .wait import wait_incrementing # noqa from .wait import wait_none # noqa from .wait import wait_random # noqa from .wait import wait_random_exponential # noqa from .wait import wait_random_exponential as wait_full_jitter # noqa # Import all built-in before strategies for easier usage. from .before import before_log # noqa from .before import before_nothing # noqa # Import all built-in after strategies for easier usage. from .after import after_log # noqa from .after import after_nothing # noqa # Import all built-in after strategies for easier usage. from .before_sleep import before_sleep_log # noqa from .before_sleep import before_sleep_nothing # noqa def retry(*dargs, **dkw): """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]) else: def wrap(f): if asyncio and asyncio.iscoroutinefunction(f): 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 class TryAgain(Exception): """Always retry the executed function when raised.""" NO_RESULT = object() class DoAttempt(object): pass class DoSleep(float): pass class BaseAction(object): """Base class for representing actions to take by retry object. Concrete implementations must define: - __init__: to initialize all necessary fields - REPR_ATTRS: class variable specifying attributes to include in repr(self) - NAME: for identification in retry object methods and callbacks """ REPR_FIELDS = () NAME = None def __repr__(self): state_str = ', '.join('%s=%r' % (field, getattr(self, field)) for field in self.REPR_FIELDS) return '%s(%s)' % (type(self).__name__, state_str) def __str__(self): return repr(self) class RetryAction(BaseAction): REPR_FIELDS = ('sleep',) NAME = 'retry' def __init__(self, sleep): self.sleep = float(sleep) _unset = object() class RetryError(Exception): """Encapsulates the last attempt instance right before giving up.""" def __init__(self, last_attempt): self.last_attempt = last_attempt super(RetryError, self).__init__(last_attempt) def reraise(self): if self.last_attempt.failed: raise self.last_attempt.result() raise self def __str__(self): return "{0}[{1}]".format(self.__class__.__name__, self.last_attempt) class AttemptManager(object): """Manage attempt context.""" def __init__(self, retry_state): self.retry_state = retry_state def __enter__(self): pass def __exit__(self, exc_type, exc_value, traceback): if isinstance(exc_value, BaseException): self.retry_state.set_exception((exc_type, exc_value, traceback)) return True # Swallow exception. else: # We don't have the result, actually. self.retry_state.set_result(None) class BaseRetrying(object): def __init__(self, sleep=sleep, stop=stop_never, wait=wait_none(), retry=retry_if_exception_type(), before=before_nothing, after=after_nothing, before_sleep=None, reraise=False, retry_error_cls=RetryError, retry_error_callback=None): 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 # This attribute was moved to RetryCallState and is deprecated on # Retrying objects but kept for backward compatibility. self.fn = None @_utils.cached_property def stop(self): return _compat.stop_func_accept_retry_state(self._stop) @_utils.cached_property def wait(self): return _compat.wait_func_accept_retry_state(self._wait) @_utils.cached_property def retry(self): return _compat.retry_func_accept_retry_state(self._retry) @_utils.cached_property def before(self): return _compat.before_func_accept_retry_state(self._before) @_utils.cached_property def after(self): return _compat.after_func_accept_retry_state(self._after) @_utils.cached_property def before_sleep(self): return _compat.before_sleep_func_accept_retry_state(self._before_sleep) @_utils.cached_property def retry_error_callback(self): return _compat.retry_error_callback_accept_retry_state( self._retry_error_callback) def copy(self, sleep=_unset, stop=_unset, wait=_unset, retry=_unset, before=_unset, after=_unset, before_sleep=_unset, reraise=_unset): """Copy this object with some parameters changed if needed.""" if before_sleep is _unset: before_sleep = self.before_sleep return self.__class__( sleep=self.sleep if sleep is _unset else sleep, stop=self.stop if stop is _unset else stop, wait=self.wait if wait is _unset else wait, retry=self.retry if retry is _unset else retry, before=self.before if before is _unset else before, after=self.after if after is _unset else after, before_sleep=before_sleep, reraise=self.reraise if after is _unset else reraise, ) def __repr__(self): attrs = dict( _utils.visible_attrs(self, attrs={'me': id(self)}), __class__=self.__class__.__name__, ) return ("<%(__class__)s object at 0x%(me)x (stop=%(stop)s, " "wait=%(wait)s, sleep=%(sleep)s, retry=%(retry)s, " "before=%(before)s, after=%(after)s)>") % (attrs) @property def statistics(self): """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 some what stable (not changing), but there 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 there own view of statistics they have collected (in the future we may provide a way to aggregate the various statistics from each thread). """ try: return self._local.statistics except AttributeError: self._local.statistics = {} return self._local.statistics def wraps(self, f): """Wrap a function for retrying. :param f: A function to wraps for retrying. """ @_utils.wraps(f) def wrapped_f(*args, **kw): return self.call(f, *args, **kw) def retry_with(*args, **kwargs): return self.copy(*args, **kwargs).wraps(f) wrapped_f.retry = self wrapped_f.retry_with = retry_with return wrapped_f def begin(self, fn): self.statistics.clear() self.statistics['start_time'] = _utils.now() self.statistics['attempt_number'] = 1 self.statistics['idle_for'] = 0 self.fn = fn def iter(self, retry_state): # noqa fut = retry_state.outcome if fut is None: if self.before is not None: self.before(retry_state) return DoAttempt() is_explicit_retry = retry_state.outcome.failed \ and isinstance(retry_state.outcome.exception(), TryAgain) if not (is_explicit_retry or self.retry(retry_state=retry_state)): return fut.result() if self.after is not None: self.after(retry_state=retry_state) self.statistics['delay_since_first_attempt'] = \ retry_state.seconds_since_start if self.stop(retry_state=retry_state): if self.retry_error_callback: return self.retry_error_callback(retry_state=retry_state) retry_exc = self.retry_error_cls(fut) if self.reraise: raise retry_exc.reraise() six.raise_from(retry_exc, fut.exception()) if self.wait: sleep = self.wait(retry_state=retry_state) else: sleep = 0.0 retry_state.next_action = RetryAction(sleep) retry_state.idle_for += sleep self.statistics['idle_for'] += sleep self.statistics['attempt_number'] += 1 if self.before_sleep is not None: self.before_sleep(retry_state=retry_state) return DoSleep(sleep) def __iter__(self): self.begin(None) 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 class Retrying(BaseRetrying): """Retrying controller.""" def call(self, fn, *args, **kwargs): self.begin(fn) 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()) else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() self.sleep(do) else: return do __call__ = call class Future(futures.Future): """Encapsulates a (future or past) attempted call to a target function.""" def __init__(self, attempt_number): super(Future, self).__init__() self.attempt_number = attempt_number @property def failed(self): """Return whether a exception is being held in this future.""" return self.exception() is not None @classmethod def construct(cls, attempt_number, value, has_exception): """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(object): """State related to a single call wrapped with Retrying.""" def __init__(self, retry_object, fn, args, kwargs): #: Retry call start timestamp self.start_time = _utils.now() #: 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 = 1 #: Last outcome (result or exception) produced by the function self.outcome = None #: Timestamp of the last outcome self.outcome_timestamp = None #: Time spent sleeping in retries self.idle_for = 0 #: Next action as decided by the retry manager self.next_action = None @property def seconds_since_start(self): if self.outcome_timestamp is None: return None return self.outcome_timestamp - self.start_time def prepare_for_next_attempt(self): self.outcome = None self.outcome_timestamp = None self.attempt_number += 1 self.next_action = None def set_result(self, val): ts = _utils.now() fut = Future(self.attempt_number) fut.set_result(val) self.outcome, self.outcome_timestamp = fut, ts def set_exception(self, exc_info): ts = _utils.now() fut = Future(self.attempt_number) _utils.capture(fut, exc_info) self.outcome, self.outcome_timestamp = fut, ts if asyncio: from tenacity._asyncio import AsyncRetrying if tornado: from tenacity.tornadoweb import TornadoRetrying ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/_asyncio.py0000644000655200065520000000420600000000000020724 0ustar00circlecicircleci00000000000000# -*- coding: utf-8 -*- # 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. try: import asyncio except ImportError: asyncio = None import sys from tenacity import BaseRetrying from tenacity import DoAttempt from tenacity import DoSleep from tenacity import RetryCallState if asyncio: class AsyncRetrying(BaseRetrying): def __init__(self, sleep=asyncio.sleep, **kwargs): super(AsyncRetrying, self).__init__(**kwargs) self.sleep = sleep def wraps(self, fn): fn = super().wraps(fn) # Ensure wrapper is recognized as a coroutine function. fn._is_coroutine = asyncio.coroutines._is_coroutine return fn @asyncio.coroutine def call(self, fn, *args, **kwargs): self.begin(fn) 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 from fn(*args, **kwargs) except BaseException: retry_state.set_exception(sys.exc_info()) else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() yield from self.sleep(do) else: return do ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/_utils.py0000644000655200065520000001065400000000000020423 0ustar00circlecicircleci00000000000000# 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 inspect import sys import time from functools import update_wrapper import six # sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint... try: MAX_WAIT = sys.maxint / 2 except AttributeError: MAX_WAIT = 1073741823 if six.PY2: from functools import WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES def wraps(fn): """Do the same as six.wraps but only copy attributes that exist. For example, object instances don't have __name__ attribute, so six.wraps fails. This is fixed in Python 3 (https://bugs.python.org/issue3445), but didn't get backported to six. Also, see https://github.com/benjaminp/six/issues/250. """ def filter_hasattr(obj, attrs): return tuple(a for a in attrs if hasattr(obj, a)) return six.wraps( fn, assigned=filter_hasattr(fn, WRAPPER_ASSIGNMENTS), updated=filter_hasattr(fn, WRAPPER_UPDATES)) def capture(fut, tb): # TODO(harlowja): delete this in future, since its # has to repeatedly calculate this crap. fut.set_exception_info(tb[1], tb[2]) def getargspec(func): # This was deprecated in Python 3. return inspect.getargspec(func) else: from functools import wraps # noqa def capture(fut, tb): fut.set_exception(tb[1]) def getargspec(func): return inspect.getfullargspec(func) def visible_attrs(obj, attrs=None): if attrs is None: attrs = {} for attr_name, attr in inspect.getmembers(obj): if attr_name.startswith("_"): continue attrs[attr_name] = attr return attrs def find_ordinal(pos_num): # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers if pos_num == 0: return "th" elif pos_num == 1: return 'st' elif pos_num == 2: return 'nd' elif pos_num == 3: return 'rd' elif pos_num >= 4 and pos_num <= 20: return 'th' else: return find_ordinal(pos_num % 10) def to_ordinal(pos_num): return "%i%s" % (pos_num, find_ordinal(pos_num)) def get_callback_name(cb): """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: try: segments.append(cb.__name__) if inspect.ismethod(cb): try: # This attribute doesn't exist on py3.x or newer, so # we optionally ignore it... (on those versions of # python `__qualname__` should have been found anyway). segments.insert(0, cb.im_class.__name__) except AttributeError: pass except AttributeError: pass if not segments: return repr(cb) else: try: # When running under sphinx it appears this can be none? if cb.__module__: segments.insert(0, cb.__module__) except AttributeError: pass return ".".join(segments) try: now = time.monotonic # noqa except AttributeError: from monotonic import monotonic as now # noqa class cached_property(object): """A property that is computed once per instance. Upon being computed it replaces itself with an ordinary attribute. Deleting the attribute resets the property. Source: https://github.com/bottlepy/bottle/blob/1de24157e74a6971d136550afe1b63eec5b0df2b/bottle.py#L234-L246 """ # noqa: E501 def __init__(self, func): update_wrapper(self, func) self.func = func def __get__(self, obj, cls): if obj is None: return self value = obj.__dict__[self.func.__name__] = self.func(obj) return value ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/after.py0000644000655200065520000000237200000000000020223 0ustar00circlecicircleci00000000000000# 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 tenacity import _utils def after_nothing(retry_state): """After call strategy that does nothing.""" def after_log(logger, log_level, sec_format="%0.3f"): """After call strategy that logs to some logger the finished attempt.""" log_tpl = ("Finished call to '%s' after " + str(sec_format) + "(s), " "this was the %s time calling it.") def log_it(retry_state): logger.log(log_level, log_tpl, _utils.get_callback_name(retry_state.fn), retry_state.seconds_since_start, _utils.to_ordinal(retry_state.attempt_number)) return log_it ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/before.py0000644000655200065520000000216300000000000020362 0ustar00circlecicircleci00000000000000# 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 tenacity import _utils def before_nothing(retry_state): """Before call strategy that does nothing.""" def before_log(logger, log_level): """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state): logger.log(log_level, "Starting call to '%s', this is the %s time calling it.", _utils.get_callback_name(retry_state.fn), _utils.to_ordinal(retry_state.attempt_number)) return log_it ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/before_sleep.py0000644000655200065520000000250700000000000021554 0ustar00circlecicircleci00000000000000# 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 tenacity import _utils def before_sleep_nothing(retry_state): """Before call strategy that does nothing.""" def before_sleep_log(logger, log_level): """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state): if retry_state.outcome.failed: verb, value = 'raised', retry_state.outcome.exception() else: verb, value = 'returned', retry_state.outcome.result() logger.log(log_level, "Retrying %s in %s seconds as it %s %s.", _utils.get_callback_name(retry_state.fn), getattr(retry_state.next_action, 'sleep'), verb, value) return log_it ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/compat.py0000644000655200065520000002423500000000000020407 0ustar00circlecicircleci00000000000000"""Utilities for providing backward compatibility.""" import inspect from fractions import Fraction from warnings import warn import six from tenacity import _utils def warn_about_non_retry_state_deprecation(cbname, func, stacklevel): msg = ( '"%s" function must accept single "retry_state" parameter,' ' please update %s' % (cbname, _utils.get_callback_name(func))) warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) def warn_about_dunder_non_retry_state_deprecation(fn, stacklevel): msg = ( '"%s" method must be called with' ' single "retry_state" parameter' % (_utils.get_callback_name(fn))) warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) def func_takes_retry_state(func): if not six.callable(func): raise Exception(func) return False if not inspect.isfunction(func) and not inspect.ismethod(func): # func is a callable object rather than a function/method func = func.__call__ func_spec = _utils.getargspec(func) return 'retry_state' in func_spec.args _unset = object() def _make_unset_exception(func_name, **kwargs): missing = [] for k, v in six.iteritems(kwargs): 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, delay): # 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) retry_state.outcome_timestamp = (retry_state.start_time + Fraction(delay)) assert retry_state.seconds_since_start == delay def make_retry_state(previous_attempt_number, delay_since_first_attempt, last_result=None): """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) from tenacity import RetryCallState retry_state = RetryCallState(None, None, (), {}) retry_state.attempt_number = previous_attempt_number if last_result is not None: retry_state.outcome = last_result else: retry_state.set_result(None) _set_delay_since_start(retry_state, delay_since_first_attempt) return retry_state def func_takes_last_result(waiter): """Check if function has a "last_result" parameter. Needed to provide backward compatibility for wait functions that didn't take "last_result" in the beginning. """ if not six.callable(waiter): return False if not inspect.isfunction(waiter) and not inspect.ismethod(waiter): # waiter is a class, check dunder-call rather than dunder-init. waiter = waiter.__call__ waiter_spec = _utils.getargspec(waiter) return 'last_result' in waiter_spec.args def stop_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "stop" signature.""" @_utils.wraps(fn) def new_fn(self, previous_attempt_number=_unset, delay_since_first_attempt=_unset, retry_state=None): if retry_state is None: from tenacity import RetryCallState retry_state_passed_as_non_kwarg = ( previous_attempt_number is not _unset and isinstance(previous_attempt_number, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = previous_attempt_number else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = make_retry_state( previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt) return fn(self, retry_state=retry_state) return new_fn def stop_func_accept_retry_state(stop_func): """Wrap "stop" function to accept "retry_state" parameter.""" if not six.callable(stop_func): return stop_func if func_takes_retry_state(stop_func): return stop_func @_utils.wraps(stop_func) def wrapped_stop_func(retry_state): warn_about_non_retry_state_deprecation( 'stop', stop_func, stacklevel=4) return stop_func( retry_state.attempt_number, retry_state.seconds_since_start, ) return wrapped_stop_func def wait_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "wait" signature.""" @_utils.wraps(fn) def new_fn(self, previous_attempt_number=_unset, delay_since_first_attempt=_unset, last_result=None, retry_state=None): if retry_state is None: from tenacity import RetryCallState retry_state_passed_as_non_kwarg = ( previous_attempt_number is not _unset and isinstance(previous_attempt_number, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = previous_attempt_number else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = make_retry_state( previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt, last_result=last_result) return fn(self, retry_state=retry_state) return new_fn def wait_func_accept_retry_state(wait_func): """Wrap wait function to accept "retry_state" parameter.""" if not six.callable(wait_func): return wait_func if func_takes_retry_state(wait_func): return wait_func if func_takes_last_result(wait_func): @_utils.wraps(wait_func) def wrapped_wait_func(retry_state): warn_about_non_retry_state_deprecation( 'wait', wait_func, stacklevel=4) return wait_func( retry_state.attempt_number, retry_state.seconds_since_start, last_result=retry_state.outcome, ) else: @_utils.wraps(wait_func) def wrapped_wait_func(retry_state): warn_about_non_retry_state_deprecation( 'wait', wait_func, stacklevel=4) return wait_func( retry_state.attempt_number, retry_state.seconds_since_start, ) return wrapped_wait_func def retry_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "retry" signature.""" @_utils.wraps(fn) def new_fn(self, attempt=_unset, retry_state=None): if retry_state is None: from tenacity import RetryCallState if attempt is _unset: raise _make_unset_exception('retry', attempt=attempt) retry_state_passed_as_non_kwarg = ( attempt is not _unset and isinstance(attempt, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = attempt else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = RetryCallState(None, None, (), {}) retry_state.outcome = attempt return fn(self, retry_state=retry_state) return new_fn def retry_func_accept_retry_state(retry_func): """Wrap "retry" function to accept "retry_state" parameter.""" if not six.callable(retry_func): return retry_func if func_takes_retry_state(retry_func): return retry_func @_utils.wraps(retry_func) def wrapped_retry_func(retry_state): warn_about_non_retry_state_deprecation( 'retry', retry_func, stacklevel=4) return retry_func(retry_state.outcome) return wrapped_retry_func def before_func_accept_retry_state(fn): """Wrap "before" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_before_func(retry_state): # func, trial_number, trial_time_taken warn_about_non_retry_state_deprecation('before', fn, stacklevel=4) return fn( retry_state.fn, retry_state.attempt_number, ) return wrapped_before_func def after_func_accept_retry_state(fn): """Wrap "after" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_after_sleep_func(retry_state): # func, trial_number, trial_time_taken warn_about_non_retry_state_deprecation('after', fn, stacklevel=4) return fn( retry_state.fn, retry_state.attempt_number, retry_state.seconds_since_start) return wrapped_after_sleep_func def before_sleep_func_accept_retry_state(fn): """Wrap "before_sleep" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_before_sleep_func(retry_state): # retry_object, sleep, last_result warn_about_non_retry_state_deprecation( 'before_sleep', fn, stacklevel=4) return fn( retry_state.retry_object, sleep=getattr(retry_state.next_action, 'sleep'), last_result=retry_state.outcome) return wrapped_before_sleep_func def retry_error_callback_accept_retry_state(fn): if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_retry_error_callback(retry_state): warn_about_non_retry_state_deprecation( 'retry_error_callback', fn, stacklevel=4) return fn(retry_state.outcome) return wrapped_retry_error_callback ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/nap.py0000644000655200065520000000213600000000000017676 0ustar00circlecicircleci00000000000000# -*- coding: utf-8 -*- # 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 #: Default sleep strategy. sleep = time.sleep class sleep_using_event(object): """Sleep strategy that waits on an event to be set.""" def __init__(self, event): self.event = event def __call__(self, timeout): # 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/retry.py0000644000655200065520000001322100000000000020262 0ustar00circlecicircleci00000000000000# 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 abc import re import six from tenacity import compat as _compat @six.add_metaclass(abc.ABCMeta) class retry_base(object): """Abstract base class for retry strategies.""" @abc.abstractmethod def __call__(self, retry_state): pass def __and__(self, other): return retry_all(self, other) def __or__(self, other): return retry_any(self, other) class _retry_never(retry_base): """Retry strategy that never rejects any result.""" def __call__(self, retry_state): return False retry_never = _retry_never() class _retry_always(retry_base): """Retry strategy that always rejects any result.""" def __call__(self, retry_state): 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): self.predicate = predicate @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if retry_state.outcome.failed: return self.predicate(retry_state.outcome.exception()) 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=Exception): self.exception_types = exception_types super(retry_if_exception_type, self).__init__( lambda e: isinstance(e, 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=Exception): self.exception_types = exception_types super(retry_unless_exception_type, self).__init__( lambda e: not isinstance(e, exception_types)) @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): # always retry if no exception was raised if not retry_state.outcome.failed: return True return self.predicate(retry_state.outcome.exception()) class retry_if_result(retry_base): """Retries if the result verifies a predicate.""" def __init__(self, predicate): self.predicate = predicate @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return self.predicate(retry_state.outcome.result()) class retry_if_not_result(retry_base): """Retries if the result refutes a predicate.""" def __init__(self, predicate): self.predicate = predicate @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return not self.predicate(retry_state.outcome.result()) class retry_if_exception_message(retry_if_exception): """Retries if an exception message equals or matches.""" def __init__(self, message=None, match=None): if message and match: raise TypeError( "{}() takes either 'message' or 'match', not both".format( self.__class__.__name__)) # set predicate if message: def message_fnc(exception): return message == str(exception) predicate = message_fnc elif match: prog = re.compile(match) def match_fnc(exception): return prog.match(str(exception)) predicate = match_fnc else: raise TypeError( "{}() missing 1 required argument 'message' or 'match'". format(self.__class__.__name__)) super(retry_if_exception_message, self).__init__(predicate) class retry_if_not_exception_message(retry_if_exception_message): """Retries until an exception message equals or matches.""" def __init__(self, *args, **kwargs): super(retry_if_not_exception_message, self).__init__(*args, **kwargs) # invert predicate if_predicate = self.predicate self.predicate = lambda *args_, **kwargs_: not if_predicate( *args_, **kwargs_) @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return True return self.predicate(retry_state.outcome.exception()) class retry_any(retry_base): """Retries if any of the retries condition is valid.""" def __init__(self, *retries): self.retries = tuple(_compat.retry_func_accept_retry_state(r) for r in retries) @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): return any(r(retry_state) for r in self.retries) class retry_all(retry_base): """Retries if all the retries condition are valid.""" def __init__(self, *retries): self.retries = tuple(_compat.retry_func_accept_retry_state(r) for r in retries) @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): return all(r(retry_state) for r in self.retries) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/stop.py0000644000655200065520000000553500000000000020113 0ustar00circlecicircleci00000000000000# 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 abc import six from tenacity import compat as _compat @six.add_metaclass(abc.ABCMeta) class stop_base(object): """Abstract base class for stop strategies.""" @abc.abstractmethod def __call__(self, retry_state): pass def __and__(self, other): return stop_all(self, other) def __or__(self, other): return stop_any(self, other) class stop_any(stop_base): """Stop if any of the stop condition is valid.""" def __init__(self, *stops): self.stops = tuple(_compat.stop_func_accept_retry_state(stop_func) for stop_func in stops) @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): 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): self.stops = tuple(_compat.stop_func_accept_retry_state(stop_func) for stop_func in stops) @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return all(x(retry_state) for x in self.stops) class _stop_never(stop_base): """Never stop.""" @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return False stop_never = _stop_never() class stop_when_event_set(stop_base): """Stop when the given event is set.""" def __init__(self, event): self.event = event @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return self.event.is_set() class stop_after_attempt(stop_base): """Stop when the previous attempt >= max_attempt.""" def __init__(self, max_attempt_number): self.max_attempt_number = max_attempt_number @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return retry_state.attempt_number >= self.max_attempt_number class stop_after_delay(stop_base): """Stop when the time from the first attempt >= limit.""" def __init__(self, max_delay): self.max_delay = max_delay @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return retry_state.seconds_since_start >= self.max_delay ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1573042360.5683148 tenacity-6.0.0/tenacity/tests/0000755000655200065520000000000000000000000017706 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/tests/__init__.py0000644000655200065520000000000000000000000022005 0ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/tests/test_asyncio.py0000644000655200065520000000610100000000000022762 0ustar00circlecicircleci00000000000000# coding: utf-8 # 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 unittest import six from tenacity import RetryError from tenacity import _asyncio as tasyncio from tenacity import retry, stop_after_attempt from tenacity.tests.test_tenacity import NoIOErrorAfterCount def asynctest(callable_): callable_ = asyncio.coroutine(callable_) @six.wraps(callable_) def wrapper(*a, **kw): loop = asyncio.get_event_loop() return loop.run_until_complete(callable_(*a, **kw)) return wrapper @retry async def _retryable_coroutine(thing): await asyncio.sleep(0.00001) return thing.go() @retry(stop=stop_after_attempt(2)) @asyncio.coroutine def _retryable_coroutine_with_2_attempts(thing): yield from asyncio.sleep(0.00001) thing.go() class TestAsync(unittest.TestCase): @asynctest def test_retry(self): assert asyncio.iscoroutinefunction(_retryable_coroutine) thing = NoIOErrorAfterCount(5) yield from _retryable_coroutine(thing) assert thing.counter == thing.count @asynctest def test_stop_after_attempt(self): assert asyncio.iscoroutinefunction( _retryable_coroutine_with_2_attempts) thing = NoIOErrorAfterCount(2) try: yield from _retryable_coroutine_with_2_attempts(thing) except RetryError: assert thing.counter == 2 def test_repr(self): repr(tasyncio.AsyncRetrying()) @asynctest def test_attempt_number_is_correct_for_interleaved_coroutines(self): attempts = [] def after(retry_state): attempts.append((retry_state.args[0], retry_state.attempt_number)) thing1 = NoIOErrorAfterCount(3) thing2 = NoIOErrorAfterCount(3) future1 = asyncio.ensure_future( _retryable_coroutine.retry_with(after=after)(thing1)) future2 = asyncio.ensure_future( _retryable_coroutine.retry_with(after=after)(thing2)) yield from asyncio.gather(future1, future2) # There's no waiting on retry, only a wait in the coroutine, so the # executions should be interleaved. thing1_attempts = attempts[::2] things1, attempt_nos1 = zip(*thing1_attempts) assert all(thing is thing1 for thing in things1) assert list(attempt_nos1) == [1, 2, 3] thing2_attempts = attempts[1::2] things2, attempt_nos2 = zip(*thing2_attempts) assert all(thing is thing2 for thing in things2) assert list(attempt_nos2) == [1, 2, 3] if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/tests/test_tenacity.py0000644000655200065520000013752200000000000023151 0ustar00circlecicircleci00000000000000# Copyright 2016 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 logging import time import unittest import warnings from contextlib import contextmanager from copy import copy import pytest import six.moves import tenacity from tenacity import RetryError, Retrying, retry from tenacity.compat import make_retry_state class TestBase(unittest.TestCase): def test_repr(self): repr(tenacity.BaseRetrying()) class TestStopConditions(unittest.TestCase): def test_never_stop(self): r = Retrying() self.assertFalse(r.stop(make_retry_state(3, 6546))) def test_stop_any(self): stop = tenacity.stop_any( tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)) def s(*args): 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): stop = tenacity.stop_all( tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)) def s(*args): 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): stop = tenacity.stop_after_delay(1) | tenacity.stop_after_attempt(4) def s(*args): 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): stop = tenacity.stop_after_delay(1) & tenacity.stop_after_attempt(4) def s(*args): 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): 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): r = Retrying(stop=tenacity.stop_after_delay(1)) 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_legacy_explicit_stop_type(self): Retrying(stop="stop_after_attempt") def test_stop_backward_compat(self): r = Retrying(stop=lambda attempt, delay: attempt == delay) with reports_deprecation_warning(): self.assertFalse(r.stop(make_retry_state(1, 3))) with reports_deprecation_warning(): self.assertFalse(r.stop(make_retry_state(100, 99))) with reports_deprecation_warning(): self.assertTrue(r.stop(make_retry_state(101, 101))) def test_retry_child_class_with_override_backward_compat(self): class MyStop(tenacity.stop_after_attempt): def __init__(self): super(MyStop, self).__init__(1) def __call__(self, attempt_number, seconds_since_start): return super(MyStop, self).__call__( attempt_number, seconds_since_start) retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=MyStop()) def failing(): raise NotImplementedError() with pytest.raises(RetryError): retrying.call(failing) def test_stop_func_with_retry_state(self): def stop_func(retry_state): 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): r = Retrying() self.assertEqual(0, r.wait(18, 9879)) def test_fixed_sleep(self): r = Retrying(wait=tenacity.wait_fixed(1)) self.assertEqual(1, r.wait(12, 6546)) def test_incrementing_sleep(self): r = Retrying(wait=tenacity.wait_incrementing( start=500, increment=100)) self.assertEqual(500, r.wait(1, 6546)) self.assertEqual(600, r.wait(2, 6546)) self.assertEqual(700, r.wait(3, 6546)) def test_random_sleep(self): r = Retrying(wait=tenacity.wait_random(min=1, max=20)) times = set() for x in six.moves.range(1000): times.add(r.wait(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_without_min(self): r = Retrying(wait=tenacity.wait_random(max=2)) times = set() times.add(r.wait(1, 6546)) times.add(r.wait(1, 6546)) times.add(r.wait(1, 6546)) times.add(r.wait(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): r = Retrying(wait=tenacity.wait_exponential()) self.assertEqual(r.wait(1, 0), 1) self.assertEqual(r.wait(2, 0), 2) self.assertEqual(r.wait(3, 0), 4) self.assertEqual(r.wait(4, 0), 8) self.assertEqual(r.wait(5, 0), 16) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 64) self.assertEqual(r.wait(8, 0), 128) def test_exponential_with_max_wait(self): r = Retrying(wait=tenacity.wait_exponential(max=40)) self.assertEqual(r.wait(1, 0), 1) self.assertEqual(r.wait(2, 0), 2) self.assertEqual(r.wait(3, 0), 4) self.assertEqual(r.wait(4, 0), 8) self.assertEqual(r.wait(5, 0), 16) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 40) self.assertEqual(r.wait(8, 0), 40) self.assertEqual(r.wait(50, 0), 40) def test_exponential_with_min_wait(self): r = Retrying(wait=tenacity.wait_exponential(min=20)) self.assertEqual(r.wait(1, 0), 20) self.assertEqual(r.wait(2, 0), 20) self.assertEqual(r.wait(3, 0), 20) self.assertEqual(r.wait(4, 0), 20) self.assertEqual(r.wait(5, 0), 20) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 64) self.assertEqual(r.wait(8, 0), 128) self.assertEqual(r.wait(20, 0), 524288) def test_exponential_with_max_wait_and_multiplier(self): r = Retrying(wait=tenacity.wait_exponential( max=50, multiplier=1)) self.assertEqual(r.wait(1, 0), 1) self.assertEqual(r.wait(2, 0), 2) self.assertEqual(r.wait(3, 0), 4) self.assertEqual(r.wait(4, 0), 8) self.assertEqual(r.wait(5, 0), 16) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 50) self.assertEqual(r.wait(8, 0), 50) self.assertEqual(r.wait(50, 0), 50) def test_exponential_with_min_wait_and_multiplier(self): r = Retrying(wait=tenacity.wait_exponential( min=20, multiplier=2)) self.assertEqual(r.wait(1, 0), 20) self.assertEqual(r.wait(2, 0), 20) self.assertEqual(r.wait(3, 0), 20) self.assertEqual(r.wait(4, 0), 20) self.assertEqual(r.wait(5, 0), 32) self.assertEqual(r.wait(6, 0), 64) self.assertEqual(r.wait(7, 0), 128) self.assertEqual(r.wait(8, 0), 256) self.assertEqual(r.wait(20, 0), 1048576) def test_exponential_with_min_wait_and_max_wait(self): r = Retrying(wait=tenacity.wait_exponential(min=10, max=100)) self.assertEqual(r.wait(1, 0), 10) self.assertEqual(r.wait(2, 0), 10) self.assertEqual(r.wait(3, 0), 10) self.assertEqual(r.wait(4, 0), 10) self.assertEqual(r.wait(5, 0), 16) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 64) self.assertEqual(r.wait(8, 0), 100) self.assertEqual(r.wait(9, 0), 100) self.assertEqual(r.wait(20, 0), 100) def test_legacy_explicit_wait_type(self): Retrying(wait="exponential_sleep") def test_wait_backward_compat_with_result(self): captures = [] def wait_capture(attempt, delay, last_result=None): captures.append(last_result) return 1 def dying(): raise Exception("Broken") r_attempts = 10 r = Retrying(wait=wait_capture, sleep=lambda secs: None, stop=tenacity.stop_after_attempt(r_attempts), reraise=True) with reports_deprecation_warning(): self.assertRaises(Exception, r.call, dying) self.assertEqual(r_attempts - 1, len(captures)) self.assertTrue(all([r.failed for r in captures])) def test_wait_func(self): def wait_func(retry_state): return retry_state.attempt_number * retry_state.seconds_since_start 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): 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 six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 8) self.assertGreaterEqual(w, 5) def test_wait_double_sum(self): r = Retrying(wait=tenacity.wait_random(0, 3) + tenacity.wait_fixed(5)) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 8) self.assertGreaterEqual(w, 5) def test_wait_triple_sum(self): 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 six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 9) self.assertGreaterEqual(w, 6) def test_wait_arbitrary_sum(self): r = Retrying(wait=sum([tenacity.wait_fixed(1), tenacity.wait_random(0, 3), tenacity.wait_fixed(5), tenacity.wait_none()])) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 9) self.assertGreaterEqual(w, 6) def _assert_range(self, wait, min_, max_): self.assertLess(wait, max_) self.assertGreaterEqual(wait, min_) def _assert_inclusive_range(self, wait, low, high): self.assertLessEqual(wait, high) self.assertGreaterEqual(wait, low) def _assert_inclusive_epsilon(self, wait, target, epsilon): self.assertLessEqual(wait, target + epsilon) self.assertGreaterEqual(wait, target - epsilon) def test_wait_chain(self): r = Retrying(wait=tenacity.wait_chain( *[tenacity.wait_fixed(1) for i in six.moves.range(2)] + [tenacity.wait_fixed(4) for i in six.moves.range(2)] + [tenacity.wait_fixed(8) for i in six.moves.range(1)])) for i in six.moves.range(10): w = r.wait(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): sleep_intervals = [] r = Retrying( sleep=sleep_intervals.append, wait=tenacity.wait_chain(*[ tenacity.wait_fixed(i + 1) for i in six.moves.range(3) ]), stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_if_result(lambda x: x == 1), ) @r.wraps def always_return_1(): 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): fn = tenacity.wait_random_exponential(0.5, 60.0) for _ in six.moves.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) fn = tenacity.wait_random_exponential(10, 5) for _ in six.moves.range(1000): self._assert_inclusive_range( fn(make_retry_state(1, 0)), 0.00, 5.00 ) # Default arguments exist fn = tenacity.wait_random_exponential() fn(0, 0) def test_wait_random_exponential_statistically(self): fn = tenacity.wait_random_exponential(0.5, 60.0) attempt = [] for i in six.moves.range(10): attempt.append( [fn(make_retry_state(i, 0)) for _ in six.moves.range(4000)] ) def mean(lst): 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_backward_compat(self): """Ensure Retrying object accepts both old and newstyle wait funcs.""" def wait1(previous_attempt_number, delay_since_first_attempt): wait1.calls.append(( previous_attempt_number, delay_since_first_attempt)) return 0 wait1.calls = [] def wait2(previous_attempt_number, delay_since_first_attempt, last_result): wait2.calls.append(( previous_attempt_number, delay_since_first_attempt, last_result)) return 0 wait2.calls = [] def dying(): raise Exception("Broken") retrying1 = Retrying(wait=wait1, stop=tenacity.stop_after_attempt(4)) with reports_deprecation_warning(): self.assertRaises(Exception, lambda: retrying1.call(dying)) self.assertEqual([t[0] for t in wait1.calls], [1, 2, 3]) # This assumes that 3 iterations complete within 1 second. self.assertTrue(all(t[1] < 1 for t in wait1.calls)) retrying2 = Retrying(wait=wait2, stop=tenacity.stop_after_attempt(4)) with reports_deprecation_warning(): self.assertRaises(Exception, lambda: retrying2.call(dying)) self.assertEqual([t[0] for t in wait2.calls], [1, 2, 3]) # This assumes that 3 iterations complete within 1 second. self.assertTrue(all(t[1] < 1 for t in wait2.calls)) self.assertEqual([str(t[2].exception()) for t in wait2.calls], ['Broken'] * 3) def test_wait_class_backward_compatibility(self): """Ensure builtin objects accept both old and new parameters.""" waitobj = tenacity.wait_fixed(5) self.assertEqual(waitobj(1, 0.1), 5) self.assertEqual( waitobj(1, 0.1, tenacity.Future.construct(1, 1, False)), 5) retry_state = make_retry_state(123, 456) self.assertEqual(retry_state.attempt_number, 123) self.assertEqual(retry_state.seconds_since_start, 456) self.assertEqual(waitobj(retry_state=retry_state), 5) def test_wait_retry_state_attributes(self): 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): raise ExtractCallState(retry_state) retrying = Retrying( wait=waitfunc, retry=(tenacity.retry_if_exception_type() | tenacity.retry_if_result(lambda result: result == 123))) def returnval(): return 123 try: retrying.call(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.assertGreater(retry_state.outcome_timestamp, retry_state.start_time) def dying(): raise Exception("Broken") try: retrying.call(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.assertGreater(retry_state.outcome_timestamp, retry_state.start_time) class TestRetryConditions(unittest.TestCase): def test_retry_if_result(self): retry = (tenacity.retry_if_result(lambda x: x == 1)) def r(fut): 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): retry = (tenacity.retry_if_not_result(lambda x: x == 1)) def r(fut): 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): retry = tenacity.retry_any( tenacity.retry_if_result(lambda x: x == 1), tenacity.retry_if_result(lambda x: x == 2)) def r(fut): 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): retry = tenacity.retry_all( tenacity.retry_if_result(lambda x: x == 1), tenacity.retry_if_result(lambda x: isinstance(x, int))) def r(fut): 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): retry = (tenacity.retry_if_result(lambda x: x == 1) & tenacity.retry_if_result(lambda x: isinstance(x, int))) def r(fut): 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): retry = (tenacity.retry_if_result(lambda x: x == "foo") | tenacity.retry_if_result(lambda x: isinstance(x, int))) def r(fut): 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 _raise_try_again(self): self._attempts += 1 if self._attempts < 3: raise tenacity.TryAgain def test_retry_try_again(self): self._attempts = 0 Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never).call(self._raise_try_again) self.assertEqual(3, self._attempts) def test_retry_try_again_forever(self): def _r(): raise tenacity.TryAgain r = Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never) self.assertRaises(tenacity.RetryError, r.call, _r) self.assertEqual(5, r.statistics['attempt_number']) def test_retry_try_again_forever_reraise(self): def _r(): 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): with self.assertRaises(TypeError): tenacity.retry_if_exception_message() def test_retry_if_exception_message_negative_too_many_inputs(self): with self.assertRaises(TypeError): tenacity.retry_if_exception_message( message="negative", match="negative") class NoneReturnUntilAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """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(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Raise an IOError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise IOError("Hi there, I'm an IOError") return True class NoNameErrorAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """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 NameErrorUntilCount(object): """Holds counter state for invoking a method several times in a row.""" derived_message = "Hi there, I'm a NameError" def __init__(self, count): self.counter = 0 self.count = count def go(self): """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(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Return True until after count threshold has been crossed. Then raise an IOError. """ if self.counter < self.count: self.counter += 1 return True raise IOError("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): self.value = value def __str__(self): return self.value class NoCustomErrorAfterCount(object): """Holds counter state for invoking a method several times in a row.""" derived_message = "This is a Custom exception class" def __init__(self, count): self.counter = 0 self.count = count def go(self): """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, **kwargs): super(CapturingHandler, self).__init__(*args, **kwargs) self.records = [] def emit(self, record): self.records.append(record) def current_time_ms(): return int(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): 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): return thing.go() @retry(retry=tenacity.retry_if_exception_type(IOError)) def _retryable_test_with_exception_type_io(thing): 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): return thing.go() @retry(retry=tenacity.retry_unless_exception_type(NameError)) def _retryable_test_with_unless_exception_type_name(thing): 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): return thing.go() @retry(retry=tenacity.retry_unless_exception_type()) def _retryable_test_with_unless_exception_type_no_input(thing): 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): return thing.go() @retry(retry=tenacity.retry_if_not_exception_message( message=NoCustomErrorAfterCount.derived_message)) def _retryable_test_if_not_exception_message_message(thing): return thing.go() @retry(retry=tenacity.retry_if_exception_message( match=NoCustomErrorAfterCount.derived_message[:3] + ".*")) def _retryable_test_if_exception_message_match(thing): 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): return thing.go() @retry(retry=tenacity.retry_if_not_exception_message( message=NameErrorUntilCount.derived_message)) def _retryable_test_not_exception_message_delay(thing): return thing.go() @retry def _retryable_default(thing): return thing.go() @retry() def _retryable_default_f(thing): return thing.go() @retry(retry=tenacity.retry_if_exception_type(CustomError)) def _retryable_test_with_exception_type_custom(thing): 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): return thing.go() class TestDecoratorWrapper(unittest.TestCase): def test_with_wait(self): 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_retry_with(self): start = current_time_ms() result = _retryable_test_with_wait.retry_with( wait=tenacity.wait_fixed(0.1))(NoneReturnUntilAfterCount(5)) t = current_time_ms() - start self.assertGreaterEqual(t, 500) self.assertTrue(result) def test_with_stop_on_return_value(self): 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): try: _retryable_test_with_stop(NoIOErrorAfterCount(5)) self.fail("Expected IOError") except IOError as re: self.assertTrue(isinstance(re, IOError)) print(re) def test_retry_if_exception_of_type(self): 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_until_exception_of_type_attempt_number(self): try: self.assertTrue(_retryable_test_with_unless_exception_type_name( NameErrorUntilCount(5))) except NameError as e: s = _retryable_test_with_unless_exception_type_name.\ retry.statistics self.assertTrue(s['attempt_number'] == 6) print(e) else: self.fail("Expected NameError") def test_retry_until_exception_of_type_no_type(self): 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.\ retry.statistics self.assertTrue(s['attempt_number'] == 6) print(e) else: self.fail("Expected NameError") def test_retry_until_exception_of_type_wrong_exception(self): 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): try: self.assertTrue(_retryable_test_if_exception_message_message( NoCustomErrorAfterCount(3))) except CustomError: print(_retryable_test_if_exception_message_message.retry. statistics) self.fail("CustomError should've been retried from errormessage") def test_retry_if_not_exception_message(self): try: self.assertTrue(_retryable_test_if_not_exception_message_message( NoCustomErrorAfterCount(2))) except CustomError: s = _retryable_test_if_not_exception_message_message.retry.\ statistics self.assertTrue(s['attempt_number'] == 1) def test_retry_if_not_exception_message_delay(self): try: self.assertTrue(_retryable_test_not_exception_message_delay( NameErrorUntilCount(3))) except NameError: s = _retryable_test_not_exception_message_delay.retry.statistics print(s['attempt_number']) self.assertTrue(s['attempt_number'] == 4) def test_retry_if_exception_message_match(self): 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): try: self.assertTrue(_retryable_test_if_not_exception_message_message( NoCustomErrorAfterCount(2))) except CustomError: s = _retryable_test_if_not_exception_message_message.retry.\ statistics self.assertTrue(s['attempt_number'] == 1) def test_defaults(self): 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): """Test that six.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(object): def __call__(self): 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_child_class_with_override_backward_compat(self): def always_true(_): return True class MyRetry(tenacity.retry_if_exception): def __init__(self): super(MyRetry, self).__init__(always_true) def __call__(self, attempt): return super(MyRetry, self).__call__(attempt) retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(1), retry=MyRetry()) def failing(): raise NotImplementedError() with pytest.raises(RetryError): retrying.call(failing) class TestBeforeAfterAttempts(unittest.TestCase): _attempt_number = 0 def test_before_attempts(self): TestBeforeAfterAttempts._attempt_number = 0 def _before(retry_state): TestBeforeAfterAttempts._attempt_number = \ retry_state.attempt_number @retry(wait=tenacity.wait_fixed(1), stop=tenacity.stop_after_attempt(1), before=_before) def _test_before(): pass _test_before() self.assertTrue(TestBeforeAfterAttempts._attempt_number == 1) def test_after_attempts(self): TestBeforeAfterAttempts._attempt_number = 0 def _after(retry_state): 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(): if TestBeforeAfterAttempts._attempt_number < 2: raise Exception("testing after_attempts handler") else: pass _test_after() self.assertTrue(TestBeforeAfterAttempts._attempt_number == 2) def test_before_sleep(self): def _before_sleep(retry_state): self.assertGreater(retry_state.next_action.sleep, 0) _before_sleep.attempt_number = retry_state.attempt_number @retry(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep) def _test_before_sleep(): if _before_sleep.attempt_number < 2: raise Exception("testing before_sleep_attempts handler") _test_before_sleep() self.assertEqual(_before_sleep.attempt_number, 2) def test_before_sleep_backward_compat(self): def _before_sleep(retry_obj, sleep, last_result): self.assertGreater(sleep, 0) _before_sleep.attempt_number = \ retry_obj.statistics['attempt_number'] _before_sleep.attempt_number = 0 @retry(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep) def _test_before_sleep(): if _before_sleep.attempt_number < 2: raise Exception("testing before_sleep_attempts handler") with reports_deprecation_warning(): _test_before_sleep() self.assertEqual(_before_sleep.attempt_number, 2) def _before_sleep(self, retry_state): self.slept += 1 def test_before_sleep_backward_compat_method(self): self.slept = 0 @retry(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=self._before_sleep) def _test_before_sleep(): raise Exception("testing before_sleep_attempts handler") try: _test_before_sleep() except tenacity.RetryError: pass self.assertEqual(self.slept, 2) def test_before_sleep_log_raises(self): 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) retrying.call(thing.go) finally: logger.removeHandler(handler) etalon_re = r'Retrying .* in 0\.01 seconds as it raised .*\.' self.assertEqual(len(handler.records), 2) self.assertRegexpMatches(handler.records[0].getMessage(), etalon_re) self.assertRegexpMatches(handler.records[1].getMessage(), etalon_re) def test_before_sleep_log_returns(self): 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) _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.call(thing.go) finally: logger.removeHandler(handler) self.assertEqual(len(handler.records), 2) etalon_re = r'Retrying .* in 0\.01 seconds as it returned None' self.assertRegexpMatches(handler.records[0].getMessage(), etalon_re) self.assertRegexpMatches(handler.records[1].getMessage(), etalon_re) class TestReraiseExceptions(unittest.TestCase): def test_reraise_by_default(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), reraise=True) def _reraised_by_default(): 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): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2)) def _raise_key_error(): calls.append('x') raise KeyError("Bad key") def _reraised_key_error(): 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): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), retry=lambda retry_state: True) def _mock_fn(): calls.append('x') def _reraised_mock_fn(): 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): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), retry=lambda retry_state: True, reraise=True) def _mock_fn(): calls.append('x') self.assertRaises(tenacity.RetryError, _mock_fn) self.assertEqual(2, len(calls)) class TestStatistics(unittest.TestCase): def test_stats(self): @retry() def _foobar(): return 42 self.assertEqual({}, _foobar.retry.statistics) _foobar() self.assertEqual(1, _foobar.retry.statistics['attempt_number']) def test_stats_failing(self): @retry(stop=tenacity.stop_after_attempt(2)) def _foobar(): raise ValueError(42) self.assertEqual({}, _foobar.retry.statistics) try: _foobar() except Exception: pass self.assertEqual(2, _foobar.retry.statistics['attempt_number']) class TestRetryErrorCallback(unittest.TestCase): def setUp(self): self._attempt_number = 0 self._callback_called = False def _callback(self, fut): self._callback_called = True return fut def test_retry_error_callback_backward_compat(self): num_attempts = 3 def retry_error_callback(fut): retry_error_callback.called_times += 1 return fut retry_error_callback.called_times = 0 @retry(stop=tenacity.stop_after_attempt(num_attempts), retry_error_callback=retry_error_callback) def _foobar(): self._attempt_number += 1 raise Exception("This exception should not be raised") with reports_deprecation_warning(): result = _foobar() self.assertEqual(retry_error_callback.called_times, 1) self.assertEqual(num_attempts, self._attempt_number) self.assertIsInstance(result, tenacity.Future) def test_retry_error_callback(self): num_attempts = 3 def retry_error_callback(retry_state): retry_error_callback.called_times += 1 return retry_state.outcome retry_error_callback.called_times = 0 @retry(stop=tenacity.stop_after_attempt(num_attempts), retry_error_callback=retry_error_callback) def _foobar(): self._attempt_number += 1 raise Exception("This exception should not be raised") result = _foobar() self.assertEqual(retry_error_callback.called_times, 1) self.assertEqual(num_attempts, self._attempt_number) self.assertIsInstance(result, tenacity.Future) class TestContextManager(unittest.TestCase): def test_context_manager_retry_one(self): 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): from tenacity import Retrying class CustomError(Exception): pass retry = Retrying(retry=tenacity.retry_if_exception_type(IOError)) def test(): for attempt in retry: with attempt: raise CustomError("Don't retry!") self.assertRaises(CustomError, test) def test_context_manager_retry_error(self): from tenacity import Retrying retry = Retrying(stop=tenacity.stop_after_attempt(2)) def test(): for attempt in retry: with attempt: raise Exception("Retry it!") self.assertRaises(RetryError, test) def test_context_manager_reraise(self): from tenacity import Retrying class CustomError(Exception): pass retry = Retrying(reraise=True, stop=tenacity.stop_after_attempt(2)) def test(): for attempt in retry: with attempt: raise CustomError("Don't retry!") self.assertRaises(CustomError, test) class TestRetryException(unittest.TestCase): def test_retry_error_is_pickleable(self): import pickle expected = RetryError(last_attempt=123) pickled = pickle.dumps(expected) actual = pickle.loads(pickled) self.assertEqual(expected.last_attempt, actual.last_attempt) @contextmanager def reports_deprecation_warning(): __tracebackhide__ = True oldfilters = copy(warnings.filters) warnings.simplefilter('always') try: with pytest.warns(DeprecationWarning): yield finally: warnings.filters = oldfilters if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/tests/test_tornado.py0000644000655200065520000000417600000000000022775 0ustar00circlecicircleci00000000000000# coding: utf-8 # 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 tenacity import RetryError, retry, stop_after_attempt from tenacity import tornadoweb from tenacity.tests.test_tenacity import NoIOErrorAfterCount from tornado import gen from tornado import testing @retry @gen.coroutine def _retryable_coroutine(thing): yield gen.sleep(0.00001) thing.go() @retry(stop=stop_after_attempt(2)) @gen.coroutine def _retryable_coroutine_with_2_attempts(thing): yield gen.sleep(0.00001) thing.go() class TestTornado(testing.AsyncTestCase): @testing.gen_test def test_retry(self): 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): 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): repr(tornadoweb.TornadoRetrying()) def test_old_tornado(self): 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): pass finally: gen.is_coroutine_function = old_attr if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/tornadoweb.py0000644000655200065520000000325700000000000021271 0ustar00circlecicircleci00000000000000# -*- coding: utf-8 -*- # 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 from tenacity import BaseRetrying from tenacity import DoAttempt from tenacity import DoSleep from tenacity import RetryCallState from tornado import gen class TornadoRetrying(BaseRetrying): def __init__(self, sleep=gen.sleep, **kwargs): super(TornadoRetrying, self).__init__(**kwargs) self.sleep = sleep @gen.coroutine def call(self, fn, *args, **kwargs): self.begin(fn) 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()) 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tenacity/wait.py0000644000655200065520000001470500000000000020071 0ustar00circlecicircleci00000000000000# 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 abc import random import six from tenacity import _utils from tenacity import compat as _compat @six.add_metaclass(abc.ABCMeta) class wait_base(object): """Abstract base class for wait strategies.""" @abc.abstractmethod def __call__(self, retry_state): pass def __add__(self, other): return wait_combine(self, other) def __radd__(self, other): # make it possible to use multiple waits with the built-in sum function if other == 0: return self return self.__add__(other) class wait_fixed(wait_base): """Wait strategy that waits a fixed amount of time between each retry.""" def __init__(self, wait): self.wait_fixed = wait @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): return self.wait_fixed class wait_none(wait_fixed): """Wait strategy that doesn't wait at all before retrying.""" def __init__(self): super(wait_none, self).__init__(0) class wait_random(wait_base): """Wait strategy that waits a random amount of time between min/max.""" def __init__(self, min=0, max=1): # noqa self.wait_random_min = min self.wait_random_max = max @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): 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): self.wait_funcs = tuple(_compat.wait_func_accept_retry_state(strategy) for strategy in strategies) @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): 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): self.strategies = [_compat.wait_func_accept_retry_state(strategy) for strategy in strategies] @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): 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_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=0, increment=100, max=_utils.MAX_WAIT): # noqa self.start = start self.increment = increment self.max = max @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): 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=1, max=_utils.MAX_WAIT, exp_base=2, min=0): # noqa self.multiplier = multiplier self.min = min self.max = max self.exp_base = exp_base @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): 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. """ @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): high = super(wait_random_exponential, self).__call__( retry_state=retry_state) return random.uniform(0, high) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1573042360.5643148 tenacity-6.0.0/tenacity.egg-info/0000755000655200065520000000000000000000000020236 5ustar00circlecicircleci00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042360.0 tenacity-6.0.0/tenacity.egg-info/PKG-INFO0000644000655200065520000000160600000000000021336 0ustar00circlecicircleci00000000000000Metadata-Version: 2.1 Name: tenacity Version: 6.0.0 Summary: Retry code until it succeeds Home-page: https://github.com/jd/tenacity Author: Julien Danjou Author-email: julien@danjou.info License: Apache 2.0 Description: Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Utilities Provides-Extra: doc ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042360.0 tenacity-6.0.0/tenacity.egg-info/SOURCES.txt0000644000655200065520000000137400000000000022127 0ustar00circlecicircleci00000000000000.gitignore .mergify.yml .readthedocs.yml LICENSE README.rst reno.yaml setup.cfg setup.py tox.ini .circleci/config.yml doc/source/api.rst doc/source/changelog.rst doc/source/conf.py doc/source/index.rst releasenotes/notes/add-reno-d1ab5710f272650a.yaml tenacity/__init__.py tenacity/_asyncio.py tenacity/_utils.py tenacity/after.py tenacity/before.py tenacity/before_sleep.py tenacity/compat.py tenacity/nap.py tenacity/retry.py tenacity/stop.py tenacity/tornadoweb.py tenacity/wait.py tenacity.egg-info/PKG-INFO tenacity.egg-info/SOURCES.txt tenacity.egg-info/dependency_links.txt tenacity.egg-info/requires.txt tenacity.egg-info/top_level.txt tenacity/tests/__init__.py tenacity/tests/test_asyncio.py tenacity/tests/test_tenacity.py tenacity/tests/test_tornado.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042360.0 tenacity-6.0.0/tenacity.egg-info/dependency_links.txt0000644000655200065520000000000100000000000024304 0ustar00circlecicircleci00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042360.0 tenacity-6.0.0/tenacity.egg-info/requires.txt0000644000655200065520000000014300000000000022634 0ustar00circlecicircleci00000000000000six>=1.9.0 [:python_version == "2.7"] futures>=3.0 monotonic>=0.6 [doc] reno sphinx tornado>=4.5 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042360.0 tenacity-6.0.0/tenacity.egg-info/top_level.txt0000644000655200065520000000001100000000000022760 0ustar00circlecicircleci00000000000000tenacity ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573042352.0 tenacity-6.0.0/tox.ini0000644000655200065520000000146700000000000016247 0ustar00circlecicircleci00000000000000[tox] envlist = py27, py35, py36, py37, pep8, pypy [testenv] usedevelop = True sitepackages = False deps = .[doc] pytest commands = py{27,py}: pytest --ignore='tenacity/tests/test_asyncio.py' {posargs} py3{5,6,7}: pytest {posargs} py3{5,6,7}: sphinx-build -a -E -W -b doctest doc/source doc/build py3{5,6,7}: sphinx-build -a -E -W -b html doc/source doc/build [testenv:pep8] basepython = python3 deps = flake8 flake8-import-order flake8-blind-except flake8-builtins flake8-docstrings flake8-rst-docstrings flake8-logging-format commands = flake8 [testenv:reno] basepython = python3 deps = reno commands = reno {posargs} [flake8] exclude = .tox,.eggs show-source = true ignore = D100,D101,D102,D103,D104,D105,D107,G200,G201,W503,W504 enable-extensions=G