From 33e0192c7ddd0049bb8688d1e32d36d2be5d90f2 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 2 Dec 2023 16:04:42 +0100 Subject: [PATCH 1/6] Add Trio support Ref #42 --- CHANGELOG.md | 4 ++++ README.md | 2 +- noxfile.py | 4 +++- pyproject.toml | 4 ++-- src/stamina/_core.py | 28 +++++++++++++++++++++++++++- tests/conftest.py | 12 ++++++++++++ tests/test_async.py | 3 +++ tests/test_instrumentation.py | 1 + tests/test_structlog.py | 2 ++ 9 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eca23b1..e0efd2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ ## [Unreleased](https://github.com/hynek/stamina/compare/23.2.0...HEAD) +### Added + +- [Trio](https://trio.readthedocs.io/) support! + ## [23.2.0](https://github.com/hynek/stamina/compare/23.1.0...23.2.0) - 2023-10-30 diff --git a/README.md b/README.md index d15b4b0..ac12483 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ It is the result of years of copy-pasting the same configuration over and over a - Retry only on certain exceptions. - Exponential **backoff** with **jitter** between retries. - Limit the number of retries **and** total time. -- Automatic **async** support. +- Automatic **async** support -- including [Trio](https://trio.readthedocs.io/en/stable/). - Preserve **type hints** of the decorated callable. - Flexible **instrumentation** with [Prometheus](https://github.com/prometheus/client_python), [*structlog*](https://www.structlog.org/), and standard library's `logging` support out-of-the-box. - Easy _global_ deactivation for testing. diff --git a/noxfile.py b/noxfile.py index b67d52b..3bb1a0d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -69,7 +69,9 @@ def _get_pkg(posargs: list[str]) -> tuple[str, list[str]]: @nox.session(python=ALL_SUPPORTED) -@nox.parametrize("opt_deps", [[], ["structlog", "prometheus-client"]]) +@nox.parametrize( + "opt_deps", [[], ["structlog", "prometheus-client"], ["trio"]] +) def tests(session: nox.Session, opt_deps: list[str]) -> None: pkg, posargs = _get_pkg(session.posargs) diff --git a/pyproject.toml b/pyproject.toml index 2585581..a1e4cdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ dependencies = ["tenacity", "typing-extensions; python_version<'3.10'"] [project.optional-dependencies] -tests = ["pytest", "pytest-asyncio!=0.22.0"] +tests = ["pytest", "anyio"] typing = ["mypy>=1.4"] docs = [ "sphinx>=7.2.2", @@ -41,6 +41,7 @@ dev = [ "tomli; python_version<'3.11'", "structlog", "prometheus-client", + "trio", ] [project.urls] @@ -100,7 +101,6 @@ addopts = ["-ra", "--strict-markers", "--strict-config"] testpaths = "tests" xfail_strict = true filterwarnings = ["once::Warning"] -asyncio_mode = "auto" [tool.coverage.run] diff --git a/src/stamina/_core.py b/src/stamina/_core.py index 6ae223f..6acb564 100644 --- a/src/stamina/_core.py +++ b/src/stamina/_core.py @@ -25,6 +25,29 @@ else: from typing_extensions import ParamSpec +try: + from sniffio import current_async_library +except ImportError: # pragma: no cover -- we always have sniffio in tests + + def current_async_library() -> str: + return "asyncio" + + +async def _smart_sleep(delay: float) -> None: + io = current_async_library() + + if io == "asyncio": + import asyncio + + await asyncio.sleep(delay) + elif io == "trio": + import trio + + await trio.sleep(delay) + else: # pragma: no cover + msg = f"Unknown async library: {io!r}" + raise RuntimeError(msg) + T = TypeVar("T") P = ParamSpec("P") @@ -113,7 +136,9 @@ class _LazyNoAsyncRetry: __slots__ = () def __aiter__(self) -> _t.AsyncRetrying: - return _t.AsyncRetrying(reraise=True, stop=_STOP_NO_RETRY).__aiter__() + return _t.AsyncRetrying( + reraise=True, stop=_STOP_NO_RETRY, sleep=_smart_sleep + ).__aiter__() _LAZY_NO_ASYNC_RETRY = _LazyNoAsyncRetry() @@ -197,6 +222,7 @@ def __iter__(self) -> Iterator[Attempt]: def __aiter__(self) -> AsyncIterator[Attempt]: if CONFIG.is_active: self._t_a_retrying = _t.AsyncRetrying( + sleep=_smart_sleep, before_sleep=_make_before_sleep( self._name, CONFIG, self._args, self._kw ), diff --git a/tests/conftest.py b/tests/conftest.py index 8ed65c6..1f19af9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: MIT +import importlib.util + import pytest import stamina._config @@ -14,3 +16,13 @@ def _reset_config(): """ stamina.set_active(True) stamina.instrumentation.set_on_retry_hooks(None) + + +BACKENDS = [pytest.param(("asyncio", {}), id="asyncio")] +if importlib.util.find_spec("trio"): + BACKENDS += [pytest.param(("trio", {}), id="trio")] + + +@pytest.fixture(params=BACKENDS) +def anyio_backend(request): + return request.param diff --git a/tests/test_async.py b/tests/test_async.py index cf08fdd..66bd6b6 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -9,6 +9,9 @@ import stamina +pytestmark = pytest.mark.anyio + + @pytest.mark.parametrize("attempts", [None, 1]) @pytest.mark.parametrize("timeout", [None, 1, dt.timedelta(days=1)]) @pytest.mark.parametrize("duration", [1, dt.timedelta(days=1)]) diff --git a/tests/test_instrumentation.py b/tests/test_instrumentation.py index a0a0016..5aa3b14 100644 --- a/tests/test_instrumentation.py +++ b/tests/test_instrumentation.py @@ -135,6 +135,7 @@ def f(): ("stamina", 30, "stamina.retry_scheduled") ] == caplog.record_tuples + @pytest.mark.anyio() async def test_async(self, caplog): """ Async retries are logged. diff --git a/tests/test_structlog.py b/tests/test_structlog.py index 443b399..680cff1 100644 --- a/tests/test_structlog.py +++ b/tests/test_structlog.py @@ -47,6 +47,7 @@ def f(): ] == log_output +@pytest.mark.anyio() async def test_decorator_async(log_output): """ Retries decorators log correct name / arguments. @@ -97,6 +98,7 @@ def test_context_sync(log_output): ] == log_output +@pytest.mark.anyio() async def test_context_async(log_output): """ Retries context blocks log correct name / arguments. From f213afaf199d92091961d081ad46b40259234349 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 2 Dec 2023 16:13:03 +0100 Subject: [PATCH 2/6] Add versionadded tags --- src/stamina/_core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/stamina/_core.py b/src/stamina/_core.py index 6acb564..5f061ea 100644 --- a/src/stamina/_core.py +++ b/src/stamina/_core.py @@ -69,6 +69,7 @@ def retry_context( Arguments have the same meaning as for :func:`stamina.retry`. .. versionadded:: 23.1.0 + .. versionadded:: 23.3.0 `Trio `_ support. """ return _RetryContextIterator.from_params( @@ -349,6 +350,8 @@ def retry( All time-related parameters can now be specified as a :class:`datetime.timedelta`. + + .. versionadded:: 23.3.0 `Trio `_ support. """ retry_ctx = _RetryContextIterator.from_params( on=on, From cf9643f7fa28bbbbd2d5455f5fe7dd01c8b2271e Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sat, 2 Dec 2023 16:46:46 +0100 Subject: [PATCH 3/6] Add PR# --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0efd2c..b58b167 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ ### Added - [Trio](https://trio.readthedocs.io/) support! + [#43](https://github.com/hynek/stamina/pull/43) ## [23.2.0](https://github.com/hynek/stamina/compare/23.1.0...23.2.0) - 2023-10-30 From 7d5044d1d8b6842fd56f246d26112513928d47b9 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 3 Dec 2023 09:28:16 +0100 Subject: [PATCH 4/6] Use consistently bare Trio URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac12483..5346e70 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ It is the result of years of copy-pasting the same configuration over and over a - Retry only on certain exceptions. - Exponential **backoff** with **jitter** between retries. - Limit the number of retries **and** total time. -- Automatic **async** support -- including [Trio](https://trio.readthedocs.io/en/stable/). +- Automatic **async** support -- including [Trio](https://trio.readthedocs.io/). - Preserve **type hints** of the decorated callable. - Flexible **instrumentation** with [Prometheus](https://github.com/prometheus/client_python), [*structlog*](https://www.structlog.org/), and standard library's `logging` support out-of-the-box. - Easy _global_ deactivation for testing. From a8c850787b668d376391eb3ad48775d893e9b3d4 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 3 Dec 2023 09:30:36 +0100 Subject: [PATCH 5/6] Mention Trio in tutorial --- docs/tutorial.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index dec063d..5879f06 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -49,7 +49,8 @@ for attempt in stamina.retry_context(on=httpx.HTTPError): ## Async -Async works with the same functions and arguments -- you just have to use async functions and `async for`: +Async works with the same functions and arguments for both [`asyncio`](https://docs.python.org/3/library/asyncio.html) and [Trio](https://trio.readthedocs.io/). +Just use async functions and `async for`: ```python import datetime as dt From 8e2fef827ddabb6838628102a691b8012b826be1 Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 3 Dec 2023 09:32:41 +0100 Subject: [PATCH 6/6] Be smart --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5346e70..a73bc63 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ It is the result of years of copy-pasting the same configuration over and over a - Retry only on certain exceptions. - Exponential **backoff** with **jitter** between retries. - Limit the number of retries **and** total time. -- Automatic **async** support -- including [Trio](https://trio.readthedocs.io/). +- Automatic **async** support – including [Trio](https://trio.readthedocs.io/). - Preserve **type hints** of the decorated callable. - Flexible **instrumentation** with [Prometheus](https://github.com/prometheus/client_python), [*structlog*](https://www.structlog.org/), and standard library's `logging` support out-of-the-box. - Easy _global_ deactivation for testing.