tenacity 8.3.0__tar.gz → 8.4.1__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 (79) hide show
  1. {tenacity-8.3.0 → tenacity-8.4.1}/.github/workflows/ci.yaml +3 -3
  2. {tenacity-8.3.0 → tenacity-8.4.1}/.github/workflows/deploy.yaml +2 -2
  3. {tenacity-8.3.0 → tenacity-8.4.1}/.mergify.yml +1 -1
  4. {tenacity-8.3.0/tenacity.egg-info → tenacity-8.4.1}/PKG-INFO +1 -1
  5. {tenacity-8.3.0 → tenacity-8.4.1}/README.rst +13 -7
  6. {tenacity-8.3.0 → tenacity-8.4.1}/doc/source/index.rst +13 -7
  7. tenacity-8.4.1/releasenotes/notes/add-async-actions-b249c527d99723bb.yaml +5 -0
  8. tenacity-8.4.1/releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml +3 -0
  9. tenacity-8.4.1/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml +6 -0
  10. {tenacity-8.3.0 → tenacity-8.4.1}/setup.cfg +2 -1
  11. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/__init__.py +19 -9
  12. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/_utils.py +12 -0
  13. tenacity-8.3.0/tenacity/_asyncio.py → tenacity-8.4.1/tenacity/asyncio/__init__.py +77 -24
  14. tenacity-8.4.1/tenacity/asyncio/retry.py +125 -0
  15. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/retry.py +8 -2
  16. {tenacity-8.3.0 → tenacity-8.4.1/tenacity.egg-info}/PKG-INFO +1 -1
  17. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity.egg-info/SOURCES.txt +5 -1
  18. {tenacity-8.3.0 → tenacity-8.4.1}/tests/test_asyncio.py +186 -3
  19. {tenacity-8.3.0 → tenacity-8.4.1}/tox.ini +4 -2
  20. {tenacity-8.3.0 → tenacity-8.4.1}/.editorconfig +0 -0
  21. {tenacity-8.3.0 → tenacity-8.4.1}/.github/dependabot.yml +0 -0
  22. {tenacity-8.3.0 → tenacity-8.4.1}/.gitignore +0 -0
  23. {tenacity-8.3.0 → tenacity-8.4.1}/.readthedocs.yml +0 -0
  24. {tenacity-8.3.0 → tenacity-8.4.1}/LICENSE +0 -0
  25. {tenacity-8.3.0 → tenacity-8.4.1}/doc/source/api.rst +0 -0
  26. {tenacity-8.3.0 → tenacity-8.4.1}/doc/source/changelog.rst +0 -0
  27. {tenacity-8.3.0 → tenacity-8.4.1}/doc/source/conf.py +0 -0
  28. {tenacity-8.3.0 → tenacity-8.4.1}/pyproject.toml +0 -0
  29. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml +0 -0
  30. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml +0 -0
  31. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/add-reno-d1ab5710f272650a.yaml +0 -0
  32. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml +0 -0
  33. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml +0 -0
  34. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/add-test-extra-55e869261b03e56d.yaml +0 -0
  35. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml +0 -0
  36. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml +0 -0
  37. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml +0 -0
  38. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/after_log-50f4d73b24ce9203.yaml +0 -0
  39. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml +0 -0
  40. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/annotate_code-197b93130df14042.yaml +0 -0
  41. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml +0 -0
  42. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml +0 -0
  43. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml +0 -0
  44. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml +0 -0
  45. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml +0 -0
  46. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml +0 -0
  47. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml +0 -0
  48. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml +0 -0
  49. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml +0 -0
  50. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml +0 -0
  51. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml +0 -0
  52. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml +0 -0
  53. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/pr320-py3-only-wheel-tag.yaml +0 -0
  54. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml +0 -0
  55. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/remove-py36-876c0416cf279d15.yaml +0 -0
  56. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml +0 -0
  57. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml +0 -0
  58. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml +0 -0
  59. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml +0 -0
  60. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml +0 -0
  61. {tenacity-8.3.0 → tenacity-8.4.1}/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml +0 -0
  62. {tenacity-8.3.0 → tenacity-8.4.1}/reno.yaml +0 -0
  63. {tenacity-8.3.0 → tenacity-8.4.1}/setup.py +0 -0
  64. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/after.py +0 -0
  65. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/before.py +0 -0
  66. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/before_sleep.py +0 -0
  67. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/nap.py +0 -0
  68. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/py.typed +0 -0
  69. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/stop.py +0 -0
  70. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/tornadoweb.py +0 -0
  71. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity/wait.py +0 -0
  72. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity.egg-info/dependency_links.txt +0 -0
  73. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity.egg-info/requires.txt +0 -0
  74. {tenacity-8.3.0 → tenacity-8.4.1}/tenacity.egg-info/top_level.txt +0 -0
  75. {tenacity-8.3.0 → tenacity-8.4.1}/tests/__init__.py +0 -0
  76. {tenacity-8.3.0 → tenacity-8.4.1}/tests/test_after.py +0 -0
  77. {tenacity-8.3.0 → tenacity-8.4.1}/tests/test_tenacity.py +0 -0
  78. {tenacity-8.3.0 → tenacity-8.4.1}/tests/test_tornado.py +0 -0
  79. {tenacity-8.3.0 → tenacity-8.4.1}/tests/test_utils.py +0 -0
