tenacity
Advanced tools
| --- | ||
| fixes: | ||
| - | | ||
| Restore the value of the `retry` attribute for wrapped functions. Also, | ||
| clarify that those attributes are write-only and statistics should be | ||
| read from the function attribute directly. |
@@ -37,3 +37,3 @@ name: Continuous Integration | ||
| - name: Checkout 🛎️ | ||
| uses: actions/checkout@v4.1.6 | ||
| uses: actions/checkout@v4.1.7 | ||
| with: | ||
@@ -40,0 +40,0 @@ fetch-depth: 0 |
@@ -14,3 +14,3 @@ name: Release deploy | ||
| - name: Checkout 🛎️ | ||
| uses: actions/checkout@v4.1.6 | ||
| uses: actions/checkout@v4.1.7 | ||
| with: | ||
@@ -17,0 +17,0 @@ fetch-depth: 0 |
+31
-5
@@ -127,4 +127,4 @@ Tenacity | ||
| If you're on a tight deadline, and exceeding your delay time isn't ok, | ||
| then you can give up on retries one attempt before you would exceed the delay. | ||
| If you're on a tight deadline, and exceeding your delay time isn't ok, | ||
| then you can give up on retries one attempt before you would exceed the delay. | ||
@@ -366,3 +366,3 @@ .. testcode:: | ||
| 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: | ||
| `statistics` attribute attached to the function: | ||
@@ -380,3 +380,3 @@ .. testcode:: | ||
| print(raise_my_exception.retry.statistics) | ||
| print(raise_my_exception.statistics) | ||
@@ -501,3 +501,3 @@ .. testoutput:: | ||
| print(raise_my_exception.retry.statistics) | ||
| print(raise_my_exception.statistics) | ||
@@ -521,2 +521,28 @@ .. testoutput:: | ||
| You may also want to change the behaviour of a decorated function temporarily, | ||
| like in tests to avoid unnecessary wait times. You can modify/patch the `retry` | ||
| attribute attached to the function. Bear in mind this is a write-only attribute, | ||
| statistics should be read from the function `statistics` attribute. | ||
| .. testcode:: | ||
| @retry(stop=stop_after_attempt(3), wait=wait_fixed(3)) | ||
| def raise_my_exception(): | ||
| raise MyException("Fail") | ||
| from unittest import mock | ||
| with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)): | ||
| try: | ||
| raise_my_exception() | ||
| except Exception: | ||
| pass | ||
| print(raise_my_exception.statistics) | ||
| .. testoutput:: | ||
| :hide: | ||
| ... | ||
| Retrying code block | ||
@@ -523,0 +549,0 @@ ~~~~~~~~~~~~~~~~~~~ |
+1
-1
| Metadata-Version: 2.1 | ||
| Name: tenacity | ||
| Version: 8.4.2 | ||
| Version: 8.5.0 | ||
| Summary: Retry code until it succeeds | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/jd/tenacity |
+31
-5
@@ -127,4 +127,4 @@ Tenacity | ||
| If you're on a tight deadline, and exceeding your delay time isn't ok, | ||
| then you can give up on retries one attempt before you would exceed the delay. | ||
| If you're on a tight deadline, and exceeding your delay time isn't ok, | ||
| then you can give up on retries one attempt before you would exceed the delay. | ||
@@ -366,3 +366,3 @@ .. testcode:: | ||
| 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: | ||
| `statistics` attribute attached to the function: | ||
@@ -380,3 +380,3 @@ .. testcode:: | ||
| print(raise_my_exception.retry.statistics) | ||
| print(raise_my_exception.statistics) | ||
@@ -501,3 +501,3 @@ .. testoutput:: | ||
| print(raise_my_exception.retry.statistics) | ||
| print(raise_my_exception.statistics) | ||
@@ -521,2 +521,28 @@ .. testoutput:: | ||
| You may also want to change the behaviour of a decorated function temporarily, | ||
| like in tests to avoid unnecessary wait times. You can modify/patch the `retry` | ||
| attribute attached to the function. Bear in mind this is a write-only attribute, | ||
| statistics should be read from the function `statistics` attribute. | ||
| .. testcode:: | ||
| @retry(stop=stop_after_attempt(3), wait=wait_fixed(3)) | ||
| def raise_my_exception(): | ||
| raise MyException("Fail") | ||
| from unittest import mock | ||
| with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)): | ||
| try: | ||
| raise_my_exception() | ||
| except Exception: | ||
| pass | ||
| print(raise_my_exception.statistics) | ||
| .. testoutput:: | ||
| :hide: | ||
| ... | ||
| Retrying code block | ||
@@ -523,0 +549,0 @@ ~~~~~~~~~~~~~~~~~~~ |
| Metadata-Version: 2.1 | ||
| Name: tenacity | ||
| Version: 8.4.2 | ||
| Version: 8.5.0 | ||
| Summary: Retry code until it succeeds | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/jd/tenacity |
@@ -41,2 +41,3 @@ .editorconfig | ||
| releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml | ||
| releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml | ||
| releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml | ||
@@ -43,0 +44,0 @@ releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml |
@@ -342,3 +342,3 @@ # Copyright 2016-2018 Julien Danjou | ||
| # Preserve attributes | ||
| wrapped_f.retry = wrapped_f # type: ignore[attr-defined] | ||
| wrapped_f.retry = self # type: ignore[attr-defined] | ||
| wrapped_f.retry_with = retry_with # type: ignore[attr-defined] | ||
@@ -345,0 +345,0 @@ wrapped_f.statistics = {} # type: ignore[attr-defined] |
@@ -192,3 +192,3 @@ # Copyright 2016 Étienne Bersac | ||
| # Preserve attributes | ||
| async_wrapped.retry = async_wrapped # type: ignore[attr-defined] | ||
| async_wrapped.retry = self # type: ignore[attr-defined] | ||
| async_wrapped.retry_with = wrapped.retry_with # type: ignore[attr-defined] | ||
@@ -195,0 +195,0 @@ async_wrapped.statistics = {} # type: ignore[attr-defined] |
@@ -20,2 +20,3 @@ # mypy: disable-error-code="no-untyped-def,no-untyped-call" | ||
| from functools import wraps | ||
| from unittest import mock | ||
@@ -63,3 +64,3 @@ try: | ||
| await asyncio.sleep(0.00001) | ||
| thing.go() | ||
| return thing.go() | ||
@@ -399,2 +400,60 @@ | ||
| class TestDecoratorWrapper(unittest.TestCase): | ||
| @asynctest | ||
| async def test_retry_function_attributes(self): | ||
| """Test that the wrapped function attributes are exposed as intended. | ||
| - statistics contains the value for the latest function run | ||
| - retry object can be modified to change its behaviour (useful to patch in tests) | ||
| - retry object statistics do not contain valid information | ||
| """ | ||
| self.assertTrue( | ||
| await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(1)) | ||
| ) | ||
| expected_stats = { | ||
| "attempt_number": 2, | ||
| "delay_since_first_attempt": mock.ANY, | ||
| "idle_for": mock.ANY, | ||
| "start_time": mock.ANY, | ||
| } | ||
| self.assertEqual( | ||
| _retryable_coroutine_with_2_attempts.statistics, # type: ignore[attr-defined] | ||
| expected_stats, | ||
| ) | ||
| self.assertEqual( | ||
| _retryable_coroutine_with_2_attempts.retry.statistics, # type: ignore[attr-defined] | ||
| {}, | ||
| ) | ||
| with mock.patch.object( | ||
| _retryable_coroutine_with_2_attempts.retry, # type: ignore[attr-defined] | ||
| "stop", | ||
| tenacity.stop_after_attempt(1), | ||
| ): | ||
| try: | ||
| self.assertTrue( | ||
| await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(2)) | ||
| ) | ||
| except RetryError as exc: | ||
| expected_stats = { | ||
| "attempt_number": 1, | ||
| "delay_since_first_attempt": mock.ANY, | ||
| "idle_for": mock.ANY, | ||
| "start_time": mock.ANY, | ||
| } | ||
| self.assertEqual( | ||
| _retryable_coroutine_with_2_attempts.statistics, # type: ignore[attr-defined] | ||
| expected_stats, | ||
| ) | ||
| self.assertEqual(exc.last_attempt.attempt_number, 1) | ||
| self.assertEqual( | ||
| _retryable_coroutine_with_2_attempts.retry.statistics, # type: ignore[attr-defined] | ||
| {}, | ||
| ) | ||
| else: | ||
| self.fail("RetryError should have been raised after 1 attempt") | ||
| # make sure mypy accepts passing an async sleep function | ||
@@ -401,0 +460,0 @@ # https://github.com/jd/tenacity/issues/399 |
+48
-10
@@ -28,2 +28,3 @@ # mypy: disable_error_code="no-untyped-def,no-untyped-call,attr-defined,arg-type,no-any-return,list-item,var-annotated,import,call-overload" | ||
| from fractions import Fraction | ||
| from unittest import mock | ||
@@ -1077,3 +1078,3 @@ import pytest | ||
| except NameError as e: | ||
| s = _retryable_test_with_unless_exception_type_name.retry.statistics | ||
| s = _retryable_test_with_unless_exception_type_name.statistics | ||
| self.assertTrue(s["attempt_number"] == 6) | ||
@@ -1093,3 +1094,3 @@ print(e) | ||
| except NameError as e: | ||
| s = _retryable_test_with_unless_exception_type_no_input.retry.statistics | ||
| s = _retryable_test_with_unless_exception_type_no_input.statistics | ||
| self.assertTrue(s["attempt_number"] == 6) | ||
@@ -1117,3 +1118,3 @@ print(e) | ||
| except CustomError: | ||
| print(_retryable_test_if_exception_message_message.retry.statistics) | ||
| print(_retryable_test_if_exception_message_message.statistics) | ||
| self.fail("CustomError should've been retried from errormessage") | ||
@@ -1129,3 +1130,3 @@ | ||
| except CustomError: | ||
| s = _retryable_test_if_not_exception_message_message.retry.statistics | ||
| s = _retryable_test_if_not_exception_message_message.statistics | ||
| self.assertTrue(s["attempt_number"] == 1) | ||
@@ -1139,3 +1140,3 @@ | ||
| except NameError: | ||
| s = _retryable_test_not_exception_message_delay.retry.statistics | ||
| s = _retryable_test_not_exception_message_delay.statistics | ||
| print(s["attempt_number"]) | ||
@@ -1160,3 +1161,3 @@ self.assertTrue(s["attempt_number"] == 4) | ||
| except CustomError: | ||
| s = _retryable_test_if_not_exception_message_message.retry.statistics | ||
| s = _retryable_test_if_not_exception_message_message.statistics | ||
| self.assertTrue(s["attempt_number"] == 1) | ||
@@ -1219,3 +1220,40 @@ | ||
| def test_retry_function_attributes(self): | ||
| """Test that the wrapped function attributes are exposed as intended. | ||
| - statistics contains the value for the latest function run | ||
| - retry object can be modified to change its behaviour (useful to patch in tests) | ||
| - retry object statistics do not contain valid information | ||
| """ | ||
| self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2))) | ||
| expected_stats = { | ||
| "attempt_number": 3, | ||
| "delay_since_first_attempt": mock.ANY, | ||
| "idle_for": mock.ANY, | ||
| "start_time": mock.ANY, | ||
| } | ||
| self.assertEqual(_retryable_test_with_stop.statistics, expected_stats) | ||
| self.assertEqual(_retryable_test_with_stop.retry.statistics, {}) | ||
| with mock.patch.object( | ||
| _retryable_test_with_stop.retry, "stop", tenacity.stop_after_attempt(1) | ||
| ): | ||
| try: | ||
| self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2))) | ||
| except RetryError as exc: | ||
| expected_stats = { | ||
| "attempt_number": 1, | ||
| "delay_since_first_attempt": mock.ANY, | ||
| "idle_for": mock.ANY, | ||
| "start_time": mock.ANY, | ||
| } | ||
| self.assertEqual(_retryable_test_with_stop.statistics, expected_stats) | ||
| self.assertEqual(exc.last_attempt.attempt_number, 1) | ||
| self.assertEqual(_retryable_test_with_stop.retry.statistics, {}) | ||
| else: | ||
| self.fail("RetryError should have been raised after 1 attempt") | ||
| class TestRetryWith: | ||
@@ -1490,5 +1528,5 @@ def test_redefine_wait(self): | ||
| self.assertEqual({}, _foobar.retry.statistics) | ||
| self.assertEqual({}, _foobar.statistics) | ||
| _foobar() | ||
| self.assertEqual(1, _foobar.retry.statistics["attempt_number"]) | ||
| self.assertEqual(1, _foobar.statistics["attempt_number"]) | ||
@@ -1500,3 +1538,3 @@ def test_stats_failing(self): | ||
| self.assertEqual({}, _foobar.retry.statistics) | ||
| self.assertEqual({}, _foobar.statistics) | ||
| try: | ||
@@ -1506,3 +1544,3 @@ _foobar() | ||
| pass | ||
| self.assertEqual(2, _foobar.retry.statistics["attempt_number"]) | ||
| self.assertEqual(2, _foobar.statistics["attempt_number"]) | ||
@@ -1509,0 +1547,0 @@ |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
219925
2.55%82
1.23%3729
2.36%