Support Trio out-of-the-box, take 2 (#463)
* 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>
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index d4648a4..b0e3c02 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -27,7 +27,7 @@
- python: "3.11"
tox: py311
- python: "3.12"
- tox: py312
+ tox: py312,py312-trio
- python: "3.12"
tox: pep8
- python: "3.11"
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 3f0764a..65dd208 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -568,28 +568,34 @@
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
- @tornado.gen.coroutine
- def my_async_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:
+ async def my_async_trio_function():
+ await trio.socket.getaddrinfo('8.8.8.8', 53)
.. code-block:: python
- @retry(sleep=trio.sleep)
- async def my_async_function(loop):
+ @retry
+ @tornado.gen.coroutine
+ def my_async_tornado_function(http_client, url):
+ yield http_client.fetch(url)
+
+You can even use alternative event loops such as `curio` by passing the correct sleep function:
+
+.. code-block:: python
+
+ @retry(sleep=curio.sleep)
+ async def my_async_curio_function():
await asks.get('https://example.org')
Contribute
diff --git a/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml b/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml
new file mode 100644
index 0000000..b8e0c14
--- /dev/null
+++ b/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml
@@ -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``.
diff --git a/tenacity/asyncio/__init__.py b/tenacity/asyncio/__init__.py
index 3ec0088..6d63ebc 100644
--- a/tenacity/asyncio/__init__.py
+++ b/tenacity/asyncio/__init__.py
@@ -46,11 +46,22 @@
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):
@@ -58,7 +69,7 @@
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(),
diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py
index 48f6286..8716529 100644
--- a/tests/test_asyncio.py
+++ b/tests/test_asyncio.py
@@ -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
@@ -55,7 +62,7 @@
thing.go()
-class TestAsync(unittest.TestCase):
+class TestAsyncio(unittest.TestCase):
@asynctest
async def test_retry(self):
thing = NoIOErrorAfterCount(5)
@@ -138,6 +145,21 @@
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):
diff --git a/tox.ini b/tox.ini
index 13e5a1d..14f8ae0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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]
@@ -8,6 +8,7 @@
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
@@ -24,10 +25,11 @@
deps =
mypy>=1.0.0
pytest # for stubs
+ trio
commands =
mypy {posargs}
[testenv:reno]
basepython = python3
deps = reno
-commands = reno {posargs}
\ No newline at end of file
+commands = reno {posargs}