tenacity 9.1.2__tar.gz → 9.1.3__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 (89) hide show
  1. {tenacity-9.1.2 → tenacity-9.1.3}/.github/workflows/ci.yaml +8 -8
  2. {tenacity-9.1.2 → tenacity-9.1.3}/.github/workflows/release.yml +3 -3
  3. tenacity-9.1.3/.mergify.yml +21 -0
  4. {tenacity-9.1.2/tenacity.egg-info → tenacity-9.1.3}/PKG-INFO +3 -3
  5. {tenacity-9.1.2 → tenacity-9.1.3}/README.rst +0 -14
  6. {tenacity-9.1.2 → tenacity-9.1.3}/doc/source/index.rst +0 -14
  7. {tenacity-9.1.2 → tenacity-9.1.3}/pyproject.toml +1 -1
  8. tenacity-9.1.3/releasenotes/notes/async-sleep-retrying-32de5866f5d041.yaml +7 -0
  9. tenacity-9.1.3/releasenotes/notes/drop-python-3.9-ecfa2d7db9773e96.yaml +5 -0
  10. tenacity-9.1.3/releasenotes/notes/logging-protocol-a4cf0f786f21e4ee.yaml +5 -0
  11. tenacity-9.1.3/releasenotes/notes/support-py3.14-14928188cab53b99.yaml +3 -0
  12. {tenacity-9.1.2 → tenacity-9.1.3}/setup.cfg +2 -2
  13. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/__init__.py +35 -23
  14. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/_utils.py +12 -0
  15. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/after.py +2 -4
  16. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/asyncio/__init__.py +5 -1
  17. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/before.py +1 -3
  18. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/before_sleep.py +3 -4
  19. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/tornadoweb.py +1 -1
  20. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/wait.py +42 -3
  21. {tenacity-9.1.2 → tenacity-9.1.3/tenacity.egg-info}/PKG-INFO +3 -3
  22. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity.egg-info/SOURCES.txt +4 -0
  23. {tenacity-9.1.2 → tenacity-9.1.3}/tests/test_after.py +1 -1
  24. {tenacity-9.1.2 → tenacity-9.1.3}/tests/test_asyncio.py +27 -3
  25. {tenacity-9.1.2 → tenacity-9.1.3}/tests/test_issue_478.py +1 -2
  26. {tenacity-9.1.2 → tenacity-9.1.3}/tests/test_tenacity.py +19 -10
  27. {tenacity-9.1.2 → tenacity-9.1.3}/tox.ini +4 -4
  28. tenacity-9.1.2/.mergify.yml +0 -41
  29. {tenacity-9.1.2 → tenacity-9.1.3}/.editorconfig +0 -0
  30. {tenacity-9.1.2 → tenacity-9.1.3}/.github/dependabot.yml +0 -0
  31. {tenacity-9.1.2 → tenacity-9.1.3}/.gitignore +0 -0
  32. {tenacity-9.1.2 → tenacity-9.1.3}/.readthedocs.yml +0 -0
  33. {tenacity-9.1.2 → tenacity-9.1.3}/LICENSE +0 -0
  34. {tenacity-9.1.2 → tenacity-9.1.3}/doc/source/api.rst +0 -0
  35. {tenacity-9.1.2 → tenacity-9.1.3}/doc/source/changelog.rst +0 -0
  36. {tenacity-9.1.2 → tenacity-9.1.3}/doc/source/conf.py +0 -0
  37. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml +0 -0
  38. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml +0 -0
  39. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/add-async-actions-b249c527d99723bb.yaml +0 -0
  40. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/add-re-pattern-to-match-types-6a4c1d9e64e2a5e1.yaml +0 -0
  41. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/add-reno-d1ab5710f272650a.yaml +0 -0
  42. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml +0 -0
  43. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml +0 -0
  44. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/add-test-extra-55e869261b03e56d.yaml +0 -0
  45. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml +0 -0
  46. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml +0 -0
  47. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml +0 -0
  48. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/after_log-50f4d73b24ce9203.yaml +0 -0
  49. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml +0 -0
  50. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/annotate_code-197b93130df14042.yaml +0 -0
  51. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml +0 -0
  52. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml +0 -0
  53. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml +0 -0
  54. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml +0 -0
  55. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml +0 -0
  56. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml +0 -0
  57. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml +0 -0
  58. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml +0 -0
  59. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml +0 -0
  60. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml +0 -0
  61. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml +0 -0
  62. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml +0 -0
  63. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml +0 -0
  64. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml +0 -0
  65. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml +0 -0
  66. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/pr320-py3-only-wheel-tag.yaml +0 -0
  67. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml +0 -0
  68. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/remove-py36-876c0416cf279d15.yaml +0 -0
  69. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml +0 -0
  70. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml +0 -0
  71. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml +0 -0
  72. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml +0 -0
  73. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml +0 -0
  74. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml +0 -0
  75. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml +0 -0
  76. {tenacity-9.1.2 → tenacity-9.1.3}/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml +0 -0
  77. {tenacity-9.1.2 → tenacity-9.1.3}/reno.yaml +0 -0
  78. {tenacity-9.1.2 → tenacity-9.1.3}/setup.py +0 -0
  79. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/asyncio/retry.py +0 -0
  80. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/nap.py +0 -0
  81. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/py.typed +0 -0
  82. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/retry.py +0 -0
  83. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity/stop.py +0 -0
  84. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity.egg-info/dependency_links.txt +0 -0
  85. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity.egg-info/requires.txt +0 -0
  86. {tenacity-9.1.2 → tenacity-9.1.3}/tenacity.egg-info/top_level.txt +0 -0
  87. {tenacity-9.1.2 → tenacity-9.1.3}/tests/__init__.py +0 -0
  88. {tenacity-9.1.2 → tenacity-9.1.3}/tests/test_tornado.py +0 -0
  89. {tenacity-9.1.2 → tenacity-9.1.3}/tests/test_utils.py +0 -0
