tenacity 8.4.1__tar.gz → 8.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tenacity-8.4.1 → tenacity-8.5.0}/.github/workflows/ci.yaml +1 -1
- {tenacity-8.4.1 → tenacity-8.5.0}/.github/workflows/deploy.yaml +1 -1
- {tenacity-8.4.1/tenacity.egg-info → tenacity-8.5.0}/PKG-INFO +1 -1
- {tenacity-8.4.1 → tenacity-8.5.0}/README.rst +31 -5
- {tenacity-8.4.1 → tenacity-8.5.0}/doc/source/index.rst +31 -5
- tenacity-8.5.0/releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml +4 -0
- tenacity-8.5.0/releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml +6 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/__init__.py +7 -1
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/asyncio/__init__.py +9 -4
- {tenacity-8.4.1 → tenacity-8.5.0/tenacity.egg-info}/PKG-INFO +1 -1
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity.egg-info/SOURCES.txt +3 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tests/test_asyncio.py +60 -1
- tenacity-8.5.0/tests/test_issue_478.py +118 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tests/test_tenacity.py +48 -10
- {tenacity-8.4.1 → tenacity-8.5.0}/.editorconfig +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/.github/dependabot.yml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/.gitignore +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/.mergify.yml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/.readthedocs.yml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/LICENSE +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/doc/source/api.rst +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/doc/source/changelog.rst +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/doc/source/conf.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/pyproject.toml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add-async-actions-b249c527d99723bb.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add-reno-d1ab5710f272650a.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add-test-extra-55e869261b03e56d.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/after_log-50f4d73b24ce9203.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/annotate_code-197b93130df14042.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/pr320-py3-only-wheel-tag.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/remove-py36-876c0416cf279d15.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/reno.yaml +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/setup.cfg +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/setup.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/_utils.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/after.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/asyncio/retry.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/before.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/before_sleep.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/nap.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/py.typed +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/retry.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/stop.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/tornadoweb.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity/wait.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity.egg-info/dependency_links.txt +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity.egg-info/requires.txt +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tenacity.egg-info/top_level.txt +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tests/__init__.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tests/test_after.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tests/test_tornado.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tests/test_utils.py +0 -0
- {tenacity-8.4.1 → tenacity-8.5.0}/tox.ini +0 -0
|
@@ -124,8 +124,8 @@ retrying stuff.
|
|
|
124
124
|
print("Stopping after 10 seconds")
|
|
125
125
|
raise Exception
|
|
126
126
|
|
|
127
|
-
If you're on a tight deadline, and exceeding your delay time isn't ok,
|
|
128
|
-
then you can give up on retries one attempt before you would exceed the delay.
|
|
127
|
+
If you're on a tight deadline, and exceeding your delay time isn't ok,
|
|
128
|
+
then you can give up on retries one attempt before you would exceed the delay.
|
|
129
129
|
|
|
130
130
|
.. testcode::
|
|
131
131
|
|
|
@@ -362,7 +362,7 @@ Statistics
|
|
|
362
362
|
~~~~~~~~~~
|
|
363
363
|
|
|
364
364
|
You can access the statistics about the retry made over a function by using the
|
|
365
|
-
`
|
|
365
|
+
`statistics` attribute attached to the function:
|
|
366
366
|
|
|
367
367
|
.. testcode::
|
|
368
368
|
|
|
@@ -375,7 +375,7 @@ You can access the statistics about the retry made over a function by using the
|
|
|
375
375
|
except Exception:
|
|
376
376
|
pass
|
|
377
377
|
|
|
378
|
-
print(raise_my_exception.
|
|
378
|
+
print(raise_my_exception.statistics)
|
|
379
379
|
|
|
380
380
|
.. testoutput::
|
|
381
381
|
:hide:
|
|
@@ -495,7 +495,7 @@ using the `retry_with` function attached to the wrapped function:
|
|
|
495
495
|
except Exception:
|
|
496
496
|
pass
|
|
497
497
|
|
|
498
|
-
print(raise_my_exception.
|
|
498
|
+
print(raise_my_exception.statistics)
|
|
499
499
|
|
|
500
500
|
.. testoutput::
|
|
501
501
|
:hide:
|
|
@@ -514,6 +514,32 @@ to use the `retry` decorator - you can instead use `Retrying` directly:
|
|
|
514
514
|
retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True)
|
|
515
515
|
retryer(never_good_enough, 'I really do try')
|
|
516
516
|
|
|
517
|
+
You may also want to change the behaviour of a decorated function temporarily,
|
|
518
|
+
like in tests to avoid unnecessary wait times. You can modify/patch the `retry`
|
|
519
|
+
attribute attached to the function. Bear in mind this is a write-only attribute,
|
|
520
|
+
statistics should be read from the function `statistics` attribute.
|
|
521
|
+
|
|
522
|
+
.. testcode::
|
|
523
|
+
|
|
524
|
+
@retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
|
|
525
|
+
def raise_my_exception():
|
|
526
|
+
raise MyException("Fail")
|
|
527
|
+
|
|
528
|
+
from unittest import mock
|
|
529
|
+
|
|
530
|
+
with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)):
|
|
531
|
+
try:
|
|
532
|
+
raise_my_exception()
|
|
533
|
+
except Exception:
|
|
534
|
+
pass
|
|
535
|
+
|
|
536
|
+
print(raise_my_exception.statistics)
|
|
537
|
+
|
|
538
|
+
.. testoutput::
|
|
539
|
+
:hide:
|
|
540
|
+
|
|
541
|
+
...
|
|
542
|
+
|
|
517
543
|
Retrying code block
|
|
518
544
|
~~~~~~~~~~~~~~~~~~~
|
|
519
545
|
|
|
@@ -124,8 +124,8 @@ retrying stuff.
|
|
|
124
124
|
print("Stopping after 10 seconds")
|
|
125
125
|
raise Exception
|
|
126
126
|
|
|
127
|
-
If you're on a tight deadline, and exceeding your delay time isn't ok,
|
|
128
|
-
then you can give up on retries one attempt before you would exceed the delay.
|
|
127
|
+
If you're on a tight deadline, and exceeding your delay time isn't ok,
|
|
128
|
+
then you can give up on retries one attempt before you would exceed the delay.
|
|
129
129
|
|
|
130
130
|
.. testcode::
|
|
131
131
|
|
|
@@ -362,7 +362,7 @@ Statistics
|
|
|
362
362
|
~~~~~~~~~~
|
|
363
363
|
|
|
364
364
|
You can access the statistics about the retry made over a function by using the
|
|
365
|
-
`
|
|
365
|
+
`statistics` attribute attached to the function:
|
|
366
366
|
|
|
367
367
|
.. testcode::
|
|
368
368
|
|
|
@@ -375,7 +375,7 @@ You can access the statistics about the retry made over a function by using the
|
|
|
375
375
|
except Exception:
|
|
376
376
|
pass
|
|
377
377
|
|
|
378
|
-
print(raise_my_exception.
|
|
378
|
+
print(raise_my_exception.statistics)
|
|
379
379
|
|
|
380
380
|
.. testoutput::
|
|
381
381
|
:hide:
|
|
@@ -495,7 +495,7 @@ using the `retry_with` function attached to the wrapped function:
|
|
|
495
495
|
except Exception:
|
|
496
496
|
pass
|
|
497
497
|
|
|
498
|
-
print(raise_my_exception.
|
|
498
|
+
print(raise_my_exception.statistics)
|
|
499
499
|
|
|
500
500
|
.. testoutput::
|
|
501
501
|
:hide:
|
|
@@ -514,6 +514,32 @@ to use the `retry` decorator - you can instead use `Retrying` directly:
|
|
|
514
514
|
retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True)
|
|
515
515
|
retryer(never_good_enough, 'I really do try')
|
|
516
516
|
|
|
517
|
+
You may also want to change the behaviour of a decorated function temporarily,
|
|
518
|
+
like in tests to avoid unnecessary wait times. You can modify/patch the `retry`
|
|
519
|
+
attribute attached to the function. Bear in mind this is a write-only attribute,
|
|
520
|
+
statistics should be read from the function `statistics` attribute.
|
|
521
|
+
|
|
522
|
+
.. testcode::
|
|
523
|
+
|
|
524
|
+
@retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
|
|
525
|
+
def raise_my_exception():
|
|
526
|
+
raise MyException("Fail")
|
|
527
|
+
|
|
528
|
+
from unittest import mock
|
|
529
|
+
|
|
530
|
+
with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)):
|
|
531
|
+
try:
|
|
532
|
+
raise_my_exception()
|
|
533
|
+
except Exception:
|
|
534
|
+
pass
|
|
535
|
+
|
|
536
|
+
print(raise_my_exception.statistics)
|
|
537
|
+
|
|
538
|
+
.. testoutput::
|
|
539
|
+
:hide:
|
|
540
|
+
|
|
541
|
+
...
|
|
542
|
+
|
|
517
543
|
Retrying code block
|
|
518
544
|
~~~~~~~~~~~~~~~~~~~
|
|
519
545
|
|
|
@@ -329,13 +329,19 @@ class BaseRetrying(ABC):
|
|
|
329
329
|
f, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__")
|
|
330
330
|
)
|
|
331
331
|
def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any:
|
|
332
|
-
|
|
332
|
+
# Always create a copy to prevent overwriting the local contexts when
|
|
333
|
+
# calling the same wrapped functions multiple times in the same stack
|
|
334
|
+
copy = self.copy()
|
|
335
|
+
wrapped_f.statistics = copy.statistics # type: ignore[attr-defined]
|
|
336
|
+
return copy(f, *args, **kw)
|
|
333
337
|
|
|
334
338
|
def retry_with(*args: t.Any, **kwargs: t.Any) -> WrappedFn:
|
|
335
339
|
return self.copy(*args, **kwargs).wraps(f)
|
|
336
340
|
|
|
341
|
+
# Preserve attributes
|
|
337
342
|
wrapped_f.retry = self # type: ignore[attr-defined]
|
|
338
343
|
wrapped_f.retry_with = retry_with # type: ignore[attr-defined]
|
|
344
|
+
wrapped_f.statistics = {} # type: ignore[attr-defined]
|
|
339
345
|
|
|
340
346
|
return wrapped_f # type: ignore[return-value]
|
|
341
347
|
|
|
@@ -175,18 +175,23 @@ class AsyncRetrying(BaseRetrying):
|
|
|
175
175
|
raise StopAsyncIteration
|
|
176
176
|
|
|
177
177
|
def wraps(self, fn: WrappedFn) -> WrappedFn:
|
|
178
|
-
|
|
178
|
+
wrapped = super().wraps(fn)
|
|
179
179
|
# Ensure wrapper is recognized as a coroutine function.
|
|
180
180
|
|
|
181
181
|
@functools.wraps(
|
|
182
182
|
fn, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__")
|
|
183
183
|
)
|
|
184
184
|
async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
185
|
-
|
|
185
|
+
# Always create a copy to prevent overwriting the local contexts when
|
|
186
|
+
# calling the same wrapped functions multiple times in the same stack
|
|
187
|
+
copy = self.copy()
|
|
188
|
+
async_wrapped.statistics = copy.statistics # type: ignore[attr-defined]
|
|
189
|
+
return await copy(fn, *args, **kwargs)
|
|
186
190
|
|
|
187
191
|
# Preserve attributes
|
|
188
|
-
async_wrapped.retry =
|
|
189
|
-
async_wrapped.retry_with =
|
|
192
|
+
async_wrapped.retry = self # type: ignore[attr-defined]
|
|
193
|
+
async_wrapped.retry_with = wrapped.retry_with # type: ignore[attr-defined]
|
|
194
|
+
async_wrapped.statistics = {} # type: ignore[attr-defined]
|
|
190
195
|
|
|
191
196
|
return async_wrapped # type: ignore[return-value]
|
|
192
197
|
|
|
@@ -37,6 +37,8 @@ releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml
|
|
|
37
37
|
releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml
|
|
38
38
|
releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml
|
|
39
39
|
releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml
|
|
40
|
+
releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml
|
|
41
|
+
releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml
|
|
40
42
|
releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml
|
|
41
43
|
releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml
|
|
42
44
|
releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml
|
|
@@ -73,6 +75,7 @@ tenacity/asyncio/retry.py
|
|
|
73
75
|
tests/__init__.py
|
|
74
76
|
tests/test_after.py
|
|
75
77
|
tests/test_asyncio.py
|
|
78
|
+
tests/test_issue_478.py
|
|
76
79
|
tests/test_tenacity.py
|
|
77
80
|
tests/test_tornado.py
|
|
78
81
|
tests/test_utils.py
|
|
@@ -17,6 +17,7 @@ import asyncio
|
|
|
17
17
|
import inspect
|
|
18
18
|
import unittest
|
|
19
19
|
from functools import wraps
|
|
20
|
+
from unittest import mock
|
|
20
21
|
|
|
21
22
|
try:
|
|
22
23
|
import trio
|
|
@@ -59,7 +60,7 @@ async def _retryable_coroutine(thing):
|
|
|
59
60
|
@retry(stop=stop_after_attempt(2))
|
|
60
61
|
async def _retryable_coroutine_with_2_attempts(thing):
|
|
61
62
|
await asyncio.sleep(0.00001)
|
|
62
|
-
thing.go()
|
|
63
|
+
return thing.go()
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
class TestAsyncio(unittest.TestCase):
|
|
@@ -394,6 +395,64 @@ class TestContextManager(unittest.TestCase):
|
|
|
394
395
|
await _async_function(thing)
|
|
395
396
|
|
|
396
397
|
|
|
398
|
+
class TestDecoratorWrapper(unittest.TestCase):
|
|
399
|
+
@asynctest
|
|
400
|
+
async def test_retry_function_attributes(self):
|
|
401
|
+
"""Test that the wrapped function attributes are exposed as intended.
|
|
402
|
+
|
|
403
|
+
- statistics contains the value for the latest function run
|
|
404
|
+
- retry object can be modified to change its behaviour (useful to patch in tests)
|
|
405
|
+
- retry object statistics do not contain valid information
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
self.assertTrue(
|
|
409
|
+
await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(1))
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
expected_stats = {
|
|
413
|
+
"attempt_number": 2,
|
|
414
|
+
"delay_since_first_attempt": mock.ANY,
|
|
415
|
+
"idle_for": mock.ANY,
|
|
416
|
+
"start_time": mock.ANY,
|
|
417
|
+
}
|
|
418
|
+
self.assertEqual(
|
|
419
|
+
_retryable_coroutine_with_2_attempts.statistics, # type: ignore[attr-defined]
|
|
420
|
+
expected_stats,
|
|
421
|
+
)
|
|
422
|
+
self.assertEqual(
|
|
423
|
+
_retryable_coroutine_with_2_attempts.retry.statistics, # type: ignore[attr-defined]
|
|
424
|
+
{},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
with mock.patch.object(
|
|
428
|
+
_retryable_coroutine_with_2_attempts.retry, # type: ignore[attr-defined]
|
|
429
|
+
"stop",
|
|
430
|
+
tenacity.stop_after_attempt(1),
|
|
431
|
+
):
|
|
432
|
+
try:
|
|
433
|
+
self.assertTrue(
|
|
434
|
+
await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(2))
|
|
435
|
+
)
|
|
436
|
+
except RetryError as exc:
|
|
437
|
+
expected_stats = {
|
|
438
|
+
"attempt_number": 1,
|
|
439
|
+
"delay_since_first_attempt": mock.ANY,
|
|
440
|
+
"idle_for": mock.ANY,
|
|
441
|
+
"start_time": mock.ANY,
|
|
442
|
+
}
|
|
443
|
+
self.assertEqual(
|
|
444
|
+
_retryable_coroutine_with_2_attempts.statistics, # type: ignore[attr-defined]
|
|
445
|
+
expected_stats,
|
|
446
|
+
)
|
|
447
|
+
self.assertEqual(exc.last_attempt.attempt_number, 1)
|
|
448
|
+
self.assertEqual(
|
|
449
|
+
_retryable_coroutine_with_2_attempts.retry.statistics, # type: ignore[attr-defined]
|
|
450
|
+
{},
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
self.fail("RetryError should have been raised after 1 attempt")
|
|
454
|
+
|
|
455
|
+
|
|
397
456
|
# make sure mypy accepts passing an async sleep function
|
|
398
457
|
# https://github.com/jd/tenacity/issues/399
|
|
399
458
|
async def my_async_sleep(x: float) -> None:
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import typing
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from functools import wraps
|
|
6
|
+
|
|
7
|
+
from tenacity import RetryCallState, retry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def asynctest(
|
|
11
|
+
callable_: typing.Callable[..., typing.Any],
|
|
12
|
+
) -> typing.Callable[..., typing.Any]:
|
|
13
|
+
@wraps(callable_)
|
|
14
|
+
def wrapper(*a: typing.Any, **kw: typing.Any) -> typing.Any:
|
|
15
|
+
loop = asyncio.get_event_loop()
|
|
16
|
+
return loop.run_until_complete(callable_(*a, **kw))
|
|
17
|
+
|
|
18
|
+
return wrapper
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
MAX_RETRY_FIX_ATTEMPTS = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestIssue478(unittest.TestCase):
|
|
25
|
+
def test_issue(self) -> None:
|
|
26
|
+
results = []
|
|
27
|
+
|
|
28
|
+
def do_retry(retry_state: RetryCallState) -> bool:
|
|
29
|
+
outcome = retry_state.outcome
|
|
30
|
+
assert outcome
|
|
31
|
+
ex = outcome.exception()
|
|
32
|
+
_subject_: str = retry_state.args[0]
|
|
33
|
+
|
|
34
|
+
if _subject_ == "Fix": # no retry on fix failure
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
if retry_state.attempt_number >= MAX_RETRY_FIX_ATTEMPTS:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
if ex:
|
|
41
|
+
do_fix_work()
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
@retry(reraise=True, retry=do_retry)
|
|
47
|
+
def _do_work(subject: str) -> None:
|
|
48
|
+
if subject == "Error":
|
|
49
|
+
results.append(f"{subject} is not working")
|
|
50
|
+
raise Exception(f"{subject} is not working")
|
|
51
|
+
results.append(f"{subject} is working")
|
|
52
|
+
|
|
53
|
+
def do_any_work(subject: str) -> None:
|
|
54
|
+
_do_work(subject)
|
|
55
|
+
|
|
56
|
+
def do_fix_work() -> None:
|
|
57
|
+
_do_work("Fix")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
do_any_work("Error")
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
assert str(exc) == "Error is not working"
|
|
63
|
+
else:
|
|
64
|
+
assert False, "No exception caught"
|
|
65
|
+
|
|
66
|
+
assert results == [
|
|
67
|
+
"Error is not working",
|
|
68
|
+
"Fix is working",
|
|
69
|
+
"Error is not working",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
@asynctest
|
|
73
|
+
async def test_async(self) -> None:
|
|
74
|
+
results = []
|
|
75
|
+
|
|
76
|
+
async def do_retry(retry_state: RetryCallState) -> bool:
|
|
77
|
+
outcome = retry_state.outcome
|
|
78
|
+
assert outcome
|
|
79
|
+
ex = outcome.exception()
|
|
80
|
+
_subject_: str = retry_state.args[0]
|
|
81
|
+
|
|
82
|
+
if _subject_ == "Fix": # no retry on fix failure
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
if retry_state.attempt_number >= MAX_RETRY_FIX_ATTEMPTS:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
if ex:
|
|
89
|
+
await do_fix_work()
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
@retry(reraise=True, retry=do_retry)
|
|
95
|
+
async def _do_work(subject: str) -> None:
|
|
96
|
+
if subject == "Error":
|
|
97
|
+
results.append(f"{subject} is not working")
|
|
98
|
+
raise Exception(f"{subject} is not working")
|
|
99
|
+
results.append(f"{subject} is working")
|
|
100
|
+
|
|
101
|
+
async def do_any_work(subject: str) -> None:
|
|
102
|
+
await _do_work(subject)
|
|
103
|
+
|
|
104
|
+
async def do_fix_work() -> None:
|
|
105
|
+
await _do_work("Fix")
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
await do_any_work("Error")
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
assert str(exc) == "Error is not working"
|
|
111
|
+
else:
|
|
112
|
+
assert False, "No exception caught"
|
|
113
|
+
|
|
114
|
+
assert results == [
|
|
115
|
+
"Error is not working",
|
|
116
|
+
"Fix is working",
|
|
117
|
+
"Error is not working",
|
|
118
|
+
]
|
|
@@ -25,6 +25,7 @@ import warnings
|
|
|
25
25
|
from contextlib import contextmanager
|
|
26
26
|
from copy import copy
|
|
27
27
|
from fractions import Fraction
|
|
28
|
+
from unittest import mock
|
|
28
29
|
|
|
29
30
|
import pytest
|
|
30
31
|
|
|
@@ -1073,7 +1074,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
|
|
1073
1074
|
_retryable_test_with_unless_exception_type_name(NameErrorUntilCount(5))
|
|
1074
1075
|
)
|
|
1075
1076
|
except NameError as e:
|
|
1076
|
-
s = _retryable_test_with_unless_exception_type_name.
|
|
1077
|
+
s = _retryable_test_with_unless_exception_type_name.statistics
|
|
1077
1078
|
self.assertTrue(s["attempt_number"] == 6)
|
|
1078
1079
|
print(e)
|
|
1079
1080
|
else:
|
|
@@ -1088,7 +1089,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
|
|
1088
1089
|
)
|
|
1089
1090
|
)
|
|
1090
1091
|
except NameError as e:
|
|
1091
|
-
s = _retryable_test_with_unless_exception_type_no_input.
|
|
1092
|
+
s = _retryable_test_with_unless_exception_type_no_input.statistics
|
|
1092
1093
|
self.assertTrue(s["attempt_number"] == 6)
|
|
1093
1094
|
print(e)
|
|
1094
1095
|
else:
|
|
@@ -1111,7 +1112,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
|
|
1111
1112
|
_retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3))
|
|
1112
1113
|
)
|
|
1113
1114
|
except CustomError:
|
|
1114
|
-
print(_retryable_test_if_exception_message_message.
|
|
1115
|
+
print(_retryable_test_if_exception_message_message.statistics)
|
|
1115
1116
|
self.fail("CustomError should've been retried from errormessage")
|
|
1116
1117
|
|
|
1117
1118
|
def test_retry_if_not_exception_message(self):
|
|
@@ -1122,7 +1123,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
|
|
1122
1123
|
)
|
|
1123
1124
|
)
|
|
1124
1125
|
except CustomError:
|
|
1125
|
-
s = _retryable_test_if_not_exception_message_message.
|
|
1126
|
+
s = _retryable_test_if_not_exception_message_message.statistics
|
|
1126
1127
|
self.assertTrue(s["attempt_number"] == 1)
|
|
1127
1128
|
|
|
1128
1129
|
def test_retry_if_not_exception_message_delay(self):
|
|
@@ -1131,7 +1132,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
|
|
1131
1132
|
_retryable_test_not_exception_message_delay(NameErrorUntilCount(3))
|
|
1132
1133
|
)
|
|
1133
1134
|
except NameError:
|
|
1134
|
-
s = _retryable_test_not_exception_message_delay.
|
|
1135
|
+
s = _retryable_test_not_exception_message_delay.statistics
|
|
1135
1136
|
print(s["attempt_number"])
|
|
1136
1137
|
self.assertTrue(s["attempt_number"] == 4)
|
|
1137
1138
|
|
|
@@ -1151,7 +1152,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
|
|
1151
1152
|
)
|
|
1152
1153
|
)
|
|
1153
1154
|
except CustomError:
|
|
1154
|
-
s = _retryable_test_if_not_exception_message_message.
|
|
1155
|
+
s = _retryable_test_if_not_exception_message_message.statistics
|
|
1155
1156
|
self.assertTrue(s["attempt_number"] == 1)
|
|
1156
1157
|
|
|
1157
1158
|
def test_retry_if_exception_cause_type(self):
|
|
@@ -1209,6 +1210,43 @@ class TestDecoratorWrapper(unittest.TestCase):
|
|
|
1209
1210
|
h = retrying.wraps(Hello())
|
|
1210
1211
|
self.assertEqual(h(), "Hello")
|
|
1211
1212
|
|
|
1213
|
+
def test_retry_function_attributes(self):
|
|
1214
|
+
"""Test that the wrapped function attributes are exposed as intended.
|
|
1215
|
+
|
|
1216
|
+
- statistics contains the value for the latest function run
|
|
1217
|
+
- retry object can be modified to change its behaviour (useful to patch in tests)
|
|
1218
|
+
- retry object statistics do not contain valid information
|
|
1219
|
+
"""
|
|
1220
|
+
|
|
1221
|
+
self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2)))
|
|
1222
|
+
|
|
1223
|
+
expected_stats = {
|
|
1224
|
+
"attempt_number": 3,
|
|
1225
|
+
"delay_since_first_attempt": mock.ANY,
|
|
1226
|
+
"idle_for": mock.ANY,
|
|
1227
|
+
"start_time": mock.ANY,
|
|
1228
|
+
}
|
|
1229
|
+
self.assertEqual(_retryable_test_with_stop.statistics, expected_stats)
|
|
1230
|
+
self.assertEqual(_retryable_test_with_stop.retry.statistics, {})
|
|
1231
|
+
|
|
1232
|
+
with mock.patch.object(
|
|
1233
|
+
_retryable_test_with_stop.retry, "stop", tenacity.stop_after_attempt(1)
|
|
1234
|
+
):
|
|
1235
|
+
try:
|
|
1236
|
+
self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2)))
|
|
1237
|
+
except RetryError as exc:
|
|
1238
|
+
expected_stats = {
|
|
1239
|
+
"attempt_number": 1,
|
|
1240
|
+
"delay_since_first_attempt": mock.ANY,
|
|
1241
|
+
"idle_for": mock.ANY,
|
|
1242
|
+
"start_time": mock.ANY,
|
|
1243
|
+
}
|
|
1244
|
+
self.assertEqual(_retryable_test_with_stop.statistics, expected_stats)
|
|
1245
|
+
self.assertEqual(exc.last_attempt.attempt_number, 1)
|
|
1246
|
+
self.assertEqual(_retryable_test_with_stop.retry.statistics, {})
|
|
1247
|
+
else:
|
|
1248
|
+
self.fail("RetryError should have been raised after 1 attempt")
|
|
1249
|
+
|
|
1212
1250
|
|
|
1213
1251
|
class TestRetryWith:
|
|
1214
1252
|
def test_redefine_wait(self):
|
|
@@ -1479,21 +1517,21 @@ class TestStatistics(unittest.TestCase):
|
|
|
1479
1517
|
def _foobar():
|
|
1480
1518
|
return 42
|
|
1481
1519
|
|
|
1482
|
-
self.assertEqual({}, _foobar.
|
|
1520
|
+
self.assertEqual({}, _foobar.statistics)
|
|
1483
1521
|
_foobar()
|
|
1484
|
-
self.assertEqual(1, _foobar.
|
|
1522
|
+
self.assertEqual(1, _foobar.statistics["attempt_number"])
|
|
1485
1523
|
|
|
1486
1524
|
def test_stats_failing(self):
|
|
1487
1525
|
@retry(stop=tenacity.stop_after_attempt(2))
|
|
1488
1526
|
def _foobar():
|
|
1489
1527
|
raise ValueError(42)
|
|
1490
1528
|
|
|
1491
|
-
self.assertEqual({}, _foobar.
|
|
1529
|
+
self.assertEqual({}, _foobar.statistics)
|
|
1492
1530
|
try:
|
|
1493
1531
|
_foobar()
|
|
1494
1532
|
except Exception: # noqa: B902
|
|
1495
1533
|
pass
|
|
1496
|
-
self.assertEqual(2, _foobar.
|
|
1534
|
+
self.assertEqual(2, _foobar.statistics["attempt_number"])
|
|
1497
1535
|
|
|
1498
1536
|
|
|
1499
1537
|
class TestRetryErrorCallback(unittest.TestCase):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add-async-actions-b249c527d99723bb.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml
RENAMED
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml
RENAMED
|
File without changes
|
{tenacity-8.4.1 → tenacity-8.5.0}/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|