tenacity 8.4.2__tar.gz → 9.0.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.
Files changed (83) hide show
  1. {tenacity-8.4.2 → tenacity-9.0.0}/.github/workflows/ci.yaml +1 -1
  2. {tenacity-8.4.2 → tenacity-9.0.0}/.github/workflows/deploy.yaml +1 -1
  3. {tenacity-8.4.2/tenacity.egg-info → tenacity-9.0.0}/PKG-INFO +1 -1
  4. {tenacity-8.4.2 → tenacity-9.0.0}/README.rst +31 -5
  5. {tenacity-8.4.2 → tenacity-9.0.0}/doc/source/index.rst +31 -5
  6. tenacity-9.0.0/releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml +6 -0
  7. tenacity-9.0.0/releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml +4 -0
  8. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/__init__.py +1 -1
  9. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/asyncio/__init__.py +1 -1
  10. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/wait.py +1 -1
  11. {tenacity-8.4.2 → tenacity-9.0.0/tenacity.egg-info}/PKG-INFO +1 -1
  12. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity.egg-info/SOURCES.txt +2 -0
  13. {tenacity-8.4.2 → tenacity-9.0.0}/tests/test_asyncio.py +60 -1
  14. {tenacity-8.4.2 → tenacity-9.0.0}/tests/test_tenacity.py +58 -12
  15. {tenacity-8.4.2 → tenacity-9.0.0}/.editorconfig +0 -0
  16. {tenacity-8.4.2 → tenacity-9.0.0}/.github/dependabot.yml +0 -0
  17. {tenacity-8.4.2 → tenacity-9.0.0}/.gitignore +0 -0
  18. {tenacity-8.4.2 → tenacity-9.0.0}/.mergify.yml +0 -0
  19. {tenacity-8.4.2 → tenacity-9.0.0}/.readthedocs.yml +0 -0
  20. {tenacity-8.4.2 → tenacity-9.0.0}/LICENSE +0 -0
  21. {tenacity-8.4.2 → tenacity-9.0.0}/doc/source/api.rst +0 -0
  22. {tenacity-8.4.2 → tenacity-9.0.0}/doc/source/changelog.rst +0 -0
  23. {tenacity-8.4.2 → tenacity-9.0.0}/doc/source/conf.py +0 -0
  24. {tenacity-8.4.2 → tenacity-9.0.0}/pyproject.toml +0 -0
  25. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml +0 -0
  26. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml +0 -0
  27. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/add-async-actions-b249c527d99723bb.yaml +0 -0
  28. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/add-reno-d1ab5710f272650a.yaml +0 -0
  29. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml +0 -0
  30. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml +0 -0
  31. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/add-test-extra-55e869261b03e56d.yaml +0 -0
  32. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml +0 -0
  33. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml +0 -0
  34. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml +0 -0
  35. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/after_log-50f4d73b24ce9203.yaml +0 -0
  36. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml +0 -0
  37. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/annotate_code-197b93130df14042.yaml +0 -0
  38. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml +0 -0
  39. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml +0 -0
  40. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml +0 -0
  41. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml +0 -0
  42. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml +0 -0
  43. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml +0 -0
  44. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml +0 -0
  45. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml +0 -0
  46. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml +0 -0
  47. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml +0 -0
  48. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml +0 -0
  49. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml +0 -0
  50. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml +0 -0
  51. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml +0 -0
  52. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/pr320-py3-only-wheel-tag.yaml +0 -0
  53. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml +0 -0
  54. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/remove-py36-876c0416cf279d15.yaml +0 -0
  55. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml +0 -0
  56. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml +0 -0
  57. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml +0 -0
  58. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml +0 -0
  59. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml +0 -0
  60. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml +0 -0
  61. {tenacity-8.4.2 → tenacity-9.0.0}/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml +0 -0
  62. {tenacity-8.4.2 → tenacity-9.0.0}/reno.yaml +0 -0
  63. {tenacity-8.4.2 → tenacity-9.0.0}/setup.cfg +0 -0
  64. {tenacity-8.4.2 → tenacity-9.0.0}/setup.py +0 -0
  65. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/_utils.py +0 -0
  66. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/after.py +0 -0
  67. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/asyncio/retry.py +0 -0
  68. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/before.py +0 -0
  69. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/before_sleep.py +0 -0
  70. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/nap.py +0 -0
  71. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/py.typed +0 -0
  72. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/retry.py +0 -0
  73. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/stop.py +0 -0
  74. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity/tornadoweb.py +0 -0
  75. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity.egg-info/dependency_links.txt +0 -0
  76. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity.egg-info/requires.txt +0 -0
  77. {tenacity-8.4.2 → tenacity-9.0.0}/tenacity.egg-info/top_level.txt +0 -0
  78. {tenacity-8.4.2 → tenacity-9.0.0}/tests/__init__.py +0 -0
  79. {tenacity-8.4.2 → tenacity-9.0.0}/tests/test_after.py +0 -0
  80. {tenacity-8.4.2 → tenacity-9.0.0}/tests/test_issue_478.py +0 -0
  81. {tenacity-8.4.2 → tenacity-9.0.0}/tests/test_tornado.py +0 -0
  82. {tenacity-8.4.2 → tenacity-9.0.0}/tests/test_utils.py +0 -0
  83. {tenacity-8.4.2 → tenacity-9.0.0}/tox.ini +0 -0
