├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.rst ├── requirements.txt ├── retrying.py ├── setup.py └── test_retrying.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist 3 | *.pyc 4 | *.egg-info 5 | build 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.6 5 | - 2.7 6 | - 3.2 7 | - 3.3 8 | - 3.4 9 | - 3.5 10 | - pypy 11 | 12 | script: python setup.py test 13 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Retrying is written and maintained by Ray Holder and 2 | various contributors: 3 | 4 | Development Lead 5 | ```````````````` 6 | 7 | - Ray Holder 8 | 9 | 10 | Patches and Suggestions 11 | ``````````````````````` 12 | 13 | - Anthony McClosky 14 | - Jason Dunkelberger 15 | - Justin Turner Arthur 16 | - J Derek Wilson 17 | - Alex Kuang 18 | - Simon Dollé 19 | - Rees Dooley 20 | - Saul Shanabrook 21 | - Daniel Nephin 22 | - Simeon Visser 23 | - Joshua Harlow 24 | - Pierre-Yves Chibon 25 | - Haïkel Guémar 26 | - Thomas Goirand 27 | - James Page 28 | - Josh Marshall 29 | - Dougal Matthews 30 | - Monty Taylor 31 | - Maxym Shalenyi 32 | - Jonathan Herriott 33 | - Job Evers 34 | - Cyrus Durgin 35 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 1.3.3 (2014-12-14) 6 | ++++++++++++++++++ 7 | - Add minimum six version of 1.7.0 since anything less will break things 8 | 9 | 1.3.2 (2014-11-09) 10 | ++++++++++++++++++ 11 | - Ensure we wrap the decorated functions to prevent information loss 12 | - Allow a jitter value to be passed in 13 | 14 | 1.3.1 (2014-09-30) 15 | ++++++++++++++++++ 16 | - Add requirements.txt to MANIFEST.in to fix pip installs 17 | 18 | 1.3.0 (2014-09-30) 19 | ++++++++++++++++++ 20 | - Add upstream six dependency, remove embedded six functionality 21 | 22 | 1.2.3 (2014-08-25) 23 | ++++++++++++++++++ 24 | - Add support for custom wait and stop functions 25 | 26 | 1.2.2 (2014-06-20) 27 | ++++++++++++++++++ 28 | - Bug fix to not raise a RetryError on failure when exceptions aren't being wrapped 29 | 30 | 1.2.1 (2014-05-05) 31 | ++++++++++++++++++ 32 | - Bug fix for explicitly passing in a wait type 33 | 34 | 1.2.0 (2014-05-04) 35 | ++++++++++++++++++ 36 | - Remove the need for explicit specification of stop/wait types when they can be inferred 37 | - Add a little checking for exception propagation 38 | 39 | 1.1.0 (2014-03-31) 40 | ++++++++++++++++++ 41 | - Added proper exception propagation through reraising with Python 2.6, 2.7, and 3.2 compatibility 42 | - Update test suite for behavior changes 43 | 44 | 1.0.1 (2013-03-20) 45 | ++++++++++++++++++ 46 | - Fixed a bug where classes not extending from the Python exception hierarchy could slip through 47 | - Update test suite for custom Python exceptions 48 | 49 | 1.0.0 (2013-01-21) 50 | ++++++++++++++++++ 51 | - First stable, tested version now exists 52 | - Apache 2.0 license applied 53 | - Sanitizing some setup.py and test suite running 54 | - Added Travis CI support 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst HISTORY.rst AUTHORS.rst LICENSE NOTICE requirements.txt 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Ray Holder 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Retrying 2 | ========================= 3 | .. image:: https://img.shields.io/pypi/v/retrying.svg 4 | :target: https://pypi.python.org/pypi/retrying 5 | 6 | .. image:: https://img.shields.io/travis/rholder/retrying.svg 7 | :target: https://travis-ci.org/rholder/retrying 8 | 9 | .. image:: https://img.shields.io/pypi/dm/retrying.svg 10 | :target: https://pypi.python.org/pypi/retrying 11 | 12 | Retrying is an Apache 2.0 licensed general-purpose retrying library, written in 13 | Python, to simplify the task of adding retry behavior to just about anything. 14 | 15 | 16 | The simplest use case is retrying a flaky function whenever an Exception occurs 17 | until a value is returned. 18 | 19 | .. code-block:: python 20 | 21 | import random 22 | from retrying import retry 23 | 24 | @retry 25 | def do_something_unreliable(): 26 | if random.randint(0, 10) > 1: 27 | raise IOError("Broken sauce, everything is hosed!!!111one") 28 | else: 29 | return "Awesome sauce!" 30 | 31 | print do_something_unreliable() 32 | 33 | 34 | Features 35 | -------- 36 | 37 | - Generic Decorator API 38 | - Specify stop condition (i.e. limit by number of attempts) 39 | - Specify wait condition (i.e. exponential backoff sleeping between attempts) 40 | - Customize retrying on Exceptions 41 | - Customize retrying on expected returned result 42 | 43 | 44 | Installation 45 | ------------ 46 | 47 | To install retrying, simply: 48 | 49 | .. code-block:: bash 50 | 51 | $ pip install retrying 52 | 53 | Or, if you absolutely must: 54 | 55 | .. code-block:: bash 56 | 57 | $ easy_install retrying 58 | 59 | But, you might regret that later. 60 | 61 | 62 | Examples 63 | ---------- 64 | 65 | As you saw above, the default behavior is to retry forever without waiting. 66 | 67 | .. code-block:: python 68 | 69 | @retry 70 | def never_give_up_never_surrender(): 71 | print "Retry forever ignoring Exceptions, don't wait between retries" 72 | 73 | 74 | Let's be a little less persistent and set some boundaries, such as the number of attempts before giving up. 75 | 76 | .. code-block:: python 77 | 78 | @retry(stop_max_attempt_number=7) 79 | def stop_after_7_attempts(): 80 | print "Stopping after 7 attempts" 81 | 82 | We don't have all day, so let's set a boundary for how long we should be retrying stuff. 83 | 84 | .. code-block:: python 85 | 86 | @retry(stop_max_delay=10000) 87 | def stop_after_10_s(): 88 | print "Stopping after 10 seconds" 89 | 90 | Most things don't like to be polled as fast as possible, so let's just wait 2 seconds between retries. 91 | 92 | .. code-block:: python 93 | 94 | @retry(wait_fixed=2000) 95 | def wait_2_s(): 96 | print "Wait 2 second between retries" 97 | 98 | 99 | Some things perform best with a bit of randomness injected. 100 | 101 | .. code-block:: python 102 | 103 | @retry(wait_random_min=1000, wait_random_max=2000) 104 | def wait_random_1_to_2_s(): 105 | print "Randomly wait 1 to 2 seconds between retries" 106 | 107 | Then again, it's hard to beat exponential backoff when retrying distributed services and other remote endpoints. 108 | 109 | .. code-block:: python 110 | 111 | @retry(wait_exponential_multiplier=1000, wait_exponential_max=10000) 112 | def wait_exponential_1000(): 113 | print "Wait 2^x * 1000 milliseconds between each retry, up to 10 seconds, then 10 seconds afterwards" 114 | 115 | 116 | We have a few options for dealing with retries that raise specific or general exceptions, as in the cases here. 117 | 118 | .. code-block:: python 119 | 120 | def retry_if_io_error(exception): 121 | """Return True if we should retry (in this case when it's an IOError), False otherwise""" 122 | return isinstance(exception, IOError) 123 | 124 | @retry(retry_on_exception=retry_if_io_error) 125 | def might_io_error(): 126 | print "Retry forever with no wait if an IOError occurs, raise any other errors" 127 | 128 | @retry(retry_on_exception=retry_if_io_error, wrap_exception=True) 129 | def only_raise_retry_error_when_not_io_error(): 130 | print "Retry forever with no wait if an IOError occurs, raise any other errors wrapped in RetryError" 131 | 132 | We can also use the result of the function to alter the behavior of retrying. 133 | 134 | .. code-block:: python 135 | 136 | def retry_if_result_none(result): 137 | """Return True if we should retry (in this case when result is None), False otherwise""" 138 | return result is None 139 | 140 | @retry(retry_on_result=retry_if_result_none) 141 | def might_return_none(): 142 | print "Retry forever ignoring Exceptions with no wait if return value is None" 143 | 144 | 145 | Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. 146 | 147 | Contribute 148 | ---------- 149 | 150 | #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. 151 | #. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it). 152 | #. Write a test which shows that the bug was fixed or that the feature works as expected. 153 | #. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS_. 154 | 155 | .. _`the repository`: http://github.com/rholder/retrying 156 | .. _AUTHORS: https://github.com/rholder/retrying/blob/master/AUTHORS.rst 157 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six>=1.7.0 2 | -------------------------------------------------------------------------------- /retrying.py: -------------------------------------------------------------------------------- 1 | ## Copyright 2013-2014 Ray Holder 2 | ## 3 | ## Licensed under the Apache License, Version 2.0 (the "License"); 4 | ## you may not use this file except in compliance with the License. 5 | ## You may obtain a copy of the License at 6 | ## 7 | ## http://www.apache.org/licenses/LICENSE-2.0 8 | ## 9 | ## Unless required by applicable law or agreed to in writing, software 10 | ## distributed under the License is distributed on an "AS IS" BASIS, 11 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ## See the License for the specific language governing permissions and 13 | ## limitations under the License. 14 | 15 | import random 16 | import six 17 | import sys 18 | import time 19 | import traceback 20 | 21 | 22 | # sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint... 23 | MAX_WAIT = 1073741823 24 | 25 | 26 | def _retry_if_exception_of_type(retryable_types): 27 | def _retry_if_exception_these_types(exception): 28 | return isinstance(exception, retryable_types) 29 | return _retry_if_exception_these_types 30 | 31 | 32 | def retry(*dargs, **dkw): 33 | """ 34 | Decorator function that instantiates the Retrying object 35 | @param *dargs: positional arguments passed to Retrying object 36 | @param **dkw: keyword arguments passed to the Retrying object 37 | """ 38 | # support both @retry and @retry() as valid syntax 39 | if len(dargs) == 1 and callable(dargs[0]): 40 | def wrap_simple(f): 41 | 42 | @six.wraps(f) 43 | def wrapped_f(*args, **kw): 44 | return Retrying().call(f, *args, **kw) 45 | 46 | return wrapped_f 47 | 48 | return wrap_simple(dargs[0]) 49 | 50 | else: 51 | def wrap(f): 52 | 53 | @six.wraps(f) 54 | def wrapped_f(*args, **kw): 55 | return Retrying(*dargs, **dkw).call(f, *args, **kw) 56 | 57 | return wrapped_f 58 | 59 | return wrap 60 | 61 | 62 | class Retrying(object): 63 | 64 | def __init__(self, 65 | stop=None, wait=None, 66 | stop_max_attempt_number=None, 67 | stop_max_delay=None, 68 | wait_fixed=None, 69 | wait_random_min=None, wait_random_max=None, 70 | wait_incrementing_start=None, wait_incrementing_increment=None, 71 | wait_incrementing_max=None, 72 | wait_exponential_multiplier=None, wait_exponential_max=None, 73 | retry_on_exception=None, 74 | retry_on_result=None, 75 | wrap_exception=False, 76 | stop_func=None, 77 | wait_func=None, 78 | wait_jitter_max=None, 79 | before_attempts=None, 80 | after_attempts=None): 81 | 82 | self._stop_max_attempt_number = 5 if stop_max_attempt_number is None else stop_max_attempt_number 83 | self._stop_max_delay = 100 if stop_max_delay is None else stop_max_delay 84 | self._wait_fixed = 1000 if wait_fixed is None else wait_fixed 85 | self._wait_random_min = 0 if wait_random_min is None else wait_random_min 86 | self._wait_random_max = 1000 if wait_random_max is None else wait_random_max 87 | self._wait_incrementing_start = 0 if wait_incrementing_start is None else wait_incrementing_start 88 | self._wait_incrementing_increment = 100 if wait_incrementing_increment is None else wait_incrementing_increment 89 | self._wait_exponential_multiplier = 1 if wait_exponential_multiplier is None else wait_exponential_multiplier 90 | self._wait_exponential_max = MAX_WAIT if wait_exponential_max is None else wait_exponential_max 91 | self._wait_incrementing_max = MAX_WAIT if wait_incrementing_max is None else wait_incrementing_max 92 | self._wait_jitter_max = 0 if wait_jitter_max is None else wait_jitter_max 93 | self._before_attempts = before_attempts 94 | self._after_attempts = after_attempts 95 | 96 | # TODO add chaining of stop behaviors 97 | # stop behavior 98 | stop_funcs = [] 99 | if stop_max_attempt_number is not None: 100 | stop_funcs.append(self.stop_after_attempt) 101 | 102 | if stop_max_delay is not None: 103 | stop_funcs.append(self.stop_after_delay) 104 | 105 | if stop_func is not None: 106 | self.stop = stop_func 107 | 108 | elif stop is None: 109 | self.stop = lambda attempts, delay: any(f(attempts, delay) for f in stop_funcs) 110 | 111 | else: 112 | self.stop = getattr(self, stop) 113 | 114 | # TODO add chaining of wait behaviors 115 | # wait behavior 116 | wait_funcs = [lambda *args, **kwargs: 0] 117 | if wait_fixed is not None: 118 | wait_funcs.append(self.fixed_sleep) 119 | 120 | if wait_random_min is not None or wait_random_max is not None: 121 | wait_funcs.append(self.random_sleep) 122 | 123 | if wait_incrementing_start is not None or wait_incrementing_increment is not None: 124 | wait_funcs.append(self.incrementing_sleep) 125 | 126 | if wait_exponential_multiplier is not None or wait_exponential_max is not None: 127 | wait_funcs.append(self.exponential_sleep) 128 | 129 | if wait_func is not None: 130 | self.wait = wait_func 131 | 132 | elif wait is None: 133 | self.wait = lambda attempts, delay: max(f(attempts, delay) for f in wait_funcs) 134 | 135 | else: 136 | self.wait = getattr(self, wait) 137 | 138 | # retry on exception filter 139 | if retry_on_exception is None: 140 | self._retry_on_exception = self.always_reject 141 | else: 142 | # this allows for providing a tuple of exception types that 143 | # should be allowed to retry on, and avoids having to create 144 | # a callback that does the same thing 145 | if isinstance(retry_on_exception, (tuple)): 146 | retry_on_exception = _retry_if_exception_of_type( 147 | retry_on_exception) 148 | self._retry_on_exception = retry_on_exception 149 | 150 | # retry on result filter 151 | if retry_on_result is None: 152 | self._retry_on_result = self.never_reject 153 | else: 154 | self._retry_on_result = retry_on_result 155 | 156 | self._wrap_exception = wrap_exception 157 | 158 | def stop_after_attempt(self, previous_attempt_number, delay_since_first_attempt_ms): 159 | """Stop after the previous attempt >= stop_max_attempt_number.""" 160 | return previous_attempt_number >= self._stop_max_attempt_number 161 | 162 | def stop_after_delay(self, previous_attempt_number, delay_since_first_attempt_ms): 163 | """Stop after the time from the first attempt >= stop_max_delay.""" 164 | return delay_since_first_attempt_ms >= self._stop_max_delay 165 | 166 | @staticmethod 167 | def no_sleep(previous_attempt_number, delay_since_first_attempt_ms): 168 | """Don't sleep at all before retrying.""" 169 | return 0 170 | 171 | def fixed_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): 172 | """Sleep a fixed amount of time between each retry.""" 173 | return self._wait_fixed 174 | 175 | def random_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): 176 | """Sleep a random amount of time between wait_random_min and wait_random_max""" 177 | return random.randint(self._wait_random_min, self._wait_random_max) 178 | 179 | def incrementing_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): 180 | """ 181 | Sleep an incremental amount of time after each attempt, starting at 182 | wait_incrementing_start and incrementing by wait_incrementing_increment 183 | """ 184 | result = self._wait_incrementing_start + (self._wait_incrementing_increment * (previous_attempt_number - 1)) 185 | if result > self._wait_incrementing_max: 186 | result = self._wait_incrementing_max 187 | if result < 0: 188 | result = 0 189 | return result 190 | 191 | def exponential_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): 192 | exp = 2 ** previous_attempt_number 193 | result = self._wait_exponential_multiplier * exp 194 | if result > self._wait_exponential_max: 195 | result = self._wait_exponential_max 196 | if result < 0: 197 | result = 0 198 | return result 199 | 200 | @staticmethod 201 | def never_reject(result): 202 | return False 203 | 204 | @staticmethod 205 | def always_reject(result): 206 | return True 207 | 208 | def should_reject(self, attempt): 209 | reject = False 210 | if attempt.has_exception: 211 | reject |= self._retry_on_exception(attempt.value[1]) 212 | else: 213 | reject |= self._retry_on_result(attempt.value) 214 | 215 | return reject 216 | 217 | def call(self, fn, *args, **kwargs): 218 | start_time = int(round(time.time() * 1000)) 219 | attempt_number = 1 220 | while True: 221 | if self._before_attempts: 222 | self._before_attempts(attempt_number) 223 | 224 | try: 225 | attempt = Attempt(fn(*args, **kwargs), attempt_number, False) 226 | except: 227 | tb = sys.exc_info() 228 | attempt = Attempt(tb, attempt_number, True) 229 | 230 | if not self.should_reject(attempt): 231 | return attempt.get(self._wrap_exception) 232 | 233 | if self._after_attempts: 234 | self._after_attempts(attempt_number) 235 | 236 | delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time 237 | if self.stop(attempt_number, delay_since_first_attempt_ms): 238 | if not self._wrap_exception and attempt.has_exception: 239 | # get() on an attempt with an exception should cause it to be raised, but raise just in case 240 | raise attempt.get() 241 | else: 242 | raise RetryError(attempt) 243 | else: 244 | sleep = self.wait(attempt_number, delay_since_first_attempt_ms) 245 | if self._wait_jitter_max: 246 | jitter = random.random() * self._wait_jitter_max 247 | sleep = sleep + max(0, jitter) 248 | time.sleep(sleep / 1000.0) 249 | 250 | attempt_number += 1 251 | 252 | 253 | class Attempt(object): 254 | """ 255 | An Attempt encapsulates a call to a target function that may end as a 256 | normal return value from the function or an Exception depending on what 257 | occurred during the execution. 258 | """ 259 | 260 | def __init__(self, value, attempt_number, has_exception): 261 | self.value = value 262 | self.attempt_number = attempt_number 263 | self.has_exception = has_exception 264 | 265 | def get(self, wrap_exception=False): 266 | """ 267 | Return the return value of this Attempt instance or raise an Exception. 268 | If wrap_exception is true, this Attempt is wrapped inside of a 269 | RetryError before being raised. 270 | """ 271 | if self.has_exception: 272 | if wrap_exception: 273 | raise RetryError(self) 274 | else: 275 | six.reraise(self.value[0], self.value[1], self.value[2]) 276 | else: 277 | return self.value 278 | 279 | def __repr__(self): 280 | if self.has_exception: 281 | return "Attempts: {0}, Error:\n{1}".format(self.attempt_number, "".join(traceback.format_tb(self.value[2]))) 282 | else: 283 | return "Attempts: {0}, Value: {1}".format(self.attempt_number, self.value) 284 | 285 | 286 | class RetryError(Exception): 287 | """ 288 | A RetryError encapsulates the last Attempt instance right before giving up. 289 | """ 290 | 291 | def __init__(self, last_attempt): 292 | self.last_attempt = last_attempt 293 | 294 | def __str__(self): 295 | return "RetryError[{0}]".format(self.last_attempt) 296 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from distutils.core import setup 10 | 11 | settings = dict() 12 | 13 | # Publish Helper. 14 | if sys.argv[-1] == 'publish': 15 | os.system('python setup.py sdist upload') 16 | sys.exit() 17 | 18 | CLASSIFIERS = [ 19 | 'Intended Audience :: Developers', 20 | 'Natural Language :: English', 21 | 'License :: OSI Approved :: Apache Software License', 22 | 'Programming Language :: Python', 23 | 'Programming Language :: Python :: 2.6', 24 | 'Programming Language :: Python :: 2.7', 25 | 'Programming Language :: Python :: 3.2', 26 | 'Programming Language :: Python :: 3.3', 27 | 'Programming Language :: Python :: 3.4', 28 | 'Programming Language :: Python :: 3.5', 29 | 'Topic :: Internet', 30 | 'Topic :: Utilities', 31 | ] 32 | 33 | with open('README.rst') as file_readme: 34 | readme = file_readme.read() 35 | 36 | with open('HISTORY.rst') as file_history: 37 | history = file_history.read() 38 | 39 | with open('requirements.txt') as file_requirements: 40 | requirements = file_requirements.read().splitlines() 41 | 42 | settings.update( 43 | name='retrying', 44 | version='1.3.4-dev', 45 | description='Retrying', 46 | long_description=readme + '\n\n' + history, 47 | author='Ray Holder', 48 | license='Apache 2.0', 49 | url='https://github.com/rholder/retrying', 50 | classifiers=CLASSIFIERS, 51 | keywords="decorator decorators retry retrying exception exponential backoff", 52 | py_modules= ['retrying'], 53 | test_suite="test_retrying", 54 | install_requires=requirements, 55 | ) 56 | 57 | 58 | setup(**settings) 59 | -------------------------------------------------------------------------------- /test_retrying.py: -------------------------------------------------------------------------------- 1 | ## Copyright 2013 Ray Holder 2 | ## 3 | ## Licensed under the Apache License, Version 2.0 (the "License"); 4 | ## you may not use this file except in compliance with the License. 5 | ## You may obtain a copy of the License at 6 | ## 7 | ## http://www.apache.org/licenses/LICENSE-2.0 8 | ## 9 | ## Unless required by applicable law or agreed to in writing, software 10 | ## distributed under the License is distributed on an "AS IS" BASIS, 11 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | ## See the License for the specific language governing permissions and 13 | ## limitations under the License. 14 | 15 | import time 16 | import unittest 17 | 18 | from retrying import RetryError 19 | from retrying import Retrying 20 | from retrying import retry 21 | 22 | 23 | class TestStopConditions(unittest.TestCase): 24 | 25 | def test_never_stop(self): 26 | r = Retrying() 27 | self.assertFalse(r.stop(3, 6546)) 28 | 29 | def test_stop_after_attempt(self): 30 | r = Retrying(stop_max_attempt_number=3) 31 | self.assertFalse(r.stop(2, 6546)) 32 | self.assertTrue(r.stop(3, 6546)) 33 | self.assertTrue(r.stop(4, 6546)) 34 | 35 | def test_stop_after_delay(self): 36 | r = Retrying(stop_max_delay=1000) 37 | self.assertFalse(r.stop(2, 999)) 38 | self.assertTrue(r.stop(2, 1000)) 39 | self.assertTrue(r.stop(2, 1001)) 40 | 41 | def test_legacy_explicit_stop_type(self): 42 | Retrying(stop="stop_after_attempt") 43 | 44 | def test_stop_func(self): 45 | r = Retrying(stop_func=lambda attempt, delay: attempt == delay) 46 | self.assertFalse(r.stop(1, 3)) 47 | self.assertFalse(r.stop(100, 99)) 48 | self.assertTrue(r.stop(101, 101)) 49 | 50 | 51 | class TestWaitConditions(unittest.TestCase): 52 | 53 | def test_no_sleep(self): 54 | r = Retrying() 55 | self.assertEqual(0, r.wait(18, 9879)) 56 | 57 | def test_fixed_sleep(self): 58 | r = Retrying(wait_fixed=1000) 59 | self.assertEqual(1000, r.wait(12, 6546)) 60 | 61 | def test_incrementing_sleep(self): 62 | r = Retrying(wait_incrementing_start=500, wait_incrementing_increment=100) 63 | self.assertEqual(500, r.wait(1, 6546)) 64 | self.assertEqual(600, r.wait(2, 6546)) 65 | self.assertEqual(700, r.wait(3, 6546)) 66 | 67 | def test_random_sleep(self): 68 | r = Retrying(wait_random_min=1000, wait_random_max=2000) 69 | times = set() 70 | times.add(r.wait(1, 6546)) 71 | times.add(r.wait(1, 6546)) 72 | times.add(r.wait(1, 6546)) 73 | times.add(r.wait(1, 6546)) 74 | 75 | # this is kind of non-deterministic... 76 | self.assertTrue(len(times) > 1) 77 | for t in times: 78 | self.assertTrue(t >= 1000) 79 | self.assertTrue(t <= 2000) 80 | 81 | def test_random_sleep_without_min(self): 82 | r = Retrying(wait_random_max=2000) 83 | times = set() 84 | times.add(r.wait(1, 6546)) 85 | times.add(r.wait(1, 6546)) 86 | times.add(r.wait(1, 6546)) 87 | times.add(r.wait(1, 6546)) 88 | 89 | # this is kind of non-deterministic... 90 | self.assertTrue(len(times) > 1) 91 | for t in times: 92 | self.assertTrue(t >= 0) 93 | self.assertTrue(t <= 2000) 94 | 95 | def test_exponential(self): 96 | r = Retrying(wait_exponential_max=100000) 97 | self.assertEqual(r.wait(1, 0), 2) 98 | self.assertEqual(r.wait(2, 0), 4) 99 | self.assertEqual(r.wait(3, 0), 8) 100 | self.assertEqual(r.wait(4, 0), 16) 101 | self.assertEqual(r.wait(5, 0), 32) 102 | self.assertEqual(r.wait(6, 0), 64) 103 | 104 | def test_exponential_with_max_wait(self): 105 | r = Retrying(wait_exponential_max=40) 106 | self.assertEqual(r.wait(1, 0), 2) 107 | self.assertEqual(r.wait(2, 0), 4) 108 | self.assertEqual(r.wait(3, 0), 8) 109 | self.assertEqual(r.wait(4, 0), 16) 110 | self.assertEqual(r.wait(5, 0), 32) 111 | self.assertEqual(r.wait(6, 0), 40) 112 | self.assertEqual(r.wait(7, 0), 40) 113 | self.assertEqual(r.wait(50, 0), 40) 114 | 115 | def test_exponential_with_max_wait_and_multiplier(self): 116 | r = Retrying(wait_exponential_max=50000, wait_exponential_multiplier=1000) 117 | self.assertEqual(r.wait(1, 0), 2000) 118 | self.assertEqual(r.wait(2, 0), 4000) 119 | self.assertEqual(r.wait(3, 0), 8000) 120 | self.assertEqual(r.wait(4, 0), 16000) 121 | self.assertEqual(r.wait(5, 0), 32000) 122 | self.assertEqual(r.wait(6, 0), 50000) 123 | self.assertEqual(r.wait(7, 0), 50000) 124 | self.assertEqual(r.wait(50, 0), 50000) 125 | 126 | def test_legacy_explicit_wait_type(self): 127 | Retrying(wait="exponential_sleep") 128 | 129 | def test_wait_func(self): 130 | r = Retrying(wait_func=lambda attempt, delay: attempt * delay) 131 | self.assertEqual(r.wait(1, 5), 5) 132 | self.assertEqual(r.wait(2, 11), 22) 133 | self.assertEqual(r.wait(10, 100), 1000) 134 | 135 | 136 | class NoneReturnUntilAfterCount: 137 | """ 138 | This class holds counter state for invoking a method several times in a row. 139 | """ 140 | 141 | def __init__(self, count): 142 | self.counter = 0 143 | self.count = count 144 | 145 | def go(self): 146 | """ 147 | Return None until after count threshold has been crossed, then return True. 148 | """ 149 | if self.counter < self.count: 150 | self.counter += 1 151 | return None 152 | return True 153 | 154 | 155 | class NoIOErrorAfterCount: 156 | """ 157 | This class holds counter state for invoking a method several times in a row. 158 | """ 159 | 160 | def __init__(self, count): 161 | self.counter = 0 162 | self.count = count 163 | 164 | def go(self): 165 | """ 166 | Raise an IOError until after count threshold has been crossed, then return True. 167 | """ 168 | if self.counter < self.count: 169 | self.counter += 1 170 | raise IOError("Hi there, I'm an IOError") 171 | return True 172 | 173 | 174 | class NoNameErrorAfterCount: 175 | """ 176 | This class holds counter state for invoking a method several times in a row. 177 | """ 178 | 179 | def __init__(self, count): 180 | self.counter = 0 181 | self.count = count 182 | 183 | def go(self): 184 | """ 185 | Raise a NameError until after count threshold has been crossed, then return True. 186 | """ 187 | if self.counter < self.count: 188 | self.counter += 1 189 | raise NameError("Hi there, I'm a NameError") 190 | return True 191 | 192 | 193 | class CustomError(Exception): 194 | """ 195 | This is a custom exception class. Note that For Python 2.x, we don't 196 | strictly need to extend BaseException, however, Python 3.x will complain. 197 | While this test suite won't run correctly under Python 3.x without 198 | extending from the Python exception hierarchy, the actual module code is 199 | backwards compatible Python 2.x and will allow for cases where exception 200 | classes don't extend from the hierarchy. 201 | """ 202 | 203 | def __init__(self, value): 204 | self.value = value 205 | 206 | def __str__(self): 207 | return repr(self.value) 208 | 209 | 210 | class NoCustomErrorAfterCount: 211 | """ 212 | This class holds counter state for invoking a method several times in a row. 213 | """ 214 | 215 | def __init__(self, count): 216 | self.counter = 0 217 | self.count = count 218 | 219 | def go(self): 220 | """ 221 | Raise a CustomError until after count threshold has been crossed, then return True. 222 | """ 223 | if self.counter < self.count: 224 | self.counter += 1 225 | derived_message = "This is a Custom exception class" 226 | raise CustomError(derived_message) 227 | return True 228 | 229 | 230 | def retry_if_result_none(result): 231 | return result is None 232 | 233 | 234 | def retry_if_exception_of_type(retryable_types): 235 | def retry_if_exception_these_types(exception): 236 | print("Detected Exception of type: {0}".format(str(type(exception)))) 237 | return isinstance(exception, retryable_types) 238 | return retry_if_exception_these_types 239 | 240 | 241 | def current_time_ms(): 242 | return int(round(time.time() * 1000)) 243 | 244 | 245 | @retry(wait_fixed=50, retry_on_result=retry_if_result_none) 246 | def _retryable_test_with_wait(thing): 247 | return thing.go() 248 | 249 | 250 | @retry(stop_max_attempt_number=3, retry_on_result=retry_if_result_none) 251 | def _retryable_test_with_stop(thing): 252 | return thing.go() 253 | 254 | 255 | @retry(retry_on_exception=(IOError,)) 256 | def _retryable_test_with_exception_type_io(thing): 257 | return thing.go() 258 | 259 | 260 | @retry(retry_on_exception=retry_if_exception_of_type(IOError), wrap_exception=True) 261 | def _retryable_test_with_exception_type_io_wrap(thing): 262 | return thing.go() 263 | 264 | 265 | @retry( 266 | stop_max_attempt_number=3, 267 | retry_on_exception=(IOError,)) 268 | def _retryable_test_with_exception_type_io_attempt_limit(thing): 269 | return thing.go() 270 | 271 | 272 | @retry( 273 | stop_max_attempt_number=3, 274 | retry_on_exception=(IOError,), 275 | wrap_exception=True) 276 | def _retryable_test_with_exception_type_io_attempt_limit_wrap(thing): 277 | return thing.go() 278 | 279 | 280 | @retry 281 | def _retryable_default(thing): 282 | return thing.go() 283 | 284 | 285 | @retry() 286 | def _retryable_default_f(thing): 287 | return thing.go() 288 | 289 | 290 | @retry(retry_on_exception=retry_if_exception_of_type(CustomError)) 291 | def _retryable_test_with_exception_type_custom(thing): 292 | return thing.go() 293 | 294 | 295 | @retry(retry_on_exception=retry_if_exception_of_type(CustomError), wrap_exception=True) 296 | def _retryable_test_with_exception_type_custom_wrap(thing): 297 | return thing.go() 298 | 299 | 300 | @retry( 301 | stop_max_attempt_number=3, 302 | retry_on_exception=retry_if_exception_of_type(CustomError)) 303 | def _retryable_test_with_exception_type_custom_attempt_limit(thing): 304 | return thing.go() 305 | 306 | 307 | @retry( 308 | stop_max_attempt_number=3, 309 | retry_on_exception=retry_if_exception_of_type(CustomError), 310 | wrap_exception=True) 311 | def _retryable_test_with_exception_type_custom_attempt_limit_wrap(thing): 312 | return thing.go() 313 | 314 | 315 | class TestDecoratorWrapper(unittest.TestCase): 316 | 317 | def test_with_wait(self): 318 | start = current_time_ms() 319 | result = _retryable_test_with_wait(NoneReturnUntilAfterCount(5)) 320 | t = current_time_ms() - start 321 | self.assertTrue(t >= 250) 322 | self.assertTrue(result) 323 | 324 | def test_with_stop_on_return_value(self): 325 | try: 326 | _retryable_test_with_stop(NoneReturnUntilAfterCount(5)) 327 | self.fail("Expected RetryError after 3 attempts") 328 | except RetryError as re: 329 | self.assertFalse(re.last_attempt.has_exception) 330 | self.assertEqual(3, re.last_attempt.attempt_number) 331 | self.assertTrue(re.last_attempt.value is None) 332 | print(re) 333 | 334 | def test_with_stop_on_exception(self): 335 | try: 336 | _retryable_test_with_stop(NoIOErrorAfterCount(5)) 337 | self.fail("Expected IOError") 338 | except IOError as re: 339 | self.assertTrue(isinstance(re, IOError)) 340 | print(re) 341 | 342 | def test_retry_if_exception_of_type(self): 343 | self.assertTrue(_retryable_test_with_exception_type_io(NoIOErrorAfterCount(5))) 344 | 345 | try: 346 | _retryable_test_with_exception_type_io(NoNameErrorAfterCount(5)) 347 | self.fail("Expected NameError") 348 | except NameError as n: 349 | self.assertTrue(isinstance(n, NameError)) 350 | print(n) 351 | 352 | try: 353 | _retryable_test_with_exception_type_io_attempt_limit_wrap(NoIOErrorAfterCount(5)) 354 | self.fail("Expected RetryError") 355 | except RetryError as re: 356 | self.assertEqual(3, re.last_attempt.attempt_number) 357 | self.assertTrue(re.last_attempt.has_exception) 358 | self.assertTrue(re.last_attempt.value[0] is not None) 359 | self.assertTrue(isinstance(re.last_attempt.value[1], IOError)) 360 | self.assertTrue(re.last_attempt.value[2] is not None) 361 | print(re) 362 | 363 | self.assertTrue(_retryable_test_with_exception_type_custom(NoCustomErrorAfterCount(5))) 364 | 365 | try: 366 | _retryable_test_with_exception_type_custom(NoNameErrorAfterCount(5)) 367 | self.fail("Expected NameError") 368 | except NameError as n: 369 | self.assertTrue(isinstance(n, NameError)) 370 | print(n) 371 | 372 | try: 373 | _retryable_test_with_exception_type_custom_attempt_limit_wrap(NoCustomErrorAfterCount(5)) 374 | self.fail("Expected RetryError") 375 | except RetryError as re: 376 | self.assertEqual(3, re.last_attempt.attempt_number) 377 | self.assertTrue(re.last_attempt.has_exception) 378 | self.assertTrue(re.last_attempt.value[0] is not None) 379 | self.assertTrue(isinstance(re.last_attempt.value[1], CustomError)) 380 | self.assertTrue(re.last_attempt.value[2] is not None) 381 | print(re) 382 | 383 | def test_wrapped_exception(self): 384 | 385 | # base exception cases 386 | self.assertTrue(_retryable_test_with_exception_type_io_wrap(NoIOErrorAfterCount(5))) 387 | 388 | try: 389 | _retryable_test_with_exception_type_io_wrap(NoNameErrorAfterCount(5)) 390 | self.fail("Expected RetryError") 391 | except RetryError as re: 392 | self.assertTrue(isinstance(re.last_attempt.value[1], NameError)) 393 | print(re) 394 | 395 | try: 396 | _retryable_test_with_exception_type_io_attempt_limit_wrap(NoIOErrorAfterCount(5)) 397 | self.fail("Expected RetryError") 398 | except RetryError as re: 399 | self.assertEqual(3, re.last_attempt.attempt_number) 400 | self.assertTrue(re.last_attempt.has_exception) 401 | self.assertTrue(re.last_attempt.value[0] is not None) 402 | self.assertTrue(isinstance(re.last_attempt.value[1], IOError)) 403 | self.assertTrue(re.last_attempt.value[2] is not None) 404 | print(re) 405 | 406 | # custom error cases 407 | self.assertTrue(_retryable_test_with_exception_type_custom_wrap(NoCustomErrorAfterCount(5))) 408 | 409 | try: 410 | _retryable_test_with_exception_type_custom_wrap(NoNameErrorAfterCount(5)) 411 | self.fail("Expected RetryError") 412 | except RetryError as re: 413 | self.assertTrue(re.last_attempt.value[0] is not None) 414 | self.assertTrue(isinstance(re.last_attempt.value[1], NameError)) 415 | self.assertTrue(re.last_attempt.value[2] is not None) 416 | print(re) 417 | 418 | try: 419 | _retryable_test_with_exception_type_custom_attempt_limit_wrap(NoCustomErrorAfterCount(5)) 420 | self.fail("Expected RetryError") 421 | except RetryError as re: 422 | self.assertEqual(3, re.last_attempt.attempt_number) 423 | self.assertTrue(re.last_attempt.has_exception) 424 | self.assertTrue(re.last_attempt.value[0] is not None) 425 | self.assertTrue(isinstance(re.last_attempt.value[1], CustomError)) 426 | self.assertTrue(re.last_attempt.value[2] is not None) 427 | 428 | self.assertTrue("This is a Custom exception class" in str(re.last_attempt.value[1])) 429 | print(re) 430 | 431 | def test_defaults(self): 432 | self.assertTrue(_retryable_default(NoNameErrorAfterCount(5))) 433 | self.assertTrue(_retryable_default_f(NoNameErrorAfterCount(5))) 434 | self.assertTrue(_retryable_default(NoCustomErrorAfterCount(5))) 435 | self.assertTrue(_retryable_default_f(NoCustomErrorAfterCount(5))) 436 | 437 | class TestBeforeAfterAttempts(unittest.TestCase): 438 | _attempt_number = 0 439 | 440 | def test_before_attempts(self): 441 | TestBeforeAfterAttempts._attempt_number = 0 442 | 443 | def _before(attempt_number): 444 | TestBeforeAfterAttempts._attempt_number = attempt_number 445 | 446 | @retry(wait_fixed = 1000, stop_max_attempt_number = 1, before_attempts = _before) 447 | def _test_before(): 448 | pass 449 | 450 | _test_before() 451 | 452 | self.assertTrue(TestBeforeAfterAttempts._attempt_number is 1) 453 | 454 | def test_after_attempts(self): 455 | TestBeforeAfterAttempts._attempt_number = 0 456 | 457 | def _after(attempt_number): 458 | TestBeforeAfterAttempts._attempt_number = attempt_number 459 | 460 | @retry(wait_fixed = 100, stop_max_attempt_number = 3, after_attempts = _after) 461 | def _test_after(): 462 | if TestBeforeAfterAttempts._attempt_number < 2: 463 | raise Exception("testing after_attempts handler") 464 | else: 465 | pass 466 | 467 | _test_after() 468 | 469 | self.assertTrue(TestBeforeAfterAttempts._attempt_number is 2) 470 | 471 | if __name__ == '__main__': 472 | unittest.main() 473 | --------------------------------------------------------------------------------