From 48720ee744c11f13e9bdecd69d8d4c712478cf8d Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 25 Apr 2021 15:25:58 -0700 Subject: [PATCH 1/9] Support Trio out-of-the-box This PR makes `@retry` just work when running under Trio. --- doc/source/index.rst | 18 +++++++++----- .../notes/trio-support-62fed9e32ccb62be.yaml | 6 +++++ tenacity/_asyncio.py | 18 ++++++++++++-- tenacity/tests/test_asyncio.py | 24 ++++++++++++++++++- tox.ini | 1 + 5 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/trio-support-62fed9e32ccb62be.yaml 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..a16b21a8 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ sitepackages = False deps = .[doc] pytest + trio typeguard;python_version>='3.0' commands = py{27,py}: pytest --ignore='tenacity/tests/test_asyncio.py' {posargs} From cfe30eff05433f2f010880523763d5b7389fca54 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 27 Apr 2021 09:07:55 -0700 Subject: [PATCH 2/9] Add a no-trio test environment --- .circleci/config.yml | 10 ++++++++++ tox.ini | 9 ++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) 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/tox.ini b/tox.ini index a16b21a8..6e99dc70 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-notrio, pep8, pypy [testenv] usedevelop = True @@ -15,6 +15,13 @@ 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-notrio] +deps = + .[doc] + pytest + typeguard;python_version>='3.0' +commands = pytest {posargs} + [testenv:pep8] basepython = python3 deps = flake8 From bb87acc44ddcb445f88115cb1a13fa7c6e01cc91 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 27 Apr 2021 15:31:38 -0700 Subject: [PATCH 3/9] Switch to only testing trio in one environment --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 6e99dc70..3414bbae 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py35, py36, py37, py38, py39, py39-notrio, pep8, pypy +envlist = py27, py35, py36, py37, py38, py39, py39-trio, pep8, pypy [testenv] usedevelop = True @@ -7,7 +7,6 @@ sitepackages = False deps = .[doc] pytest - trio typeguard;python_version>='3.0' commands = py{27,py}: pytest --ignore='tenacity/tests/test_asyncio.py' {posargs} @@ -15,10 +14,11 @@ 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-notrio] +[testenv:py39-trio] deps = .[doc] pytest + trio typeguard;python_version>='3.0' commands = pytest {posargs} From ff76c621b5de2c0016307f50a7e53efb338e0d46 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 24 May 2024 11:41:43 +0200 Subject: [PATCH 4/9] bump releasenote so it is later in history->reno puts it in the correct place in the changelog --- ...d9e32ccb62be.yaml => trio-support-retry-22bd544800cd1f36.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename releasenotes/notes/{trio-support-62fed9e32ccb62be.yaml => trio-support-retry-22bd544800cd1f36.yaml} (100%) diff --git a/releasenotes/notes/trio-support-62fed9e32ccb62be.yaml b/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml similarity index 100% rename from releasenotes/notes/trio-support-62fed9e32ccb62be.yaml rename to releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml From 121a1533b0a6f5d955e7cb91e853938cfb88f928 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Fri, 24 May 2024 11:49:17 +0200 Subject: [PATCH 5/9] fix mypy & pep8 checks --- tenacity/_asyncio.py | 7 +++++-- tests/test_tornado.py | 1 + tox.ini | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tenacity/_asyncio.py b/tenacity/_asyncio.py index 652ecefc..5f29b152 100644 --- a/tenacity/_asyncio.py +++ b/tenacity/_asyncio.py @@ -30,19 +30,22 @@ WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]]) -async def _portable_async_sleep(seconds): +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, sniffio + import trio + import sniffio + if sniffio.current_async_library() == "trio": await trio.sleep(seconds) return # Otherwise, assume asyncio # Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead). import asyncio + await asyncio.sleep(seconds) diff --git a/tests/test_tornado.py b/tests/test_tornado.py index 7217b0a1..39050c78 100644 --- a/tests/test_tornado.py +++ b/tests/test_tornado.py @@ -76,5 +76,6 @@ def retryable(thing): def runTest(self): self.subTest() + if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini index a1a21adc..14f8ae00 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ commands = deps = mypy>=1.0.0 pytest # for stubs + trio commands = mypy {posargs} From 96881727cd92a5498006e647a3cee296cea56e20 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:17:05 +0200 Subject: [PATCH 6/9] Update doc/source/index.rst fix example Co-authored-by: Julien Danjou --- doc/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index af202897..b1632d51 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -580,7 +580,7 @@ Sleeps are done asynchronously too. .. code-block:: python @retry - def my_async_trio_function(): + async def my_async_trio_function(): await trio.socket.getaddrinfo('8.8.8.8', 53) .. code-block:: python From 1e0b36dfa8c23ff4d652c1596b452f3afa82fa9e Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Wed, 12 Jun 2024 15:44:45 +0200 Subject: [PATCH 7/9] Update tests/test_tornado.py --- tests/test_tornado.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_tornado.py b/tests/test_tornado.py index 39050c78..2a544818 100644 --- a/tests/test_tornado.py +++ b/tests/test_tornado.py @@ -73,8 +73,6 @@ def retryable(thing): gen.is_coroutine_function = old_attr # temporary workaround for https://github.com/jd/tenacity/issues/460 - def runTest(self): - self.subTest() if __name__ == "__main__": From 45c7cf5b81be004d52f239088bf379c57e5a8b50 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Wed, 12 Jun 2024 15:44:58 +0200 Subject: [PATCH 8/9] Update tests/test_tornado.py --- tests/test_tornado.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_tornado.py b/tests/test_tornado.py index 2a544818..24858cff 100644 --- a/tests/test_tornado.py +++ b/tests/test_tornado.py @@ -72,8 +72,6 @@ def retryable(thing): finally: gen.is_coroutine_function = old_attr - # temporary workaround for https://github.com/jd/tenacity/issues/460 - if __name__ == "__main__": unittest.main() From 7106f54880b410ffe272b48dd4e7d8207b6f0fcb Mon Sep 17 00:00:00 2001 From: jakkdl Date: Thu, 13 Jun 2024 16:01:38 +0200 Subject: [PATCH 9/9] make _portably_async_sleep a sync function that returns an async function --- tenacity/_asyncio.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tenacity/_asyncio.py b/tenacity/_asyncio.py index 5f29b152..5c9d618d 100644 --- a/tenacity/_asyncio.py +++ b/tenacity/_asyncio.py @@ -30,7 +30,7 @@ WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]]) -async def _portable_async_sleep(seconds: float) -> 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. @@ -40,13 +40,12 @@ async def _portable_async_sleep(seconds: float) -> None: import sniffio if sniffio.current_async_library() == "trio": - await trio.sleep(seconds) - return + return trio.sleep(seconds) # Otherwise, assume asyncio # Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead). import asyncio - await asyncio.sleep(seconds) + return asyncio.sleep(seconds) class AsyncRetrying(BaseRetrying):