Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,10 @@ Async and retry
Finally, ``retry`` works also on asyncio, Trio, and Tornado coroutines.
Sleeps are done asynchronously too.

``AsyncRetrying`` also accepts a synchronous ``sleep`` callback, such as
``sleep=lambda seconds: None`` to skip sleeping in tests. If the callback returns
an awaitable, it is awaited before the next attempt.

.. code-block:: python

@retry
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fixes:
- |
Allow synchronous sleep callbacks returning None in AsyncRetrying, as
supported by its type signature, without raising TypeError on a retry.
10 changes: 8 additions & 2 deletions tenacity/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,16 @@ async def __call__( # type: ignore[override]
retry_state.set_result(result)
elif isinstance(do, DoSleep):
retry_state.prepare_for_next_attempt()
await self.sleep(do) # type: ignore[misc]
await self._sleep(do)
else:
return do # type: ignore[no-any-return]

async def _sleep(self, seconds: float) -> None:
sleep: t.Callable[[float], t.Awaitable[None] | None] = self.sleep
result = sleep(seconds)
if result is not None:
await result

@override
def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
self.iter_state.actions.append(_utils.wrap_to_async_func(fn))
Expand Down Expand Up @@ -208,7 +214,7 @@ async def __anext__(self) -> AttemptManager:
return AttemptManager(retry_state=self._retry_state)
if isinstance(do, DoSleep):
self._retry_state.prepare_for_next_attempt()
await self.sleep(do) # type: ignore[misc]
await self._sleep(do)
else:
raise StopAsyncIteration

Expand Down
39 changes: 38 additions & 1 deletion tests/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import asyncio
import inspect
import unittest
from collections.abc import Callable, Coroutine
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, TypeVar
from unittest import mock
Expand Down Expand Up @@ -76,6 +76,43 @@ async def _retryable_coroutine_with_2_attempts(thing: NoIOErrorAfterCount) -> An
return thing.go()


@pytest.mark.parametrize("as_iterator", [False, True])
@pytest.mark.parametrize("sleep_kind", ["sync", "async", "awaitable"])
@asynctest
async def test_sleep_callback(sleep_kind: str, as_iterator: bool) -> None:
sleeps: list[float] = []

async def async_sleep(seconds: float) -> None:
sleeps.append(seconds)

def awaitable_sleep(seconds: float) -> Awaitable[None]:
return async_sleep(seconds)

callbacks: dict[str, Callable[[float], Awaitable[None] | None]] = {
"sync": sleeps.append,
"async": async_sleep,
"awaitable": awaitable_sleep,
}
retrying = AsyncRetrying(
sleep=callbacks[sleep_kind],
wait=wait_fixed(0.5),
stop=stop_after_attempt(3),
)
thing = NoIOErrorAfterCount(2)
result = None
if as_iterator:
async for attempt in retrying:
with attempt:
result = thing.go()
else:
result = await retrying(_async_function, thing)

assert result is True
assert sleeps == [0.5, 0.5]
assert retrying.statistics["attempt_number"] == 3
assert retrying.statistics["idle_for"] == 1.0


class TestAsyncio(unittest.TestCase):
@asynctest
async def test_retry(self) -> None:
Expand Down
Loading