diff --git a/.circleci/config.yml b/.circleci/config.yml index 43a8a4ed..b366b894 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,6 +64,15 @@ jobs: command: | sudo pip install tox tox -e py39 + py39-notrio: + docker: + - image: circleci/python:3.9 + steps: + - checkout + - run: + command: | + sudo pip install tox + tox -e py39-notrio deploy: docker: - image: circleci/python:3.9 @@ -100,6 +109,7 @@ workflows: - py37 - py38 - py39 + - py39-notrio - deploy: filters: tags: diff --git a/doc/source/index.rst b/doc/source/index.rst index 2f025ef1..02aa3e0f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -546,28 +546,34 @@ With async code you can use AsyncRetrying. 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 + 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 diff --git a/releasenotes/notes/trio-support-62fed9e32ccb62be.yaml b/releasenotes/notes/trio-support-62fed9e32ccb62be.yaml new file mode 100644 index 00000000..b8e0c149 --- /dev/null +++ b/releasenotes/notes/trio-support-62fed9e32ccb62be.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + If you're using `Trio `__, then + ``@retry`` now works automatically. It's no longer necessary to + pass ``sleep=trio.sleep``. diff --git a/tenacity/_asyncio.py b/tenacity/_asyncio.py index 979b6544..55e9bf2b 100644 --- a/tenacity/_asyncio.py +++ b/tenacity/_asyncio.py @@ -16,7 +16,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import sys -from asyncio import sleep from tenacity import AttemptManager from tenacity import BaseRetrying @@ -25,8 +24,23 @@ from tenacity import RetryCallState +async def _portable_async_sleep(seconds): + # 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, sniffio + if sniffio.current_async_library() == "trio": + await trio.sleep(seconds) + return + # Otherwise, assume asyncio + import asyncio + await asyncio.sleep(seconds) + + class AsyncRetrying(BaseRetrying): - def __init__(self, sleep=sleep, **kwargs): + def __init__(self, sleep=_portable_async_sleep, **kwargs): super(AsyncRetrying, self).__init__(**kwargs) self.sleep = sleep diff --git a/tenacity/tests/test_asyncio.py b/tenacity/tests/test_asyncio.py index 2057fd2d..c43a8e9e 100644 --- a/tenacity/tests/test_asyncio.py +++ b/tenacity/tests/test_asyncio.py @@ -17,6 +17,13 @@ import inspect import unittest +try: + import trio +except ImportError: + have_trio = False +else: + have_trio = True + import pytest import six @@ -54,7 +61,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) @@ -125,6 +132,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): diff --git a/tox.ini b/tox.ini index 20f3b12a..3414bbae 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py35, py36, py37, py38, py39, pep8, pypy +envlist = py27, py35, py36, py37, py38, py39, py39-trio, pep8, pypy [testenv] usedevelop = True @@ -14,6 +14,14 @@ commands = py3{5,6,7,8,9}: sphinx-build -a -E -W -b doctest doc/source doc/build py3{5,6,7,8,9}: sphinx-build -a -E -W -b html doc/source doc/build +[testenv:py39-trio] +deps = + .[doc] + pytest + trio + typeguard;python_version>='3.0' +commands = pytest {posargs} + [testenv:pep8] basepython = python3 deps = flake8