@@ -18,28 +18,28 @@ jobs:
18
18
  strategy:
19
19
  matrix:
20
20
  include:
21
- - python: "3.9"
22
- tox: py39
23
21
  - python: "3.10"
24
22
  tox: py310
25
23
  - python: "3.11"
26
24
  tox: py311
27
25
  - python: "3.12"
28
26
  tox: py312
29
- - python: "3.12"
30
- tox: pep8
31
27
  - python: "3.13"
32
- tox: py313,py313-trio
33
- - python: "3.11"
28
+ tox: py313
29
+ - python: "3.14"
30
+ tox: py314,py314-trio
31
+ - python: "3.14"
32
+ tox: pep8
33
+ - python: "3.14"
34
34
  tox: mypy
35
35
  steps:
36
36
  - name: Checkout 🛎️
37
- uses: actions/checkout@v4.2.2
37
+ uses: actions/checkout@v6.0.2
38
38
  with:
39
39
  fetch-depth: 0
40
40
 
41
41
  - name: Setup Python 🔧
42
- uses: actions/setup-python@v5.5.0
42
+ uses: actions/setup-python@v6.2.0
43
43
  with:
44
44
  python-version: ${{ matrix.python }}
45
45
  allow-prereleases: true
@@ -13,14 +13,14 @@ jobs:
13
13
  id-token: write
14
14
  contents: write
15
15
  steps:
16
- - uses: actions/checkout@v4.2.2
16
+ - uses: actions/checkout@v6.0.2
17
17
  with:
18
18
  fetch-depth: 0
19
19
  fetch-tags: true
20
20
 
21
- - uses: actions/setup-python@v5.5.0
21
+ - uses: actions/setup-python@v6.2.0
22
22
  with:
23
- python-version: 3.13
23
+ python-version: 3.14
24
24
 
25
25
  - name: Install build
26
26
  run: |
