tenacity
Advanced tools
| --- | ||
| fixes: | ||
| - | | ||
| Passing an async ``sleep`` callable (e.g. ``trio.sleep``) to ``@retry`` | ||
| now correctly uses ``AsyncRetrying``, even when the decorated function is | ||
| synchronous. Previously, the async sleep would silently not be awaited, | ||
| resulting in no delay between retries. |
| --- | ||
| upgrade: | ||
| - | | ||
| Python 3.9 has reached end-of-life and is no longer supported. | ||
| The minimum supported version is now Python 3.10. |
| --- | ||
| other: | ||
| - | | ||
| Accept non-standard logger in helpers logging something (eg: structlog, loguru...) | ||
| --- | ||
| features: | ||
| - Python 3.14 support has been added. |
@@ -21,4 +21,2 @@ name: Continuous Integration | ||
| include: | ||
| - python: "3.9" | ||
| tox: py39 | ||
| - python: "3.10" | ||
@@ -30,11 +28,13 @@ tox: py310 | ||
| tox: py312 | ||
| - python: "3.12" | ||
| - python: "3.13" | ||
| tox: py313 | ||
| - python: "3.14" | ||
| tox: py314,py314-trio | ||
| - python: "3.14" | ||
| tox: pep8 | ||
| - python: "3.13" | ||
| tox: py313,py313-trio | ||
| - python: "3.11" | ||
| - python: "3.14" | ||
| tox: mypy | ||
| steps: | ||
| - name: Checkout 🛎️ | ||
| uses: actions/checkout@v4.2.2 | ||
| uses: actions/checkout@v6.0.2 | ||
| with: | ||
@@ -44,3 +44,3 @@ fetch-depth: 0 | ||
| - name: Setup Python 🔧 | ||
| uses: actions/setup-python@v5.5.0 | ||
| uses: actions/setup-python@v6.2.0 | ||
| with: | ||
@@ -47,0 +47,0 @@ python-version: ${{ matrix.python }} |
@@ -16,3 +16,3 @@ name: upload release to PyPI | ||
| steps: | ||
| - uses: actions/checkout@v4.2.2 | ||
| - uses: actions/checkout@v6.0.2 | ||
| with: | ||
@@ -22,5 +22,5 @@ fetch-depth: 0 | ||
| - uses: actions/setup-python@v5.5.0 | ||
| - uses: actions/setup-python@v6.2.0 | ||
| with: | ||
| python-version: 3.13 | ||
| python-version: 3.14 | ||
@@ -27,0 +27,0 @@ - name: Install build |
+4
-24
| queue_rules: | ||
| - name: default | ||
| merge_method: squash | ||
| autoqueue: true | ||
| queue_conditions: | ||
@@ -9,31 +10,10 @@ - or: | ||
| - author = dependabot[bot] | ||
| - or: | ||
| - files ~= ^releasenotes/notes/ | ||
| - label = no-changelog | ||
| - author = dependabot[bot] | ||
| - "check-success=test (3.9, py39)" | ||
| - "check-success=test (3.10, py310)" | ||
| - "check-success=test (3.11, py311)" | ||
| - "check-success=test (3.12, py312)" | ||
| - "check-success=test (3.13, py313,py313-trio)" | ||
| - "check-success=test (3.12, pep8)" | ||
| - "check-success=test (3.13, py313)" | ||
| - "check-success=test (3.14, py314,py314-trio)" | ||
| - "check-success=test (3.14, pep8)" | ||
| pull_request_rules: | ||
| - name: warn on no changelog | ||
| conditions: | ||
| - -files~=^releasenotes/notes/ | ||
| - label!=no-changelog | ||
| - -closed | ||
| actions: | ||
| comment: | ||
| message: > | ||
| ⚠️ No release notes detected. Please make sure to use | ||
| [reno](https://docs.openstack.org/reno/latest/user/usage.html) to add | ||
| a changelog entry. | ||
| - name: automatic queue | ||
| conditions: [] | ||
| actions: | ||
| queue: | ||
| - name: dismiss reviews | ||
@@ -40,0 +20,0 @@ conditions: [] |
+0
-14
@@ -640,15 +640,1 @@ Tenacity | ||
| .. _`the repository`: https://github.com/jd/tenacity | ||
| Changelogs | ||
| ~~~~~~~~~~ | ||
| `reno`_ is used for managing changelogs. Take a look at their usage docs. | ||
| The doc generation will automatically compile the changelogs. You just need to add them. | ||
| .. code-block:: sh | ||
| # Opens a template file in an editor | ||
| tox -e reno -- new some-slug-for-my-change --edit | ||
| .. _`reno`: https://docs.openstack.org/reno/latest/user/usage.html |
+3
-3
| Metadata-Version: 2.4 | ||
| Name: tenacity | ||
| Version: 9.1.2 | ||
| Version: 9.1.3 | ||
| Summary: Retry code until it succeeds | ||
@@ -14,3 +14,2 @@ Home-page: https://github.com/jd/tenacity | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
@@ -20,4 +19,5 @@ Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Classifier: Programming Language :: Python :: 3.14 | ||
| Classifier: Topic :: Utilities | ||
| Requires-Python: >=3.9 | ||
| Requires-Python: >=3.10 | ||
| License-File: LICENSE | ||
@@ -24,0 +24,0 @@ Provides-Extra: doc |
+1
-1
@@ -14,3 +14,3 @@ [build-system] | ||
| indent-width = 4 | ||
| target-version = "py39" | ||
| target-version = "py310" | ||
@@ -17,0 +17,0 @@ [tool.mypy] |
+0
-14
@@ -640,15 +640,1 @@ Tenacity | ||
| .. _`the repository`: https://github.com/jd/tenacity | ||
| Changelogs | ||
| ~~~~~~~~~~ | ||
| `reno`_ is used for managing changelogs. Take a look at their usage docs. | ||
| The doc generation will automatically compile the changelogs. You just need to add them. | ||
| .. code-block:: sh | ||
| # Opens a template file in an editor | ||
| tox -e reno -- new some-slug-for-my-change --edit | ||
| .. _`reno`: https://docs.openstack.org/reno/latest/user/usage.html |
+2
-2
@@ -16,3 +16,2 @@ [metadata] | ||
| Programming Language :: Python :: 3 :: Only | ||
| Programming Language :: Python :: 3.9 | ||
| Programming Language :: Python :: 3.10 | ||
@@ -22,2 +21,3 @@ Programming Language :: Python :: 3.11 | ||
| Programming Language :: Python :: 3.13 | ||
| Programming Language :: Python :: 3.14 | ||
| Topic :: Utilities | ||
@@ -27,3 +27,3 @@ | ||
| install_requires = | ||
| python_requires = >=3.9 | ||
| python_requires = >=3.10 | ||
| packages = find: | ||
@@ -30,0 +30,0 @@ |
| Metadata-Version: 2.4 | ||
| Name: tenacity | ||
| Version: 9.1.2 | ||
| Version: 9.1.3 | ||
| Summary: Retry code until it succeeds | ||
@@ -14,3 +14,2 @@ Home-page: https://github.com/jd/tenacity | ||
| Classifier: Programming Language :: Python :: 3 :: Only | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
@@ -20,4 +19,5 @@ Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Classifier: Programming Language :: Python :: 3.14 | ||
| Classifier: Topic :: Utilities | ||
| Requires-Python: >=3.9 | ||
| Requires-Python: >=3.10 | ||
| License-File: LICENSE | ||
@@ -24,0 +24,0 @@ Provides-Extra: doc |
@@ -33,2 +33,3 @@ .editorconfig | ||
| releasenotes/notes/annotate_code-197b93130df14042.yaml | ||
| releasenotes/notes/async-sleep-retrying-32de5866f5d041.yaml | ||
| releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml | ||
@@ -39,2 +40,3 @@ releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml | ||
| releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml | ||
| releasenotes/notes/drop-python-3.9-ecfa2d7db9773e96.yaml | ||
| releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml | ||
@@ -48,2 +50,3 @@ releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml | ||
| releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml | ||
| releasenotes/notes/logging-protocol-a4cf0f786f21e4ee.yaml | ||
| releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml | ||
@@ -57,2 +60,3 @@ releasenotes/notes/no-async-iter-6132a42e52348a75.yaml | ||
| releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml | ||
| releasenotes/notes/support-py3.14-14928188cab53b99.yaml | ||
| releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml | ||
@@ -59,0 +63,0 @@ releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml |
+35
-23
@@ -62,2 +62,3 @@ # Copyright 2016-2018 Julien Danjou | ||
| from .wait import wait_combine # noqa | ||
| from .wait import wait_exception # noqa | ||
| from .wait import wait_exponential # noqa | ||
@@ -102,10 +103,7 @@ from .wait import wait_fixed # noqa | ||
| WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any]) | ||
| P = t.ParamSpec("P") | ||
| R = t.TypeVar("R") | ||
| dataclass_kwargs = {} | ||
| if sys.version_info >= (3, 10): | ||
| dataclass_kwargs.update({"slots": True}) | ||
| @dataclasses.dataclass(**dataclass_kwargs) | ||
| @dataclasses.dataclass(slots=True) | ||
| class IterState: | ||
@@ -312,15 +310,11 @@ actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field( | ||
| """ | ||
| try: | ||
| return self._local.statistics # type: ignore[no-any-return] | ||
| except AttributeError: | ||
| if not hasattr(self._local, "statistics"): | ||
| self._local.statistics = t.cast(t.Dict[str, t.Any], {}) | ||
| return self._local.statistics | ||
| return self._local.statistics # type: ignore[no-any-return] | ||
| @property | ||
| def iter_state(self) -> IterState: | ||
| try: | ||
| return self._local.iter_state # type: ignore[no-any-return] | ||
| except AttributeError: | ||
| if not hasattr(self._local, "iter_state"): | ||
| self._local.iter_state = IterState() | ||
| return self._local.iter_state | ||
| return self._local.iter_state # type: ignore[no-any-return] | ||
@@ -495,9 +489,3 @@ def wraps(self, f: WrappedFn) -> WrappedFn: | ||
| if sys.version_info >= (3, 9): | ||
| FutureGenericT = futures.Future[t.Any] | ||
| else: | ||
| FutureGenericT = futures.Future | ||
| class Future(FutureGenericT): | ||
| class Future(futures.Future[t.Any]): | ||
| """Encapsulates a (future or past) attempted call to a target function.""" | ||
@@ -610,3 +598,23 @@ | ||
| def retry( | ||
| sleep: t.Callable[[t.Union[int, float]], t.Union[None, t.Awaitable[None]]] = sleep, | ||
| *, | ||
| sleep: t.Callable[[t.Union[int, float]], t.Awaitable[None]], | ||
| stop: "StopBaseT" = ..., | ||
| wait: "WaitBaseT" = ..., | ||
| retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = ..., | ||
| before: t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] = ..., | ||
| after: t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] = ..., | ||
| before_sleep: t.Optional[ | ||
| t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] | ||
| ] = ..., | ||
| reraise: bool = ..., | ||
| retry_error_cls: t.Type["RetryError"] = ..., | ||
| retry_error_callback: t.Optional[ | ||
| t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]] | ||
| ] = ..., | ||
| ) -> t.Callable[[t.Callable[P, R | t.Awaitable[R]]], t.Callable[P, t.Awaitable[R]]]: ... | ||
| @t.overload | ||
| def retry( | ||
| sleep: t.Callable[[t.Union[int, float]], None] = sleep, | ||
| stop: "StopBaseT" = stop_never, | ||
@@ -650,3 +658,6 @@ wait: "WaitBaseT" = wait_none(), | ||
| r: "BaseRetrying" | ||
| if _utils.is_coroutine_callable(f): | ||
| sleep = dkw.get("sleep") | ||
| if _utils.is_coroutine_callable(f) or ( | ||
| sleep is not None and _utils.is_coroutine_callable(sleep) | ||
| ): | ||
| r = AsyncRetrying(*dargs, **dkw) | ||
@@ -699,2 +710,3 @@ elif ( | ||
| "wait_combine", | ||
| "wait_exception", | ||
| "wait_exponential", | ||
@@ -701,0 +713,0 @@ "wait_fixed", |
+12
-0
@@ -28,2 +28,14 @@ # Copyright 2016 Julien Danjou | ||
| class LoggerProtocol(typing.Protocol): | ||
| """ | ||
| Protocol used by utils expecting a logger (eg: before_log). | ||
| Compatible with logging, structlog, loguru, etc... | ||
| """ | ||
| def log( | ||
| self, level: int, msg: str, /, *args: typing.Any, **kwargs: typing.Any | ||
| ) -> typing.Any: ... | ||
| def find_ordinal(pos_num: int) -> str: | ||
@@ -30,0 +42,0 @@ # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers |
@@ -22,4 +22,2 @@ # Copyright 2016 Julien Danjou | ||
| if typing.TYPE_CHECKING: | ||
| import logging | ||
| from tenacity import RetryCallState | ||
@@ -33,5 +31,5 @@ | ||
| def after_log( | ||
| logger: "logging.Logger", | ||
| logger: _utils.LoggerProtocol, | ||
| log_level: int, | ||
| sec_format: str = "%0.3f", | ||
| sec_format: str = "%.3g", | ||
| ) -> typing.Callable[["RetryCallState"], None]: | ||
@@ -38,0 +36,0 @@ """After call strategy that logs to some logger the finished attempt.""" |
@@ -110,2 +110,3 @@ # Copyright 2016 Étienne Bersac | ||
| retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) | ||
| is_async = _utils.is_coroutine_callable(fn) | ||
| while True: | ||
@@ -115,3 +116,6 @@ do = await self.iter(retry_state=retry_state) | ||
| try: | ||
| result = await fn(*args, **kwargs) | ||
| if is_async: | ||
| result = await fn(*args, **kwargs) | ||
| else: | ||
| result = fn(*args, **kwargs) | ||
| except BaseException: # noqa: B902 | ||
@@ -118,0 +122,0 @@ retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] |
@@ -22,4 +22,2 @@ # Copyright 2016 Julien Danjou | ||
| if typing.TYPE_CHECKING: | ||
| import logging | ||
| from tenacity import RetryCallState | ||
@@ -33,5 +31,6 @@ | ||
| def before_sleep_log( | ||
| logger: "logging.Logger", | ||
| logger: _utils.LoggerProtocol, | ||
| log_level: int, | ||
| exc_info: bool = False, | ||
| sec_format: str = "%.3g", | ||
| ) -> typing.Callable[["RetryCallState"], None]: | ||
@@ -70,3 +69,3 @@ """Before sleep strategy that logs to some logger the attempt.""" | ||
| f"Retrying {fn_name} " | ||
| f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.", | ||
| f"in {sec_format % retry_state.next_action.sleep} seconds as it {verb} {value}.", | ||
| exc_info=local_exc_info, | ||
@@ -73,0 +72,0 @@ ) |
@@ -22,4 +22,2 @@ # Copyright 2016 Julien Danjou | ||
| if typing.TYPE_CHECKING: | ||
| import logging | ||
| from tenacity import RetryCallState | ||
@@ -33,3 +31,3 @@ | ||
| def before_log( | ||
| logger: "logging.Logger", log_level: int | ||
| logger: _utils.LoggerProtocol, log_level: int | ||
| ) -> typing.Callable[["RetryCallState"], None]: | ||
@@ -36,0 +34,0 @@ """Before call strategy that logs to some logger the attempt.""" |
@@ -40,3 +40,3 @@ # Copyright 2017 Elisey Zanko | ||
| @gen.coroutine # type: ignore[misc] | ||
| @gen.coroutine # type: ignore[untyped-decorator] | ||
| def __call__( | ||
@@ -43,0 +43,0 @@ self, |
+42
-3
@@ -101,6 +101,6 @@ # Copyright 2016–2021 Julien Danjou | ||
| [wait_fixed(2) for j in range(5)] + | ||
| [wait_fixed(5) for k in range(4))) | ||
| [wait_fixed(5) for k in range(4)])) | ||
| def wait_chained(): | ||
| print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s | ||
| thereafter.") | ||
| print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s " | ||
| "thereafter.") | ||
| """ | ||
@@ -117,2 +117,41 @@ | ||
| class wait_exception(wait_base): | ||
| """Wait strategy that waits the amount of time returned by the predicate. | ||
| The predicate is passed the exception object. Based on the exception, the | ||
| user can decide how much time to wait before retrying. | ||
| For example:: | ||
| def http_error(exception: BaseException) -> float: | ||
| if ( | ||
| isinstance(exception, requests.HTTPError) | ||
| and exception.response.status_code == requests.codes.too_many_requests | ||
| ): | ||
| return float(exception.response.headers.get("Retry-After", "1")) | ||
| return 60.0 | ||
| @retry( | ||
| stop=stop_after_attempt(3), | ||
| wait=wait_exception(http_error), | ||
| ) | ||
| def http_get_request(url: str) -> None: | ||
| response = requests.get(url) | ||
| response.raise_for_status() | ||
| """ | ||
| def __init__(self, predicate: typing.Callable[[BaseException], float]) -> None: | ||
| self.predicate = predicate | ||
| def __call__(self, retry_state: "RetryCallState") -> float: | ||
| if retry_state.outcome is None: | ||
| raise RuntimeError("__call__() called before outcome was set") | ||
| exception = retry_state.outcome.exception() | ||
| if exception is None: | ||
| raise RuntimeError("outcome failed but the exception is None") | ||
| return self.predicate(exception) | ||
| class wait_incrementing(wait_base): | ||
@@ -119,0 +158,0 @@ """Wait an incremental amount of time after each attempt. |
@@ -30,3 +30,3 @@ # mypy: disable-error-code="no-untyped-def,no-untyped-call" | ||
| sec_format = "%0.3f" | ||
| sec_format = "%.3g" | ||
| delay_since_first_attempt = 0.1 | ||
@@ -33,0 +33,0 @@ |
@@ -37,3 +37,7 @@ # mypy: disable-error-code="no-untyped-def,no-untyped-call" | ||
| from .test_tenacity import NoIOErrorAfterCount, current_time_ms | ||
| from .test_tenacity import ( | ||
| NoIOErrorAfterCount, | ||
| NoneReturnUntilAfterCount, | ||
| current_time_ms, | ||
| ) | ||
@@ -44,4 +48,3 @@ | ||
| def wrapper(*a, **kw): | ||
| loop = asyncio.get_event_loop() | ||
| return loop.run_until_complete(callable_(*a, **kw)) | ||
| return asyncio.run(callable_(*a, **kw)) | ||
@@ -469,3 +472,24 @@ return wrapper | ||
| class TestSyncFunctionWithAsyncSleep(unittest.TestCase): | ||
| @asynctest | ||
| async def test_sync_function_with_async_sleep(self): | ||
| """A sync function with an async sleep callable uses AsyncRetrying.""" | ||
| mock_sleep = mock.AsyncMock() | ||
| thing = NoneReturnUntilAfterCount(2) | ||
| @retry( | ||
| sleep=mock_sleep, | ||
| wait=wait_fixed(1), | ||
| retry=retry_if_result(lambda x: x is None), | ||
| ) | ||
| def sync_function(): | ||
| return thing.go() | ||
| result = await sync_function() | ||
| assert result is True | ||
| assert mock_sleep.await_count == 2 | ||
| if __name__ == "__main__": | ||
| unittest.main() |
@@ -15,4 +15,3 @@ import asyncio | ||
| def wrapper(*a: typing.Any, **kw: typing.Any) -> typing.Any: | ||
| loop = asyncio.get_event_loop() | ||
| return loop.run_until_complete(callable_(*a, **kw)) | ||
| return asyncio.run(callable_(*a, **kw)) | ||
@@ -19,0 +18,0 @@ return wrapper |
+19
-10
@@ -20,3 +20,2 @@ # mypy: disable_error_code="no-untyped-def,no-untyped-call,attr-defined,arg-type,no-any-return,list-item,var-annotated,import,call-overload" | ||
| import re | ||
| import sys | ||
| import time | ||
@@ -32,2 +31,3 @@ import typing | ||
| import pytest | ||
| from typeguard import check_type | ||
@@ -374,2 +374,20 @@ import tenacity | ||
| def test_wait_exception(self): | ||
| def predicate(exc): | ||
| if isinstance(exc, ValueError): | ||
| return 3.5 | ||
| return 10.0 | ||
| r = Retrying(wait=tenacity.wait_exception(predicate)) | ||
| fut1 = tenacity.Future.construct(1, ValueError(), True) | ||
| self.assertEqual(r.wait(make_retry_state(1, 0, last_result=fut1)), 3.5) | ||
| fut2 = tenacity.Future.construct(1, KeyError(), True) | ||
| self.assertEqual(r.wait(make_retry_state(1, 0, last_result=fut2)), 10.0) | ||
| fut3 = tenacity.Future.construct(1, None, False) | ||
| with self.assertRaises(RuntimeError): | ||
| r.wait(make_retry_state(1, 0, last_result=fut3)) | ||
| def test_wait_double_sum(self): | ||
@@ -1717,14 +1735,5 @@ r = Retrying(wait=tenacity.wait_random(0, 3) + tenacity.wait_fixed(5)) | ||
| class TestRetryTyping(unittest.TestCase): | ||
| @pytest.mark.skipif( | ||
| sys.version_info < (3, 0), reason="typeguard not supported for python 2" | ||
| ) | ||
| def test_retry_type_annotations(self): | ||
| """The decorator should maintain types of decorated functions.""" | ||
| # Just in case this is run with unit-test, return early for py2 | ||
| if sys.version_info < (3, 0): | ||
| return | ||
| # Function-level import because we can't install this for python 2. | ||
| from typeguard import check_type | ||
| def num_to_str(number): | ||
@@ -1731,0 +1740,0 @@ # type: (int) -> str |
+4
-4
| [tox] | ||
| # we only test trio on latest python version | ||
| envlist = py3{9,10,11,12,13,13-trio}, pep8, pypy3 | ||
| envlist = py3{10,11,12,13,14,14-trio}, pep8, pypy3 | ||
| skip_missing_interpreters = True | ||
@@ -14,5 +14,5 @@ | ||
| commands = | ||
| py3{8,9,10,11,12,13},pypy3: pytest {posargs} | ||
| py3{8,9,10,11,12,13},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build | ||
| py3{8,9,10,11,12,13},pypy3: sphinx-build -a -E -W -b html doc/source doc/build | ||
| py3{10,11,12,13,14},pypy3: pytest {posargs} | ||
| py3{10,11,12,13,14},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build | ||
| py3{10,11,12,13,14},pypy3: sphinx-build -a -E -W -b html doc/source doc/build | ||
@@ -19,0 +19,0 @@ [testenv:pep8] |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
224018
1.35%88
4.76%3813
2.03%