@@ -34,7 +34,7 @@ jobs:
34
34
  tox: mypy
35
35
  steps:
36
36
  - name: Checkout 🛎️
37
- uses: actions/checkout@v4.1.6
37
+ uses: actions/checkout@v4.1.7
38
38
  with:
39
39
  fetch-depth: 0
40
40
 
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - name: Checkout 🛎️
14
- uses: actions/checkout@v4.1.6
14
+ uses: actions/checkout@v4.1.7
15
15
  with:
16
16
  fetch-depth: 0
17
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tenacity
3
- Version: 8.4.2
3
+ Version: 9.0.0
4
4
  Summary: Retry code until it succeeds
5
5
  Home-page: https://github.com/jd/tenacity
6
6
  Author: Julien Danjou
@@ -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
- `retry` attribute attached to the function and its `statistics` attribute:
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.retry.statistics)
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.retry.statistics)
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
- `retry` attribute attached to the function and its `statistics` attribute:
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.retry.statistics)
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.retry.statistics)
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
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ fixes:
3
+ - |
4
+ Restore the value of the `retry` attribute for wrapped functions. Also,
5
+ clarify that those attributes are write-only and statistics should be
6
+ read from the function attribute directly.
@@ -0,0 +1,4 @@
1
+ ---
2
+ fixes:
3
+ - |
4
+ Respects `min` arg for `wait_random_exponential`
@@ -339,7 +339,7 @@ class BaseRetrying(ABC):
339
339
  return self.copy(*args, **kwargs).wraps(f)
340
340
 
341
341
  # Preserve attributes
342
- wrapped_f.retry = wrapped_f # type: ignore[attr-defined]
342
+ wrapped_f.retry = self # type: ignore[attr-defined]
343
343
  wrapped_f.retry_with = retry_with # type: ignore[attr-defined]
344
344
  wrapped_f.statistics = {} # type: ignore[attr-defined]
345
345
 
@@ -189,7 +189,7 @@ class AsyncRetrying(BaseRetrying):
189
189
  return await copy(fn, *args, **kwargs)
190
190
 
191
191
  # Preserve attributes
192
- async_wrapped.retry = async_wrapped # type: ignore[attr-defined]
192
+ async_wrapped.retry = self # type: ignore[attr-defined]
193
193
  async_wrapped.retry_with = wrapped.retry_with # type: ignore[attr-defined]
194
194
  async_wrapped.statistics = {} # type: ignore[attr-defined]
195
195
 
@@ -197,7 +197,7 @@ class wait_random_exponential(wait_exponential):
197
197
 
198
198
  def __call__(self, retry_state: "RetryCallState") -> float:
199
199
  high = super().__call__(retry_state=retry_state)
