Skip to content

Commit

Permalink
Support Trio out-of-the-box, take 2 (#463)
Browse files Browse the repository at this point in the history
* Support Trio out-of-the-box

This PR makes `@retry` just work when running under Trio.

* Add a no-trio test environment

* Switch to only testing trio in one environment

* bump releasenote so it is later in history->reno puts it in the correct place in the changelog

* fix mypy & pep8 checks

* Update doc/source/index.rst

fix example

Co-authored-by: Julien Danjou <julien@danjou.info>

* Update tests/test_tornado.py

* Update tests/test_tornado.py

* make _portably_async_sleep a sync function that returns an async function

---------

Co-authored-by: Nathaniel J. Smith <njs@pobox.com>
Co-authored-by: Julien Danjou <julien@danjou.info>
  • Loading branch information
3 people authored Jun 17, 2024
1 parent 952189b commit ade0567
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 13 deletions.
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``.
17 changes: 14 additions & 3 deletions tenacity/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,30 @@
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]])


def asyncio_sleep(duration: float) -> t.Awaitable[None]:
def _portable_async_sleep(seconds: float) -> t.Awaitable[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":
return trio.sleep(seconds)
# Otherwise, assume asyncio
# Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead).
import asyncio

return asyncio.sleep(duration)
return asyncio.sleep(seconds)


class AsyncRetrying(BaseRetrying):
def __init__(
self,
sleep: t.Callable[
[t.Union[int, float]], t.Union[None, t.Awaitable[None]]
] = asyncio_sleep,
] = _portable_async_sleep,
stop: "StopBaseT" = tenacity.stop.stop_never,
wait: "WaitBaseT" = tenacity.wait.wait_none(),
retry: "t.Union[SyncRetryBaseT, RetryBaseT]" = tenacity.retry_if_exception_type(),
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
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}

0 comments on commit ade0567

Please sign in to comment.