@@ -27,19 +27,19 @@ jobs:
27
27
  - python: "3.11"
28
28
  tox: py311
29
29
  - python: "3.12"
30
- tox: py312
30
+ tox: py312,py312-trio
31
31
  - python: "3.12"
32
32
  tox: pep8
33
33
  - python: "3.11"
34
34
  tox: mypy
35
35
  steps:
36
36
  - name: Checkout 🛎️
37
- uses: actions/checkout@v4.1.1
37
+ uses: actions/checkout@v4.1.6
38
38
  with:
39
39
  fetch-depth: 0
40
40
 
41
41
  - name: Setup Python 🔧
42
- uses: actions/setup-python@v5.0.0
42
+ uses: actions/setup-python@v5.1.0
43
43
  with:
44
44
  python-version: ${{ matrix.python }}
45
45
  allow-prereleases: true
@@ -11,12 +11,12 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - name: Checkout 🛎️
14
- uses: actions/checkout@v4.1.1
14
+ uses: actions/checkout@v4.1.6
15
15
  with:
16
16
  fetch-depth: 0
17
17
 
18
18
  - name: Setup Python 🔧
19
- uses: actions/setup-python@v5.0.0
19
+ uses: actions/setup-python@v5.1.0
20
20
  with:
21
21
  python-version: 3.11
22
22
 
@@ -14,7 +14,7 @@ queue_rules:
14
14
  - "check-success=test (3.9, py39)"
15
15
  - "check-success=test (3.10, py310)"
16
16
  - "check-success=test (3.11, py311)"
17
- - "check-success=test (3.12, py312)"
17
+ - "check-success=test (3.12, py312,py312-trio)"
18
18
  - "check-success=test (3.12, pep8)"
19
19
 
20
20
  pull_request_rules:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tenacity
3
- Version: 8.3.0
3
+ Version: 8.4.1
4
4
  Summary: Retry code until it succeeds
5
5
  Home-page: https://github.com/jd/tenacity
6
6
  Author: Julien Danjou
@@ -79,7 +79,7 @@ Examples
79
79
  Basic Retry
80
80
  ~~~~~~~~~~~
81
81
 
82
- .. testsetup:: *
82
+ .. testsetup::
83
83
 
84
84
  import logging
85
85
  #
@@ -568,28 +568,34 @@ in retry strategies like ``retry_if_result``. This can be done accessing the
568
568
  Async and retry
569
569
  ~~~~~~~~~~~~~~~
570
570
 
571
- Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines.
571
+ Finally, ``retry`` works also on asyncio, Trio, and Tornado (>= 4.5) coroutines.
572
572
  Sleeps are done asynchronously too.
573
573
 
574
574
  .. code-block:: python
575
575
 
576
576
  @retry
577
- async def my_async_function(loop):
577
+ async def my_asyncio_function(loop):
578
578
  await loop.getaddrinfo('8.8.8.8', 53)
579
579
 
580
+ .. code-block:: python
581
+
582
+ @retry
583
+ async def my_async_trio_function():
584
+ await trio.socket.getaddrinfo('8.8.8.8', 53)
585
+
580
586
  .. code-block:: python
581
587
 
582
588
  @retry
583
589
  @tornado.gen.coroutine
584
- def my_async_function(http_client, url):
590
+ def my_async_tornado_function(http_client, url):
585
591
  yield http_client.fetch(url)
586
592
 
587
- You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function:
593
+ You can even use alternative event loops such as `curio` by passing the correct sleep function:
588
594
 
589
595
  .. code-block:: python
590
596
 
591
- @retry(sleep=trio.sleep)
592
- async def my_async_function(loop):
597
+ @retry(sleep=curio.sleep)
598
+ async def my_async_curio_function():
593
599
  await asks.get('https://example.org')
594
600
 
595
601
  Contribute
@@ -79,7 +79,7 @@ Examples
79
79
  Basic Retry
80
80
  ~~~~~~~~~~~
81
81
 
82
- .. testsetup:: *
82
+ .. testsetup::
83
83
 
