Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Trio out-of-the-box, take 2 #463

Merged
merged 11 commits into from
Jun 17, 2024
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- python: "3.11"
tox: py311
- python: "3.12"
tox: py312
tox: py312,py312-trio
- python: "3.12"
tox: pep8
- python: "3.11"
Expand Down
18 changes: 12 additions & 6 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -568,28 +568,34 @@ in retry strategies like ``retry_if_result``. This can be done accessing the
Async and retry
~~~~~~~~~~~~~~~

Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines.
Finally, ``retry`` works also on asyncio, Trio, and Tornado (>= 4.5) coroutines.
Sleeps are done asynchronously too.

.. code-block:: python

@retry
async def my_async_function(loop):
async def my_asyncio_function(loop):
await loop.getaddrinfo('8.8.8.8', 53)

.. code-block:: python

@retry
async def my_async_trio_function():
await trio.socket.getaddrinfo('8.8.8.8', 53)

.. code-block:: python

@retry
@tornado.gen.coroutine
def my_async_function(http_client, url):
def my_async_tornado_function(http_client, url):
yield http_client.fetch(url)

You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function:
You can even use alternative event loops such as `curio` by passing the correct sleep function:

.. code-block:: python

@retry(sleep=trio.sleep)
async def my_async_function(loop):
@retry(sleep=curio.sleep)
async def my_async_curio_function():
await asks.get('https://example.org')

Contribute
Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
If you're using `Trio <https://trio.readthedocs.io>`__, then
``@retry`` now works automatically. It's no longer necessary to
pass ``sleep=trio.sleep``.
18 changes: 15 additions & 3 deletions tenacity/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,31 @@
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]])


def asyncio_sleep(duration: float) -> t.Awaitable[None]:
async def _portable_async_sleep(seconds: float) -> None:
# If trio is already imported, then importing it is cheap.
# If trio isn't already imported, then it's definitely not running, so we
# can skip further checks.
if "trio" in sys.modules:
# If trio is available, then sniffio is too
import trio
import sniffio

if sniffio.current_async_library() == "trio":
await trio.sleep(seconds)
return
jd marked this conversation as resolved.
Show resolved Hide resolved
# Otherwise, assume asyncio
# Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead).
import asyncio

return asyncio.sleep(duration)
await asyncio.sleep(seconds)
jd marked this conversation as resolved.
Show resolved Hide resolved


class AsyncRetrying(BaseRetrying):
sleep: t.Callable[[float], t.Awaitable[t.Any]]

def __init__(
self,
sleep: t.Callable[[float], t.Awaitable[t.Any]] = asyncio_sleep,
sleep: t.Callable[[float], t.Awaitable[t.Any]] = _portable_async_sleep,
**kwargs: t.Any,
) -> None:
super().__init__(**kwargs)
Expand Down
24 changes: 23 additions & 1 deletion tests/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
import unittest
from functools import wraps

try:
import trio
except ImportError:
have_trio = False
else:
have_trio = True

import pytest

import tenacity
Expand Down Expand Up @@ -55,7 +62,7 @@ async def _retryable_coroutine_with_2_attempts(thing):
thing.go()


class TestAsync(unittest.TestCase):
class TestAsyncio(unittest.TestCase):
@asynctest
async def test_retry(self):
thing = NoIOErrorAfterCount(5)
Expand Down Expand Up @@ -138,6 +145,21 @@ def after(retry_state):
assert list(attempt_nos2) == [1, 2, 3]


@unittest.skipIf(not have_trio, "trio not installed")
class TestTrio(unittest.TestCase):
def test_trio_basic(self):
thing = NoIOErrorAfterCount(5)

@retry
async def trio_function():
await trio.sleep(0.00001)
return thing.go()

trio.run(trio_function)

assert thing.counter == thing.count


class TestContextManager(unittest.TestCase):
@asynctest
async def test_do_max_attempts(self):
Expand Down
4 changes: 4 additions & 0 deletions tests/test_tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ def retryable(thing):
finally:
gen.is_coroutine_function = old_attr

# temporary workaround for https://github.com/jd/tenacity/issues/460
def runTest(self):
self.subTest()
jd marked this conversation as resolved.
Show resolved Hide resolved


if __name__ == "__main__":
unittest.main()
6 changes: 4 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py3{8,9,10,11,12}, pep8, pypy3
envlist = py3{8,9,10,11,12,12-trio}, pep8, pypy3
skip_missing_interpreters = True

[testenv]
Expand All @@ -8,6 +8,7 @@ sitepackages = False
deps =
.[test]
.[doc]
trio: trio
commands =
py3{8,9,10,11,12},pypy3: pytest {posargs}
py3{8,9,10,11,12},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build
Expand All @@ -24,10 +25,11 @@ commands =
deps =
mypy>=1.0.0
pytest # for stubs
trio
commands =
mypy {posargs}

[testenv:reno]
basepython = python3
deps = reno
commands = reno {posargs}
commands = reno {posargs}