200
- return random.uniform(0, high)
200
+ return random.uniform(self.min, high)
201
201
 
202
202
 
203
203
  class wait_exponential_jitter(wait_base):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tenacity
3
- Version: 8.4.2
3
+ Version: 9.0.0
4
4
  Summary: Retry code until it succeeds
5
5
  Home-page: https://github.com/jd/tenacity
6
6
  Author: Julien Danjou
@@ -38,6 +38,7 @@ 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
40
  releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml
41
+ releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml
41
42
  releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml
42
43
  releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml
43
44
  releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml
@@ -52,6 +53,7 @@ releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml
52
53
  releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml
53
54
  releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml
54
55
  releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml
56
+ releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml
55
57
  releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml
56
58
  tenacity/__init__.py
57
59
  tenacity/_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:
@@ -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
 
@@ -471,9 +472,17 @@ class TestWaitConditions(unittest.TestCase):
471
472
  self._assert_inclusive_range(fn(make_retry_state(8, 0)), 0, 60.0)
472
473
  self._assert_inclusive_range(fn(make_retry_state(9, 0)), 0, 60.0)
473
474
 
474
- fn = tenacity.wait_random_exponential(10, 5)
475
+ # max wait
476
+ max_wait = 5
477
+ fn = tenacity.wait_random_exponential(10, max_wait)
475
478
  for _ in range(1000):
476
- self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0.00, 5.00)
479
+ self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0.00, max_wait)
480
+
481
+ # min wait
482
+ min_wait = 5
483
+ fn = tenacity.wait_random_exponential(min=min_wait)
484
+ for _ in range(1000):
485
+ self._assert_inclusive_range(fn(make_retry_state(1, 0)), min_wait, 5)
477
486
 
478
487
  # Default arguments exist
479
488
  fn = tenacity.wait_random_exponential()
@@ -1073,7 +1082,7 @@ class TestDecoratorWrapper(unittest.TestCase):
1073
1082
  _retryable_test_with_unless_exception_type_name(NameErrorUntilCount(5))
1074
1083
  )
1075
1084
  except NameError as e:
1076
- s = _retryable_test_with_unless_exception_type_name.retry.statistics
1085
+ s = _retryable_test_with_unless_exception_type_name.statistics
1077
1086
  self.assertTrue(s["attempt_number"] == 6)
1078
1087
  print(e)
1079
1088
  else:
@@ -1088,7 +1097,7 @@ class TestDecoratorWrapper(unittest.TestCase):
1088
1097
  )
1089
1098
  )
1090
1099
  except NameError as e:
1091
- s = _retryable_test_with_unless_exception_type_no_input.retry.statistics
1100
+ s = _retryable_test_with_unless_exception_type_no_input.statistics
1092
1101
  self.assertTrue(s["attempt_number"] == 6)
1093
1102
  print(e)
1094
1103
  else:
@@ -1111,7 +1120,7 @@ class TestDecoratorWrapper(unittest.TestCase):
1111
1120
  _retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3))
1112
1121
  )
1113
1122
  except CustomError:
1114
- print(_retryable_test_if_exception_message_message.retry.statistics)
1123
+ print(_retryable_test_if_exception_message_message.statistics)
1115
1124
  self.fail("CustomError should've been retried from errormessage")
1116
1125
 
1117
1126
  def test_retry_if_not_exception_message(self):
@@ -1122,7 +1131,7 @@ class TestDecoratorWrapper(unittest.TestCase):
1122
1131
  )
1123
1132
  )
1124
1133
  except CustomError:
1125
- s = _retryable_test_if_not_exception_message_message.retry.statistics
1134
+ s = _retryable_test_if_not_exception_message_message.statistics
1126
1135
  self.assertTrue(s["attempt_number"] == 1)
1127
1136
 
1128
1137
  def test_retry_if_not_exception_message_delay(self):
@@ -1131,7 +1140,7 @@ class TestDecoratorWrapper(unittest.TestCase):
1131
1140
  _retryable_test_not_exception_message_delay(NameErrorUntilCount(3))