@@ -0,0 +1,21 @@
1
+ queue_rules:
2
+ - name: default
3
+ merge_method: squash
4
+ autoqueue: true
5
+ queue_conditions:
6
+ - or:
7
+ - author = jd
8
+ - "#approved-reviews-by >= 1"
9
+ - author = dependabot[bot]
10
+ - "check-success=test (3.10, py310)"
11
+ - "check-success=test (3.11, py311)"
12
+ - "check-success=test (3.12, py312)"
13
+ - "check-success=test (3.13, py313)"
14
+ - "check-success=test (3.14, py314,py314-trio)"
15
+ - "check-success=test (3.14, pep8)"
16
+
17
+ pull_request_rules:
18
+ - name: dismiss reviews
19
+ conditions: []
20
+ actions:
21
+ dismiss_reviews: {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tenacity
3
- Version: 9.1.2
3
+ Version: 9.1.3
4
4
  Summary: Retry code until it succeeds
5
5
  Home-page: https://github.com/jd/tenacity
6
6
  Author: Julien Danjou
@@ -11,13 +11,13 @@ Classifier: License :: OSI Approved :: Apache Software License
11
11
  Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
14
- Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Utilities
20
- Requires-Python: >=3.9
20
+ Requires-Python: >=3.10
21
21
  License-File: LICENSE
22
22
  Provides-Extra: doc
23
23
  Requires-Dist: reno; extra == "doc"
@@ -637,17 +637,3 @@ Contribute
637
637
  #. Make the docs better (or more detailed, or more easier to read, or ...)
638
638
 
639
639
  .. _`the repository`: https://github.com/jd/tenacity
640
-
641
- Changelogs
642
- ~~~~~~~~~~
643
-
644
- `reno`_ is used for managing changelogs. Take a look at their usage docs.
645
-
646
- The doc generation will automatically compile the changelogs. You just need to add them.
647
-
648
- .. code-block:: sh
649
-
650
- # Opens a template file in an editor
651
- tox -e reno -- new some-slug-for-my-change --edit
652
-
653
- .. _`reno`: https://docs.openstack.org/reno/latest/user/usage.html
@@ -637,17 +637,3 @@ Contribute
637
637
  #. Make the docs better (or more detailed, or more easier to read, or ...)
638
638
 
639
639
  .. _`the repository`: https://github.com/jd/tenacity
640
-
641
- Changelogs
642
- ~~~~~~~~~~
643
-
644
- `reno`_ is used for managing changelogs. Take a look at their usage docs.
645
-
646
- The doc generation will automatically compile the changelogs. You just need to add them.
647
-
648
- .. code-block:: sh
649
-
650
- # Opens a template file in an editor
651
- tox -e reno -- new some-slug-for-my-change --edit
652
-
653
- .. _`reno`: https://docs.openstack.org/reno/latest/user/usage.html
@@ -11,7 +11,7 @@ build-backend="setuptools.build_meta"
11
11
  [tool.ruff]
12
12
  line-length = 88
13
13
  indent-width = 4
14
- target-version = "py39"
14
+ target-version = "py310"
15
15
 
16
16
  [tool.mypy]
17
17
  strict = true
@@ -0,0 +1,7 @@
1
+ ---
2
+ fixes:
3
+ - |
4
+ Passing an async ``sleep`` callable (e.g. ``trio.sleep``) to ``@retry``
5
+ now correctly uses ``AsyncRetrying``, even when the decorated function is
6
+ synchronous. Previously, the async sleep would silently not be awaited,
7
+ resulting in no delay between retries.
@@ -0,0 +1,5 @@
1
+ ---
2
+ upgrade:
3
+ - |
4
+ Python 3.9 has reached end-of-life and is no longer supported.
5
+ The minimum supported version is now Python 3.10.
@@ -0,0 +1,5 @@
1
+ ---
2
+ other:
3
+ - |
4
+ Accept non-standard logger in helpers logging something (eg: structlog, loguru...)
5
+
@@ -0,0 +1,3 @@
1
+ ---
2
+ features:
3
+ - Python 3.14 support has been added.
@@ -13,16 +13,16 @@ classifier =
13
13
  Programming Language :: Python
14
14
  Programming Language :: Python :: 3
15
15
  Programming Language :: Python :: 3 :: Only
16
- Programming Language :: Python :: 3.9
17
16
  Programming Language :: Python :: 3.10
18
17
  Programming Language :: Python :: 3.11
19
18
  Programming Language :: Python :: 3.12
20
19
  Programming Language :: Python :: 3.13
20
+ Programming Language :: Python :: 3.14
21
21
  Topic :: Utilities
22
22
 
23
23
  [options]
24
24
  install_requires =
25
- python_requires = >=3.9
25
+ python_requires = >=3.10
26
26
  packages = find:
27
27
 
28
28
  [options.packages.find]
@@ -59,6 +59,7 @@ from .stop import stop_when_event_set # noqa
59
59
  # Import all built-in wait strategies for easier usage.
60
60
  from .wait import wait_chain # noqa
61
61
  from .wait import wait_combine # noqa
62
+ from .wait import wait_exception # noqa
62
63
  from .wait import wait_exponential # noqa
63
64
  from .wait import wait_fixed # noqa
64
65
  from .wait import wait_incrementing # noqa
@@ -98,14 +99,11 @@ if t.TYPE_CHECKING:
98
99
 
99
100
  WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
100
101
  WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any])