84
84
  import logging
85
85
  #
@@ -568,28 +568,34 @@ in retry strategies like ``retry_if_result``. This can be done accessing the
568
568
  Async and retry
569
569
  ~~~~~~~~~~~~~~~
570
570
 
571
- Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines.
571
+ Finally, ``retry`` works also on asyncio, Trio, and Tornado (>= 4.5) coroutines.
572
572
  Sleeps are done asynchronously too.
573
573
 
574
574
  .. code-block:: python
575
575
 
576
576
  @retry
577
- async def my_async_function(loop):
577
+ async def my_asyncio_function(loop):
578
578
  await loop.getaddrinfo('8.8.8.8', 53)
579
579
 
580
+ .. code-block:: python
581
+
582
+ @retry
583
+ async def my_async_trio_function():
584
+ await trio.socket.getaddrinfo('8.8.8.8', 53)
585
+
580
586
  .. code-block:: python
581
587
 
582
588
  @retry
583
589
  @tornado.gen.coroutine
584
- def my_async_function(http_client, url):
590
+ def my_async_tornado_function(http_client, url):
585
591
  yield http_client.fetch(url)
586
592
 
587
- You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function:
593
+ You can even use alternative event loops such as `curio` by passing the correct sleep function:
588
594
 
589
595
  .. code-block:: python
590
596
 
591
- @retry(sleep=trio.sleep)
592
- async def my_async_function(loop):
597
+ @retry(sleep=curio.sleep)
598
+ async def my_async_curio_function():
593
599
  await asks.get('https://example.org')
594
600
 
595
601
  Contribute
@@ -0,0 +1,5 @@
1
+ ---
2
+ features:
3
+ - |
4
+ Added the ability to use async functions for retries. This way, you can now use
5
+ asyncio coroutines for retry strategy predicates.
@@ -0,0 +1,3 @@
1
+ ---
2
+ fixes:
3
+ - Fix setuptools config to include tenacity.asyncio package in release distributions.
@@ -0,0 +1,6 @@
1
+ ---
2
+ features:
3
+ - |
4
+ If you're using `Trio <https://trio.readthedocs.io>`__, then
5
+ ``@retry`` now works automatically. It's no longer necessary to
6
+ pass ``sleep=trio.sleep``.
@@ -23,9 +23,10 @@ classifier =
23
23
  [options]
24
24
  install_requires =
25
25
  python_requires = >=3.8
26
- packages = tenacity
26
+ packages = find:
27
27
 
28
28
  [options.packages.find]
29
+ include = tenacity*
29
30
  exclude = tests
30
31
 
31
32
  [options.package_data]
@@ -24,7 +24,8 @@ import typing as t
24
24
  import warnings
25
25
  from abc import ABC, abstractmethod
26
26
  from concurrent import futures
27
- from inspect import iscoroutinefunction
27
+
28
+ from . import _utils
28
29
 
29
30
  # Import all built-in retry strategies for easier usage.
30
31
  from .retry import retry_base # noqa
@@ -87,6 +88,7 @@ except ImportError:
87
88
  if t.TYPE_CHECKING:
88
89
  import types
89
90
 
91
+ from . import asyncio as tasyncio
90
92
  from .retry import RetryBaseT
91
93
  from .stop import StopBaseT
92
94
  from .wait import WaitBaseT
@@ -593,16 +595,24 @@ def retry(func: WrappedFn) -> WrappedFn: ...
593
595
 
594
596
  @t.overload
595
597
  def retry(
596
- sleep: t.Callable[[t.Union[int, float]], t.Optional[t.Awaitable[None]]] = sleep,
598
+ sleep: t.Callable[[t.Union[int, float]], t.Union[None, t.Awaitable[None]]] = sleep,
597
599
  stop: "StopBaseT" = stop_never,
598
600
  wait: "WaitBaseT" = wait_none(),
599
- retry: "RetryBaseT" = retry_if_exception_type(),
600
- before: t.Callable[["RetryCallState"], None] = before_nothing,
601
- after: t.Callable[["RetryCallState"], None] = after_nothing,
602
- before_sleep: t.Optional[t.Callable[["RetryCallState"], None]] = None,
601
+ retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = retry_if_exception_type(),
602
+ before: t.Callable[
603
+ ["RetryCallState"], t.Union[None, t.Awaitable[None]]
604
+ ] = before_nothing,
605
+ after: t.Callable[
606
+ ["RetryCallState"], t.Union[None, t.Awaitable[None]]
607
+ ] = after_nothing,
608
+ before_sleep: t.Optional[
609
+ t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]]
610
+ ] = None,
603
611
  reraise: bool = False,
604
612
  retry_error_cls: t.Type["RetryError"] = RetryError,