1132
1141
  )
1133
1142
  except NameError:
1134
- s = _retryable_test_not_exception_message_delay.retry.statistics
1143
+ s = _retryable_test_not_exception_message_delay.statistics
1135
1144
  print(s["attempt_number"])
1136
1145
  self.assertTrue(s["attempt_number"] == 4)
1137
1146
 
@@ -1151,7 +1160,7 @@ class TestDecoratorWrapper(unittest.TestCase):
1151
1160
  )
1152
1161
  )
1153
1162
  except CustomError:
1154
- s = _retryable_test_if_not_exception_message_message.retry.statistics
1163
+ s = _retryable_test_if_not_exception_message_message.statistics
1155
1164
  self.assertTrue(s["attempt_number"] == 1)
1156
1165
 
1157
1166
  def test_retry_if_exception_cause_type(self):
@@ -1209,6 +1218,43 @@ class TestDecoratorWrapper(unittest.TestCase):
1209
1218
  h = retrying.wraps(Hello())
1210
1219
  self.assertEqual(h(), "Hello")
1211
1220
 
1221
+ def test_retry_function_attributes(self):
1222
+ """Test that the wrapped function attributes are exposed as intended.
1223
+
1224
+ - statistics contains the value for the latest function run
1225
+ - retry object can be modified to change its behaviour (useful to patch in tests)
1226
+ - retry object statistics do not contain valid information
1227
+ """
1228
+
1229
+ self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2)))
1230
+
1231
+ expected_stats = {
1232
+ "attempt_number": 3,
1233
+ "delay_since_first_attempt": mock.ANY,
1234
+ "idle_for": mock.ANY,
1235
+ "start_time": mock.ANY,
1236
+ }
1237
+ self.assertEqual(_retryable_test_with_stop.statistics, expected_stats)
1238
+ self.assertEqual(_retryable_test_with_stop.retry.statistics, {})
1239
+
1240
+ with mock.patch.object(
1241
+ _retryable_test_with_stop.retry, "stop", tenacity.stop_after_attempt(1)
1242
+ ):
1243
+ try:
1244
+ self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2)))
1245
+ except RetryError as exc:
1246
+ expected_stats = {
1247
+ "attempt_number": 1,
1248
+ "delay_since_first_attempt": mock.ANY,
1249
+ "idle_for": mock.ANY,
1250
+ "start_time": mock.ANY,
1251
+ }
1252
+ self.assertEqual(_retryable_test_with_stop.statistics, expected_stats)
1253
+ self.assertEqual(exc.last_attempt.attempt_number, 1)
1254
+ self.assertEqual(_retryable_test_with_stop.retry.statistics, {})
1255
+ else:
1256
+ self.fail("RetryError should have been raised after 1 attempt")
1257
+
1212
1258
 
1213
1259
  class TestRetryWith:
1214
1260
  def test_redefine_wait(self):
@@ -1479,21 +1525,21 @@ class TestStatistics(unittest.TestCase):
1479
1525
  def _foobar():
1480
1526
  return 42
1481
1527
 
1482
- self.assertEqual({}, _foobar.retry.statistics)
1528
+ self.assertEqual({}, _foobar.statistics)
1483
1529
  _foobar()
1484
- self.assertEqual(1, _foobar.retry.statistics["attempt_number"])
1530
+ self.assertEqual(1, _foobar.statistics["attempt_number"])
1485
1531
 
1486
1532
  def test_stats_failing(self):
1487
1533
  @retry(stop=tenacity.stop_after_attempt(2))
1488
1534
  def _foobar():
1489
1535
  raise ValueError(42)
1490
1536
 
1491
- self.assertEqual({}, _foobar.retry.statistics)
1537
+ self.assertEqual({}, _foobar.statistics)
1492
1538
  try:
1493
1539
  _foobar()
1494
1540
  except Exception: # noqa: B902
1495
1541
  pass
1496
- self.assertEqual(2, _foobar.retry.statistics["attempt_number"])
1542
+ self.assertEqual(2, _foobar.statistics["attempt_number"])
1497
1543
 
1498
1544
 
1499
1545
  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
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