102
+ P = t.ParamSpec("P")
103
+ R = t.TypeVar("R")
101
104
 
102
105
 
103
- dataclass_kwargs = {}
104
- if sys.version_info >= (3, 10):
105
- dataclass_kwargs.update({"slots": True})
106
-
107
-
108
- @dataclasses.dataclass(**dataclass_kwargs)
106
+ @dataclasses.dataclass(slots=True)
109
107
  class IterState:
110
108
  actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field(
111
109
  default_factory=list
@@ -307,19 +305,15 @@ class BaseRetrying(ABC):
307
305
  future we may provide a way to aggregate the various
308
306
  statistics from each thread).
309
307
  """
310
- try:
311
- return self._local.statistics # type: ignore[no-any-return]
312
- except AttributeError:
308
+ if not hasattr(self._local, "statistics"):
313
309
  self._local.statistics = t.cast(t.Dict[str, t.Any], {})
314
- return self._local.statistics
310
+ return self._local.statistics # type: ignore[no-any-return]
315
311
 
316
312
  @property
317
313
  def iter_state(self) -> IterState:
318
- try:
319
- return self._local.iter_state # type: ignore[no-any-return]
320
- except AttributeError:
314
+ if not hasattr(self._local, "iter_state"):
321
315
  self._local.iter_state = IterState()
322
- return self._local.iter_state
316
+ return self._local.iter_state # type: ignore[no-any-return]
323
317
 
324
318
  def wraps(self, f: WrappedFn) -> WrappedFn:
325
319
  """Wrap a function for retrying.
@@ -489,13 +483,7 @@ class Retrying(BaseRetrying):
489
483
  return do # type: ignore[no-any-return]
490
484
 
491
485
 
492
- if sys.version_info >= (3, 9):
493
- FutureGenericT = futures.Future[t.Any]
494
- else:
495
- FutureGenericT = futures.Future
496
-
497
-
498
- class Future(FutureGenericT):
486
+ class Future(futures.Future[t.Any]):
499
487
  """Encapsulates a (future or past) attempted call to a target function."""
500
488
 
501
489
  def __init__(self, attempt_number: int) -> None:
@@ -603,7 +591,27 @@ def retry(func: WrappedFn) -> WrappedFn: ...
603
591
 
604
592
  @t.overload
605
593
  def retry(
606
- sleep: t.Callable[[t.Union[int, float]], t.Union[None, t.Awaitable[None]]] = sleep,
594
+ *,
595
+ sleep: t.Callable[[t.Union[int, float]], t.Awaitable[None]],
596
+ stop: "StopBaseT" = ...,
597
+ wait: "WaitBaseT" = ...,
598
+ retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = ...,
599
+ before: t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] = ...,
600
+ after: t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] = ...,
601
+ before_sleep: t.Optional[
602
+ t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]]
603
+ ] = ...,
604
+ reraise: bool = ...,
605
+ retry_error_cls: t.Type["RetryError"] = ...,
606
+ retry_error_callback: t.Optional[
607
+ t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]]
608
+ ] = ...,
609
+ ) -> t.Callable[[t.Callable[P, R | t.Awaitable[R]]], t.Callable[P, t.Awaitable[R]]]: ...
610
+
611
+
612
+ @t.overload
613
+ def retry(
614
+ sleep: t.Callable[[t.Union[int, float]], None] = sleep,
607
615
  stop: "StopBaseT" = stop_never,
608
616
  wait: "WaitBaseT" = wait_none(),
609
617
  retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = retry_if_exception_type(),
@@ -642,7 +650,10 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
642
650
  f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)"
643
651
  )
644
652
  r: "BaseRetrying"