605
- retry_error_callback: t.Optional[t.Callable[["RetryCallState"], t.Any]] = None,
613
+ retry_error_callback: t.Optional[
614
+ t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]]
615
+ ] = None,
606
616
  ) -> t.Callable[[WrappedFn], WrappedFn]: ...
607
617
 
608
618
 
@@ -624,7 +634,7 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
624
634
  f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)"
625
635
  )
626
636
  r: "BaseRetrying"
627
- if iscoroutinefunction(f):
637
+ if _utils.is_coroutine_callable(f):
628
638
  r = AsyncRetrying(*dargs, **dkw)
629
639
  elif (
630
640
  tornado
@@ -640,7 +650,7 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
640
650
  return wrap
641
651
 
642
652
 
643
- from tenacity._asyncio import AsyncRetrying # noqa:E402,I100
653
+ from tenacity.asyncio import AsyncRetrying # noqa:E402,I100
644
654
 
645
655
  if tornado:
646
656
  from tenacity.tornadoweb import TornadoRetrying
@@ -87,3 +87,15 @@ def is_coroutine_callable(call: typing.Callable[..., typing.Any]) -> bool:
87
87
  partial_call = isinstance(call, functools.partial) and call.func
88
88
  dunder_call = partial_call or getattr(call, "__call__", None)
89
89
  return inspect.iscoroutinefunction(dunder_call)
90
+
91
+
92
+ def wrap_to_async_func(
93
+ call: typing.Callable[..., typing.Any],
94
+ ) -> typing.Callable[..., typing.Awaitable[typing.Any]]:
95
+ if is_coroutine_callable(call):
96
+ return call
97
+
98
+ async def inner(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
99
+ return call(*args, **kwargs)
100
+
101
+ return inner
@@ -19,34 +19,87 @@ import functools
19
19
  import sys
20
20
  import typing as t
21
21
 
22
+ import tenacity
22
23
  from tenacity import AttemptManager
23
24
  from tenacity import BaseRetrying
24
25
  from tenacity import DoAttempt
25
26
  from tenacity import DoSleep
26
27
  from tenacity import RetryCallState
28
+ from tenacity import RetryError
29
+ from tenacity import after_nothing
30
+ from tenacity import before_nothing
27
31
  from tenacity import _utils
28
32
 
33
+ # Import all built-in retry strategies for easier usage.
34
+ from .retry import RetryBaseT
35
+ from .retry import retry_all # noqa
36
+ from .retry import retry_any # noqa
37
+ from .retry import retry_if_exception # noqa
38
+ from .retry import retry_if_result # noqa
39
+ from ..retry import RetryBaseT as SyncRetryBaseT
40
+
41
+ if t.TYPE_CHECKING:
42
+ from tenacity.stop import StopBaseT
43
+ from tenacity.wait import WaitBaseT
44
+
29
45
  WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
30
46
  WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]])
31
47
 
32
48
 
33
- def asyncio_sleep(duration: float) -> t.Awaitable[None]:
49
+ def _portable_async_sleep(seconds: float) -> t.Awaitable[None]:
50
+ # If trio is already imported, then importing it is cheap.
51
+ # If trio isn't already imported, then it's definitely not running, so we
52
+ # can skip further checks.
53
+ if "trio" in sys.modules:
54
+ # If trio is available, then sniffio is too
55
+ import trio
56
+ import sniffio
57
+
58
+ if sniffio.current_async_library() == "trio":
59
+ return trio.sleep(seconds)
60
+ # Otherwise, assume asyncio
34
61
  # Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead).
35
62
  import asyncio
36
63
 
37
- return asyncio.sleep(duration)
64
+ return asyncio.sleep(seconds)
38
65
 
39
66
 
40
67
  class AsyncRetrying(BaseRetrying):
41
- sleep: t.Callable[[float], t.Awaitable[t.Any]]
42
-
43
68
  def __init__(
44
69
  self,
45
- sleep: t.Callable[[float], t.Awaitable[t.Any]] = asyncio_sleep,
46
- **kwargs: t.Any,
70
+ sleep: t.Callable[
71
+ [t.Union[int, float]], t.Union[None, t.Awaitable[None]]
72
+ ] = _portable_async_sleep,
73
+ stop: "StopBaseT" = tenacity.stop.stop_never,
74
+ wait: "WaitBaseT" = tenacity.wait.wait_none(),
75
+ retry: "t.Union[SyncRetryBaseT, RetryBaseT]" = tenacity.retry_if_exception_type(),
76
+ before: t.Callable[
77
+ ["RetryCallState"], t.Union[None, t.Awaitable[None]]
78
+ ] = before_nothing,
79
+ after: t.Callable[
80
+ ["RetryCallState"], t.Union[None, t.Awaitable[None]]
81
+ ] = after_nothing,
82
+ before_sleep: t.Optional[
83
+ t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]]
84
+ ] = None,
85
+ reraise: bool = False,
86
+ retry_error_cls: t.Type["RetryError"] = RetryError,
87
+ retry_error_callback: t.Optional[
88
+ t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]]
89
+ ] = None,
47
90
  ) -> None:
48
- super().__init__(**kwargs)
49
- self.sleep = sleep
91
+ super().__init__(
92
+ sleep=sleep, # type: ignore[arg-type]
93
+ stop=stop,
94
+ wait=wait,
95
+ retry=retry, # type: ignore[arg-type]
96
+ before=before, # type: ignore[arg-type]
97
+ after=after, # type: ignore[arg-type]
98
+ before_sleep=before_sleep, # type: ignore[arg-type]
99
+ reraise=reraise,
100
+ retry_error_cls=retry_error_cls,
101
+ retry_error_callback=retry_error_callback,
102
+ )
50
103
 
51
104
  async def __call__( # type: ignore[override]
52
105
  self, fn: WrappedFn, *args: t.Any, **kwargs: t.Any
@@ -65,31 +118,21 @@ class AsyncRetrying(BaseRetrying):
65
118
  retry_state.set_result(result)
66
119
  elif isinstance(do, DoSleep):
67
120
  retry_state.prepare_for_next_attempt()
68
- await self.sleep(do)
121
+ await self.sleep(do) # type: ignore[misc]
69
122
  else:
70
123
  return do # type: ignore[no-any-return]
71
124
 
72
- @classmethod
73
- def _wrap_action_func(cls, fn: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
74
- if _utils.is_coroutine_callable(fn):
75
- return fn
76
-
77
- async def inner(*args: t.Any, **kwargs: t.Any) -> t.Any:
78
- return fn(*args, **kwargs)
79
-
80
- return inner
81
-
82
125
  def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
83
- self.iter_state.actions.append(self._wrap_action_func(fn))
126
+ self.iter_state.actions.append(_utils.wrap_to_async_func(fn))
84
127
 
85
128
  async def _run_retry(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
86
- self.iter_state.retry_run_result = await self._wrap_action_func(self.retry)(
129
+ self.iter_state.retry_run_result = await _utils.wrap_to_async_func(self.retry)(
87
130
  retry_state
88
131
  )
89
132
 
90
133
  async def _run_wait(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
91
134
  if self.wait:
92
- sleep = await self._wrap_action_func(self.wait)(retry_state)
135
+ sleep = await _utils.wrap_to_async_func(self.wait)(retry_state)
93
136
  else:
94
137
  sleep = 0.0
95
138
 
@@ -97,7 +140,7 @@ class AsyncRetrying(BaseRetrying):
97
140
 
98
141
  async def _run_stop(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
99
142
  self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
100
- self.iter_state.stop_run_result = await self._wrap_action_func(self.stop)(
143
+ self.iter_state.stop_run_result = await _utils.wrap_to_async_func(self.stop)(
101
144
  retry_state
102
145
  )
103
146
 
@@ -127,7 +170,7 @@ class AsyncRetrying(BaseRetrying):
127
170
  return AttemptManager(retry_state=self._retry_state)
128
171
  elif isinstance(do, DoSleep):
129
172
  self._retry_state.prepare_for_next_attempt()
130
- await self.sleep(do)
173
+ await self.sleep(do) # type: ignore[misc]
131
174
  else:
132
175
  raise StopAsyncIteration
133
176
 
@@ -146,3 +189,13 @@ class AsyncRetrying(BaseRetrying):
146
189
  async_wrapped.retry_with = fn.retry_with # type: ignore[attr-defined]
147
190
 
148
191
  return async_wrapped # type: ignore[return-value]
192
+
193
+
194
+ __all__ = [
195
+ "retry_all",
196
+ "retry_any",
197
+ "retry_if_exception",
198
+ "retry_if_result",
199
+ "WrappedFn",
200
+ "AsyncRetrying",
201
+ ]
@@ -0,0 +1,125 @@
1
+ # Copyright 2016–2021 Julien Danjou
2
+ # Copyright 2016 Joshua Harlow
3
+ # Copyright 2013-2014 Ray Holder
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ import abc
17
+ import typing
18
+
19
+ from tenacity import _utils
20
+ from tenacity import retry_base
21
+
22
+ if typing.TYPE_CHECKING:
23
+ from tenacity import RetryCallState
24
+
25
+
26
+ class async_retry_base(retry_base):
27
+ """Abstract base class for async retry strategies."""
28
+
29
+ @abc.abstractmethod
30
+ async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override]
31
+ pass
32
+
33
+ def __and__( # type: ignore[override]
34
+ self, other: "typing.Union[retry_base, async_retry_base]"
35
+ ) -> "retry_all":
36
+ return retry_all(self, other)
37
+
38
+ def __rand__( # type: ignore[misc,override]
39
+ self, other: "typing.Union[retry_base, async_retry_base]"
40
+ ) -> "retry_all":
41
+ return retry_all(other, self)
42
+
43
+ def __or__( # type: ignore[override]
44
+ self, other: "typing.Union[retry_base, async_retry_base]"
45
+ ) -> "retry_any":
46
+ return retry_any(self, other)
47
+
48
+ def __ror__( # type: ignore[misc,override]
49
+ self, other: "typing.Union[retry_base, async_retry_base]"
50
+ ) -> "retry_any":
51
+ return retry_any(other, self)
52
+
53
+
54
+ RetryBaseT = typing.Union[
55
+ async_retry_base, typing.Callable[["RetryCallState"], typing.Awaitable[bool]]
56
+ ]
57
+
58
+
59
+ class retry_if_exception(async_retry_base):
60
+ """Retry strategy that retries if an exception verifies a predicate."""
61
+
62
+ def __init__(
63
+ self, predicate: typing.Callable[[BaseException], typing.Awaitable[bool]]
64
+ ) -> None:
65
+ self.predicate = predicate
66
+
67
+ async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override]
68
+ if retry_state.outcome is None:
69
+ raise RuntimeError("__call__() called before outcome was set")
70
+
71
+ if retry_state.outcome.failed:
72
+ exception = retry_state.outcome.exception()
73
+ if exception is None:
74
+ raise RuntimeError("outcome failed but the exception is None")
75
+ return await self.predicate(exception)
76
+ else:
77
+ return False
78
+
79
+
80
+ class retry_if_result(async_retry_base):
81
+ """Retries if the result verifies a predicate."""
82
+
83
+ def __init__(
84
+ self, predicate: typing.Callable[[typing.Any], typing.Awaitable[bool]]
85
+ ) -> None:
86
+ self.predicate = predicate
87
+
88
+ async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override]
89
+ if retry_state.outcome is None:
90
+ raise RuntimeError("__call__() called before outcome was set")
91
+
92
+ if not retry_state.outcome.failed:
93
+ return await self.predicate(retry_state.outcome.result())
94
+ else:
95
+ return False
96
+
97
+
98
+ class retry_any(async_retry_base):
99
+ """Retries if any of the retries condition is valid."""
100
+
101
+ def __init__(self, *retries: typing.Union[retry_base, async_retry_base]) -> None:
102
+ self.retries = retries
103
+
104
+ async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override]
105
+ result = False
106
+ for r in self.retries:
107
+ result = result or await _utils.wrap_to_async_func(r)(retry_state)
108
+ if result:
109
+ break
110
+ return result
111
+
112
+
113
+ class retry_all(async_retry_base):
114
+ """Retries if all the retries condition are valid."""
115
+
116
+ def __init__(self, *retries: typing.Union[retry_base, async_retry_base]) -> None:
117
+ self.retries = retries
118
+
119
+ async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override]
120
+ result = True
121
+ for r in self.retries:
122
+ result = result and await _utils.wrap_to_async_func(r)(retry_state)
123
+ if not result:
124
+ break
125
+ return result
@@ -30,10 +30,16 @@ class retry_base(abc.ABC):
30
30
  pass
31
31
 
32
32
  def __and__(self, other: "retry_base") -> "retry_all":
33
- return retry_all(self, other)
33
+ return other.__rand__(self)
34
+
35
+ def __rand__(self, other: "retry_base") -> "retry_all":
36
+ return retry_all(other, self)
34
37
 
35
38
  def __or__(self, other: "retry_base") -> "retry_any":
36
- return retry_any(self, other)
39
+ return other.__ror__(self)
40
+
41
+ def __ror__(self, other: "retry_base") -> "retry_any":
42
+ return retry_any(other, self)
37
43
 
38
44
 
39
45
  RetryBaseT = typing.Union[retry_base, typing.Callable[["RetryCallState"], bool]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tenacity
3
- Version: 8.3.0
3
+ Version: 8.4.1
4
4
  Summary: Retry code until it succeeds
5
5
  Home-page: https://github.com/jd/tenacity
6
6
  Author: Julien Danjou
@@ -18,6 +18,7 @@ doc/source/conf.py
18
18
  doc/source/index.rst
19
19
  releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml
20
20
  releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml
21
+ releasenotes/notes/add-async-actions-b249c527d99723bb.yaml
21
22
  releasenotes/notes/add-reno-d1ab5710f272650a.yaml
22
23
  releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml
23
24
  releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml
@@ -36,6 +37,7 @@ releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml
36
37
  releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml
37
38
  releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml
38
39
  releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml
40
+ releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml
39
41
  releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml
40
42
  releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml
41
43
  releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml
@@ -48,9 +50,9 @@ releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml
48
50
  releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml
49
51
  releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml
50
52
  releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml
53
+ releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml
51
54
  releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml
52
55
  tenacity/__init__.py
53
- tenacity/_asyncio.py
54
56
  tenacity/_utils.py
55
57
  tenacity/after.py
56
58
  tenacity/before.py
@@ -66,6 +68,8 @@ tenacity.egg-info/SOURCES.txt
66
68
  tenacity.egg-info/dependency_links.txt
67
69
  tenacity.egg-info/requires.txt
68
70
  tenacity.egg-info/top_level.txt
71
+ tenacity/asyncio/__init__.py
72
+ tenacity/asyncio/retry.py
69
73
  tests/__init__.py
70
74
  tests/test_after.py
71
75
  tests/test_asyncio.py
@@ -18,12 +18,19 @@ import inspect
18
18
  import unittest
19
19
  from functools import wraps
20
20
 
21
+ try:
22
+ import trio
23
+ except ImportError:
24
+ have_trio = False
25
+ else:
26
+ have_trio = True
27
+
21
28
  import pytest
22
29
 
23
30
  import tenacity
24
31
  from tenacity import AsyncRetrying, RetryError
25
- from tenacity import _asyncio as tasyncio
26
- from tenacity import retry, retry_if_result, stop_after_attempt
32
+ from tenacity import asyncio as tasyncio
33
+ from tenacity import retry, retry_if_exception, retry_if_result, stop_after_attempt
27
34
  from tenacity.wait import wait_fixed
28
35
 
29
36
  from .test_tenacity import NoIOErrorAfterCount, current_time_ms
@@ -55,7 +62,7 @@ async def _retryable_coroutine_with_2_attempts(thing):
55
62
  thing.go()
56
63
 
57
64
 
58
- class TestAsync(unittest.TestCase):
65
+ class TestAsyncio(unittest.TestCase):
59
66
  @asynctest
60
67
  async def test_retry(self):
61
68
  thing = NoIOErrorAfterCount(5)
@@ -138,6 +145,21 @@ class TestAsync(unittest.TestCase):
138
145
  assert list(attempt_nos2) == [1, 2, 3]
139
146
 
140
147
 
148
+ @unittest.skipIf(not have_trio, "trio not installed")
149
+ class TestTrio(unittest.TestCase):
150
+ def test_trio_basic(self):
151
+ thing = NoIOErrorAfterCount(5)
152
+
153
+ @retry
154
+ async def trio_function():
155
+ await trio.sleep(0.00001)
156
+ return thing.go()
157
+
158
+ trio.run(trio_function)
159
+
160
+ assert thing.counter == thing.count
161
+
162
+
141
163
  class TestContextManager(unittest.TestCase):
142
164
  @asynctest
143
165
  async def test_do_max_attempts(self):
@@ -202,6 +224,167 @@ class TestContextManager(unittest.TestCase):
202
224
 
203
225
  self.assertEqual(3, result)
204
226
 
227
+ @asynctest
228
+ async def test_retry_with_async_result(self):
229
+ async def test():
230
+ attempts = 0
231
+
232
+ async def lt_3(x: float) -> bool:
233
+ return x < 3
234
+
235
+ async for attempt in tasyncio.AsyncRetrying(
236
+ retry=tasyncio.retry_if_result(lt_3)
237
+ ):
238
+ with attempt:
239
+ attempts += 1
240
+
241
+ assert attempt.retry_state.outcome # help mypy
242
+ if not attempt.retry_state.outcome.failed:
243
+ attempt.retry_state.set_result(attempts)
244
+
245
+ return attempts
246
+
247
+ result = await test()
248
+
249
+ self.assertEqual(3, result)
250
+
251
+ @asynctest
252
+ async def test_retry_with_async_exc(self):
253
+ async def test():
254
+ attempts = 0
255
+
256
+ class CustomException(Exception):
257
+ pass
258
+
259
+ async def is_exc(e: BaseException) -> bool:
260
+ return isinstance(e, CustomException)
261
+
262
+ async for attempt in tasyncio.AsyncRetrying(
263
+ retry=tasyncio.retry_if_exception(is_exc)
264
+ ):
265
+ with attempt:
266
+ attempts += 1
267
+ if attempts < 3:
268
+ raise CustomException()
269
+
270
+ assert attempt.retry_state.outcome # help mypy
271
+ if not attempt.retry_state.outcome.failed:
272
+ attempt.retry_state.set_result(attempts)
273
+
274
+ return attempts
275
+
276
+ result = await test()
277
+
278
+ self.assertEqual(3, result)
279
+
280
+ @asynctest
281
+ async def test_retry_with_async_result_or(self):
282
+ async def test():
283
+ attempts = 0
284
+
285
+ async def lt_3(x: float) -> bool:
286
+ return x < 3
287
+
288
+ class CustomException(Exception):
289
+ pass
290
+
291
+ def is_exc(e: BaseException) -> bool:
292
+ return isinstance(e, CustomException)
293
+
294
+ retry_strategy = tasyncio.retry_if_result(lt_3) | retry_if_exception(is_exc)
295
+ async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy):
296
+ with attempt:
297
+ attempts += 1
298
+ if 2 < attempts < 4:
299
+ raise CustomException()
300
+
301
+ assert attempt.retry_state.outcome # help mypy
302
+ if not attempt.retry_state.outcome.failed:
303
+ attempt.retry_state.set_result(attempts)
304
+
305
+ return attempts
306
+
307
+ result = await test()
308
+
309
+ self.assertEqual(4, result)
310
+
311
+ @asynctest
312
+ async def test_retry_with_async_result_ror(self):
313
+ async def test():
314
+ attempts = 0
315
+
316
+ def lt_3(x: float) -> bool:
317
+ return x < 3
318
+
319
+ class CustomException(Exception):
320
+ pass
321
+
322
+ async def is_exc(e: BaseException) -> bool:
323
+ return isinstance(e, CustomException)
324
+
325
+ retry_strategy = retry_if_result(lt_3) | tasyncio.retry_if_exception(is_exc)
326
+ async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy):
327
+ with attempt:
328
+ attempts += 1
329
+ if 2 < attempts < 4:
330
+ raise CustomException()
331
+
332
+ assert attempt.retry_state.outcome # help mypy
333
+ if not attempt.retry_state.outcome.failed:
334
+ attempt.retry_state.set_result(attempts)
335
+
336
+ return attempts
337
+
338
+ result = await test()
339
+
340
+ self.assertEqual(4, result)
341
+
342
+ @asynctest
343
+ async def test_retry_with_async_result_and(self):
344
+ async def test():
345
+ attempts = 0
346
+
347
+ async def lt_3(x: float) -> bool:
348
+ return x < 3
349
+
350
+ def gt_0(x: float) -> bool:
351
+ return x > 0
352
+
353
+ retry_strategy = tasyncio.retry_if_result(lt_3) & retry_if_result(gt_0)
354
+ async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy):
355
+ with attempt:
356
+ attempts += 1
357
+ attempt.retry_state.set_result(attempts)
358
+
359
+ return attempts
360
+
361
+ result = await test()
362
+
363
+ self.assertEqual(3, result)
364
+
365
+ @asynctest
366
+ async def test_retry_with_async_result_rand(self):
367
+ async def test():
368
+ attempts = 0
369
+
370
+ async def lt_3(x: float) -> bool:
371
+ return x < 3
372
+
373
+ def gt_0(x: float) -> bool:
374
+ return x > 0
375
+
376
+ retry_strategy = retry_if_result(gt_0) & tasyncio.retry_if_result(lt_3)
377
+ async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy):
378
+ with attempt:
379
+ attempts += 1
380
+ attempt.retry_state.set_result(attempts)
381
+
382
+ return attempts
383
+
384
+ result = await test()
385
+
386
+ self.assertEqual(3, result)
387
+
205
388
  @asynctest
206
389
  async def test_async_retying_iterator(self):
207
390
  thing = NoIOErrorAfterCount(5)
@@ -1,5 +1,5 @@
1
1
  [tox]
2
- envlist = py3{8,9,10,11,12}, pep8, pypy3
2
+ envlist = py3{8,9,10,11,12,12-trio}, pep8, pypy3
3
3
  skip_missing_interpreters = True
4
4
 
5
5
  [testenv]
@@ -8,6 +8,7 @@ sitepackages = False
8
8
  deps =
9
9
  .[test]
10
10
  .[doc]
11
+ trio: trio
11
12
  commands =
12
13
  py3{8,9,10,11,12},pypy3: pytest {posargs}
13
14
  py3{8,9,10,11,12},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build
@@ -24,10 +25,11 @@ commands =
24
25
  deps =
25
26
  mypy>=1.0.0
26
27
  pytest # for stubs
28
+ trio
27
29
  commands =
28
30
  mypy {posargs}
29
31
 
30
32
  [testenv:reno]
31
33
  basepython = python3
32
34
  deps = reno
33
- commands = reno {posargs}
35
+ commands = reno {posargs}
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