645
- if _utils.is_coroutine_callable(f):
653
+ sleep = dkw.get("sleep")
654
+ if _utils.is_coroutine_callable(f) or (
655
+ sleep is not None and _utils.is_coroutine_callable(sleep)
656
+ ):
646
657
  r = AsyncRetrying(*dargs, **dkw)
647
658
  elif (
648
659
  tornado
@@ -690,6 +701,7 @@ __all__ = [
690
701
  "stop_when_event_set",
691
702
  "wait_chain",
692
703
  "wait_combine",
704
+ "wait_exception",
693
705
  "wait_exponential",
694
706
  "wait_fixed",
695
707
  "wait_incrementing",
@@ -25,6 +25,18 @@ from datetime import timedelta
25
25
  MAX_WAIT = sys.maxsize / 2
26
26
 
27
27
 
28
+ class LoggerProtocol(typing.Protocol):
29
+ """
30
+ Protocol used by utils expecting a logger (eg: before_log).
31
+
32
+ Compatible with logging, structlog, loguru, etc...
33
+ """
34
+
35
+ def log(
36
+ self, level: int, msg: str, /, *args: typing.Any, **kwargs: typing.Any
37
+ ) -> typing.Any: ...
38
+
39
+
28
40
  def find_ordinal(pos_num: int) -> str:
29
41
  # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers
30
42
  if pos_num == 0:
@@ -19,8 +19,6 @@ import typing
19
19
  from tenacity import _utils
20
20
 
21
21
  if typing.TYPE_CHECKING:
22
- import logging
23
-
24
22
  from tenacity import RetryCallState
25
23
 
26
24
 
@@ -29,9 +27,9 @@ def after_nothing(retry_state: "RetryCallState") -> None:
29
27
 
30
28
 
31
29
  def after_log(
32
- logger: "logging.Logger",
30
+ logger: _utils.LoggerProtocol,
33
31
  log_level: int,
34
- sec_format: str = "%0.3f",
32
+ sec_format: str = "%.3g",
35
33
  ) -> typing.Callable[["RetryCallState"], None]:
36
34
  """After call strategy that logs to some logger the finished attempt."""
37
35
 
@@ -107,11 +107,15 @@ class AsyncRetrying(BaseRetrying):
107
107
  self.begin()
108
108
 
109
109
  retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs)
110
+ is_async = _utils.is_coroutine_callable(fn)
110
111
  while True:
111
112
  do = await self.iter(retry_state=retry_state)
112
113
  if isinstance(do, DoAttempt):
113
114
  try:
114
- result = await fn(*args, **kwargs)
115
+ if is_async:
116
+ result = await fn(*args, **kwargs)
117
+ else:
118
+ result = fn(*args, **kwargs)
115
119
  except BaseException: # noqa: B902
116
120
  retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type]
117
121
  else:
@@ -19,8 +19,6 @@ import typing
19
19
  from tenacity import _utils
20
20
 
21
21
  if typing.TYPE_CHECKING:
22
- import logging
23
-
24
22
  from tenacity import RetryCallState
25
23
 
26
24
 
@@ -29,7 +27,7 @@ def before_nothing(retry_state: "RetryCallState") -> None:
29
27
 
30
28
 
31
29
  def before_log(
32
- logger: "logging.Logger", log_level: int
30
+ logger: _utils.LoggerProtocol, log_level: int
33
31
  ) -> typing.Callable[["RetryCallState"], None]:
34
32
  """Before call strategy that logs to some logger the attempt."""
35
33
 
@@ -19,8 +19,6 @@ import typing
19
19
  from tenacity import _utils
20
20
 
21
21
  if typing.TYPE_CHECKING:
22
- import logging
23
-
24
22
  from tenacity import RetryCallState
25
23
 
26
24
 
@@ -29,9 +27,10 @@ def before_sleep_nothing(retry_state: "RetryCallState") -> None:
29
27
 
30
28
 
31
29
  def before_sleep_log(
32
- logger: "logging.Logger",
30
+ logger: _utils.LoggerProtocol,
33
31
  log_level: int,
34
32
  exc_info: bool = False,
33
+ sec_format: str = "%.3g",
35
34
  ) -> typing.Callable[["RetryCallState"], None]:
36
35
  """Before sleep strategy that logs to some logger the attempt."""
37
36
 
@@ -65,7 +64,7 @@ def before_sleep_log(
65
64
  logger.log(
66
65
  log_level,
67
66
  f"Retrying {fn_name} "
68
- f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.",
67
+ f"in {sec_format % retry_state.next_action.sleep} seconds as it {verb} {value}.",
69
68
  exc_info=local_exc_info,
70
69
  )
71
70
 
@@ -37,7 +37,7 @@ class TornadoRetrying(BaseRetrying):
37
37
  super().__init__(**kwargs)
38
38
  self.sleep = sleep
39
39
 
40
- @gen.coroutine # type: ignore[misc]
40
+ @gen.coroutine # type: ignore[untyped-decorator]
41
41
  def __call__(
42
42
  self,
43
43
  fn: "typing.Callable[..., typing.Union[typing.Generator[typing.Any, typing.Any, _RetValT], Future[_RetValT]]]",
@@ -98,10 +98,10 @@ class wait_chain(wait_base):
98
98
 
99
99
  @retry(wait=wait_chain(*[wait_fixed(1) for i in range(3)] +
100
100
  [wait_fixed(2) for j in range(5)] +
101
- [wait_fixed(5) for k in range(4)))
101
+ [wait_fixed(5) for k in range(4)]))
102
102
  def wait_chained():
103
- print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s
104
- thereafter.")
103
+ print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s "
104
+ "thereafter.")
105
105
  """
106
106
 
107
107
  def __init__(self, *strategies: wait_base) -> None:
@@ -113,6 +113,45 @@ class wait_chain(wait_base):
113
113
  return wait_func(retry_state=retry_state)
114
114
 
115
115
 
116
+ class wait_exception(wait_base):
117
+ """Wait strategy that waits the amount of time returned by the predicate.
118
+
119
+ The predicate is passed the exception object. Based on the exception, the
120
+ user can decide how much time to wait before retrying.
121
+
122
+ For example::
123
+
124
+ def http_error(exception: BaseException) -> float:
125
+ if (
126
+ isinstance(exception, requests.HTTPError)
127
+ and exception.response.status_code == requests.codes.too_many_requests
128
+ ):
129
+ return float(exception.response.headers.get("Retry-After", "1"))
130
+ return 60.0
131
+
132
+
133
+ @retry(
134
+ stop=stop_after_attempt(3),
135
+ wait=wait_exception(http_error),
136
+ )
137
+ def http_get_request(url: str) -> None:
138
+ response = requests.get(url)
139
+ response.raise_for_status()
140
+ """
141
+
142
+ def __init__(self, predicate: typing.Callable[[BaseException], float]) -> None:
143
+ self.predicate = predicate
144
+
145
+ def __call__(self, retry_state: "RetryCallState") -> float:
146
+ if retry_state.outcome is None:
147
+ raise RuntimeError("__call__() called before outcome was set")
148
+
149
+ exception = retry_state.outcome.exception()
150
+ if exception is None:
151
+ raise RuntimeError("outcome failed but the exception is None")
152
+ return self.predicate(exception)
153
+
154
+
116
155
  class wait_incrementing(wait_base):
117
156
  """Wait an incremental amount of time after each attempt.
118
157
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tenacity
3
- Version: 9.1.2
3
+ Version: 9.1.3
4
4
  Summary: Retry code until it succeeds
5
5
  Home-page: https://github.com/jd/tenacity
6
6
  Author: Julien Danjou
@@ -11,13 +11,13 @@ Classifier: License :: OSI Approved :: Apache Software License
11
11
  Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
14
- Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Utilities
20
- Requires-Python: >=3.9
20
+ Requires-Python: >=3.10
21
21
  License-File: LICENSE
22
22
  Provides-Extra: doc
23
23
  Requires-Dist: reno; extra == "doc"
@@ -30,11 +30,13 @@ releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml
30
30
  releasenotes/notes/after_log-50f4d73b24ce9203.yaml
31
31
  releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml
32
32
  releasenotes/notes/annotate_code-197b93130df14042.yaml
33
+ releasenotes/notes/async-sleep-retrying-32de5866f5d041.yaml
33
34
  releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml
34
35
  releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml
35
36
  releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml
36
37
  releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml
37
38
  releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml
39
+ releasenotes/notes/drop-python-3.9-ecfa2d7db9773e96.yaml
38
40
  releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml
39
41
  releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml
40
42
  releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml
@@ -43,6 +45,7 @@ releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml
43
45
  releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml
44
46
  releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml
45
47
  releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml
48
+ releasenotes/notes/logging-protocol-a4cf0f786f21e4ee.yaml
46
49
  releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml
47
50
  releasenotes/notes/no-async-iter-6132a42e52348a75.yaml
48
51
  releasenotes/notes/pr320-py3-only-wheel-tag.yaml
@@ -51,6 +54,7 @@ releasenotes/notes/remove-py36-876c0416cf279d15.yaml
51
54
  releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml
52
55
  releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml
53
56
  releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml
57
+ releasenotes/notes/support-py3.14-14928188cab53b99.yaml
54
58
  releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml
55
59
  releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml
56
60
  releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml
@@ -27,7 +27,7 @@ class TestAfterLogFormat(unittest.TestCase):
27
27
  log = unittest.mock.MagicMock(spec="logging.Logger.log")
28
28
  logger = unittest.mock.MagicMock(spec="logging.Logger", log=log)
29
29
 
30
- sec_format = "%0.3f"
30
+ sec_format = "%.3g"
31
31
  delay_since_first_attempt = 0.1
32
32
 
33
33
  retry_state = test_tenacity.make_retry_state(
@@ -34,14 +34,17 @@ from tenacity import asyncio as tasyncio
34
34
  from tenacity import retry, retry_if_exception, retry_if_result, stop_after_attempt
35
35
  from tenacity.wait import wait_fixed
36
36
 
37
- from .test_tenacity import NoIOErrorAfterCount, current_time_ms
37
+ from .test_tenacity import (
38
+ NoIOErrorAfterCount,
39
+ NoneReturnUntilAfterCount,
40
+ current_time_ms,
41
+ )
38
42
 
39
43
 
40
44
  def asynctest(callable_):
41
45
  @wraps(callable_)
42
46
  def wrapper(*a, **kw):
43
- loop = asyncio.get_event_loop()
44
- return loop.run_until_complete(callable_(*a, **kw))
47
+ return asyncio.run(callable_(*a, **kw))
45
48
 
46
49
  return wrapper
47
50
 
@@ -464,5 +467,26 @@ async def foo():
464
467
  pass
465
468
 
466
469
 
470
+ class TestSyncFunctionWithAsyncSleep(unittest.TestCase):
471
+ @asynctest
472
+ async def test_sync_function_with_async_sleep(self):
473
+ """A sync function with an async sleep callable uses AsyncRetrying."""
474
+ mock_sleep = mock.AsyncMock()
475
+
476
+ thing = NoneReturnUntilAfterCount(2)
477
+
478
+ @retry(
479
+ sleep=mock_sleep,
480
+ wait=wait_fixed(1),
481
+ retry=retry_if_result(lambda x: x is None),
482
+ )
483
+ def sync_function():
484
+ return thing.go()
485
+
486
+ result = await sync_function()
487
+ assert result is True
488
+ assert mock_sleep.await_count == 2
489
+
490
+
467
491
  if __name__ == "__main__":
468
492
  unittest.main()
@@ -12,8 +12,7 @@ def asynctest(
12
12
  ) -> typing.Callable[..., typing.Any]:
13
13
  @wraps(callable_)
14
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))
15
+ return asyncio.run(callable_(*a, **kw))
17
16
 
18
17
  return wrapper
19
18
 
@@ -17,7 +17,6 @@
17
17
  import datetime
18
18
  import logging
19
19
  import re
20
- import sys
21
20
  import time
22
21
  import typing
23
22
  import unittest
@@ -28,6 +27,7 @@ from fractions import Fraction
28
27
  from unittest import mock
29
28
 
30
29
  import pytest
30
+ from typeguard import check_type
31
31
 
32
32
  import tenacity
33
33
  from tenacity import RetryCallState, RetryError, Retrying, retry
@@ -369,6 +369,24 @@ class TestWaitConditions(unittest.TestCase):
369
369
  self.assertLess(w, 8)
370
370
  self.assertGreaterEqual(w, 5)
371
371
 
372
+ def test_wait_exception(self):
373
+ def predicate(exc):
374
+ if isinstance(exc, ValueError):
375
+ return 3.5
376
+ return 10.0
377
+
378
+ r = Retrying(wait=tenacity.wait_exception(predicate))
379
+
380
+ fut1 = tenacity.Future.construct(1, ValueError(), True)
381
+ self.assertEqual(r.wait(make_retry_state(1, 0, last_result=fut1)), 3.5)
382
+
383
+ fut2 = tenacity.Future.construct(1, KeyError(), True)
384
+ self.assertEqual(r.wait(make_retry_state(1, 0, last_result=fut2)), 10.0)
385
+
386
+ fut3 = tenacity.Future.construct(1, None, False)
387
+ with self.assertRaises(RuntimeError):
388
+ r.wait(make_retry_state(1, 0, last_result=fut3))
389
+
372
390
  def test_wait_double_sum(self):
373
391
  r = Retrying(wait=tenacity.wait_random(0, 3) + tenacity.wait_fixed(5))
374
392
  # Test it a few time since it's random
@@ -1711,17 +1729,8 @@ class TestRetryException(unittest.TestCase):
1711
1729
 
1712
1730
 
1713
1731
  class TestRetryTyping(unittest.TestCase):
1714
- @pytest.mark.skipif(
1715
- sys.version_info < (3, 0), reason="typeguard not supported for python 2"
1716
- )
1717
1732
  def test_retry_type_annotations(self):
1718
1733
  """The decorator should maintain types of decorated functions."""
1719
- # Just in case this is run with unit-test, return early for py2
1720
- if sys.version_info < (3, 0):
1721
- return
1722
-
1723
- # Function-level import because we can't install this for python 2.
1724
- from typeguard import check_type
1725
1734
 
1726
1735
  def num_to_str(number):
1727
1736
  # type: (int) -> str
@@ -1,6 +1,6 @@
1
1
  [tox]
2
2
  # we only test trio on latest python version
3
- envlist = py3{9,10,11,12,13,13-trio}, pep8, pypy3
3
+ envlist = py3{10,11,12,13,14,14-trio}, pep8, pypy3
4
4
  skip_missing_interpreters = True
5
5
 
6
6
  [testenv]
@@ -11,9 +11,9 @@ deps =
11
11
  .[doc]
12
12
  trio: trio
13
13
  commands =
14
- py3{8,9,10,11,12,13},pypy3: pytest {posargs}
15
- py3{8,9,10,11,12,13},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build
16
- py3{8,9,10,11,12,13},pypy3: sphinx-build -a -E -W -b html doc/source doc/build
14
+ py3{10,11,12,13,14},pypy3: pytest {posargs}
15
+ py3{10,11,12,13,14},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build
16
+ py3{10,11,12,13,14},pypy3: sphinx-build -a -E -W -b html doc/source doc/build
17
17
 
18
18
  [testenv:pep8]
19
19
  basepython = python3
@@ -1,41 +0,0 @@
1
- queue_rules:
2
- - name: default
3
- merge_method: squash
4
- queue_conditions:
5
- - or:
6
- - author = jd
7
- - "#approved-reviews-by >= 1"
8
- - author = dependabot[bot]
9
- - or:
10
- - files ~= ^releasenotes/notes/
11
- - label = no-changelog
12
- - author = dependabot[bot]
13
- - "check-success=test (3.9, py39)"
14
- - "check-success=test (3.10, py310)"
15
- - "check-success=test (3.11, py311)"
16
- - "check-success=test (3.12, py312)"
17
- - "check-success=test (3.13, py313,py313-trio)"
18
- - "check-success=test (3.12, pep8)"
19
-
20
- pull_request_rules:
21
- - name: warn on no changelog
22
- conditions:
23
- - -files~=^releasenotes/notes/
24
- - label!=no-changelog
25
- - -closed
26
- actions:
27
- comment:
28
- message: >
29
- ⚠️ No release notes detected. Please make sure to use
30
- [reno](https://docs.openstack.org/reno/latest/user/usage.html) to add
31
- a changelog entry.
32
-
33
- - name: automatic queue
34
- conditions: []
35
- actions:
36
- queue:
37
-
38
- - name: dismiss reviews
39
- conditions: []
40
- actions:
41
- dismiss_reviews: {}
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