From 28bf0079bb6a4f1ee4531d1a414085dfc27477c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= Date: Fri, 28 Aug 2026 16:41:14 -0600 Subject: [PATCH] feat: Full typing coverage and type regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Edgar Ramírez Mondragón --- .github/workflows/ci.yaml | 6 +- backoff/_async.py | 3 +- backoff/_common.py | 24 +-- backoff/_decorator.py | 10 +- backoff/_sync.py | 3 +- backoff/_typing.py | 22 +-- pyproject.toml | 25 ++- tests/test_backoff.py | 155 ++++++++++-------- tests/test_backoff_async.py | 87 +++++----- tests/test_integration.py | 8 +- tests/test_retry_context.py | 2 +- tests/typing/__init__.py | 0 tests/{test_typing.py => typing/basic.py} | 6 +- .../decorators.py} | 0 .../{typing_pyrefly.py => typing/pyrefly.py} | 8 +- 15 files changed, 193 insertions(+), 166 deletions(-) create mode 100644 tests/typing/__init__.py rename tests/{test_typing.py => typing/basic.py} (91%) rename tests/{typing_decorators.py => typing/decorators.py} (100%) rename tests/{typing_pyrefly.py => typing/pyrefly.py} (62%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7bf37de..a4a4ee3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -62,8 +62,6 @@ jobs: - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Install tox run: uv tool install --with tox-uv tox - - name: Type checks - run: tox -e typing - name: Lint code run: tox -e lint @@ -83,6 +81,10 @@ jobs: with: persist-credentials: false - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + - name: Type checks + env: + TOXENV: typing-${{ matrix.python-version }} + run: uvx --with tox-uv tox - name: Run tests env: TOXENV: ${{ matrix.python-version }} diff --git a/backoff/_async.py b/backoff/_async.py index 14e3fae..094c9f8 100644 --- a/backoff/_async.py +++ b/backoff/_async.py @@ -73,9 +73,8 @@ async def _call_handlers( "kwargs": kwargs, "tries": tries, "elapsed": elapsed, + **extra, } - # pyrefly: ignore [no-matching-overload] - details.update(extra) for handler in handlers: await handler(details) diff --git a/backoff/_common.py b/backoff/_common.py index 81c8f81..6a41f91 100644 --- a/backoff/_common.py +++ b/backoff/_common.py @@ -13,6 +13,8 @@ from backoff._typing import ( ContextDetails, Details, + _AnyLogger, + _AnyLoggerOrName, _ContextHandler, _Jitterer, _MaybeCallable, @@ -166,9 +168,7 @@ def _dispatch_handlers(handlers: Iterable[_ContextHandler], **details: Any) -> N hdlr(details) # type: ignore[arg-type] # ty:ignore[invalid-argument-type] -def _prepare_logger( - logger: str | logging.Logger | logging.LoggerAdapter | None, -) -> logging.Logger | logging.LoggerAdapter | None: +def _prepare_logger(logger: _AnyLoggerOrName | None) -> _AnyLogger | None: if isinstance(logger, str): logger = logging.getLogger(logger) return logger @@ -180,7 +180,7 @@ def _config_handlers( user_handlers: _HandlerT | Iterable[_HandlerT] | None, *, default_handler: Callable[..., None] | None = None, - logger: logging.Logger | logging.LoggerAdapter | None = None, + logger: _AnyLogger | None = None, log_level: int | None = None, ) -> list[_HandlerT]: handlers: list[_HandlerT] = [] @@ -213,11 +213,7 @@ def _config_handlers( # Default backoff handler -def _log_backoff( - details: Details, - logger: logging.Logger | logging.LoggerAdapter, - log_level: int, -) -> None: +def _log_backoff(details: Details, logger: _AnyLogger, log_level: int) -> None: msg = "Backing off %s(...) for %.1fs (%s)" log_args = [details["target"].__name__, details["wait"]] # ty:ignore[unresolved-attribute] @@ -231,11 +227,7 @@ def _log_backoff( # Default giveup handler -def _log_giveup( - details: Details, - logger: logging.Logger | logging.LoggerAdapter, - log_level: int, -) -> None: +def _log_giveup(details: Details, logger: _AnyLogger, log_level: int) -> None: msg = "Giving up %s(...) after %d tries (%s)" log_args = [details["target"].__name__, details["tries"]] # ty:ignore[unresolved-attribute] @@ -254,7 +246,7 @@ def _log_giveup( # directly since it's no longer the active exception by this point). def _log_backoff_context( details: ContextDetails, - logger: logging.Logger | logging.LoggerAdapter, + logger: _AnyLogger, log_level: int, ) -> None: logger.log( @@ -268,7 +260,7 @@ def _log_backoff_context( # Default giveup handler for retry_context/aretry_context. def _log_giveup_context( details: ContextDetails, - logger: logging.Logger | logging.LoggerAdapter, + logger: logging.Logger | logging.LoggerAdapter[Any], log_level: int, ) -> None: logger.log( diff --git a/backoff/_decorator.py b/backoff/_decorator.py index 7f4fe6b..a6235eb 100644 --- a/backoff/_decorator.py +++ b/backoff/_decorator.py @@ -22,12 +22,12 @@ from backoff._common import _Attempt from backoff._typing import ( + _AnyLoggerOrName, _CallableT, _ContextHandler, _Handler, _Jitterer, _MaybeCallable, - _MaybeLogger, _MaybeTuple, _Predicate, _WaitGenerator, @@ -45,7 +45,7 @@ def on_predicate( on_success: _Handler | Iterable[_Handler] | None = None, on_backoff: _Handler | Iterable[_Handler] | None = None, on_giveup: _Handler | Iterable[_Handler] | None = None, - logger: _MaybeLogger = "backoff", + logger: _AnyLoggerOrName | None = "backoff", backoff_log_level: int = logging.INFO, giveup_log_level: int = logging.ERROR, **wait_gen_kwargs: Any, @@ -168,7 +168,7 @@ def on_exception( on_backoff: _Handler | Iterable[_Handler] | None = None, on_giveup: _Handler | Iterable[_Handler] | None = None, raise_on_giveup: bool = True, - logger: _MaybeLogger = "backoff", + logger: _AnyLoggerOrName | None = "backoff", backoff_log_level: int = logging.INFO, giveup_log_level: int = logging.ERROR, **wait_gen_kwargs: Any, @@ -297,7 +297,7 @@ def retry_context( on_backoff: _ContextHandler | Iterable[_ContextHandler] | None = None, on_giveup: _ContextHandler | Iterable[_ContextHandler] | None = None, raise_on_giveup: bool = True, - logger: _MaybeLogger = "backoff", + logger: _AnyLoggerOrName | None = "backoff", backoff_log_level: int = logging.INFO, giveup_log_level: int = logging.ERROR, **wait_gen_kwargs: Any, @@ -396,7 +396,7 @@ def aretry_context( on_backoff: _ContextHandler | Iterable[_ContextHandler] | None = None, on_giveup: _ContextHandler | Iterable[_ContextHandler] | None = None, raise_on_giveup: bool = True, - logger: _MaybeLogger = "backoff", + logger: _AnyLoggerOrName | None = "backoff", backoff_log_level: int = logging.INFO, giveup_log_level: int = logging.ERROR, **wait_gen_kwargs: Any, diff --git a/backoff/_sync.py b/backoff/_sync.py index 56fe5d4..dab9474 100644 --- a/backoff/_sync.py +++ b/backoff/_sync.py @@ -49,9 +49,8 @@ def _call_handlers( "kwargs": kwargs, "tries": tries, "elapsed": elapsed, + **extra, } - # pyrefly: ignore [no-matching-overload] - details.update(extra) for hdlr in hdlrs: hdlr(details) diff --git a/backoff/_typing.py b/backoff/_typing.py index f2e1848..4524526 100644 --- a/backoff/_typing.py +++ b/backoff/_typing.py @@ -4,14 +4,6 @@ from collections.abc import Callable, Coroutine, Generator from typing import Any, TypeAlias, TypedDict, TypeVar -__all__ = [ - "_ContextHandler", - "_Handler", - "_MaybeCallable", - "_MaybeLogger", - "_MaybeTuple", -] - class _BaseDetails(TypedDict): target: Callable[..., Any] @@ -38,6 +30,7 @@ class _BaseContextDetails(TypedDict): class _ContextCallDetails(TypedDict, total=False): wait: float # present in the on_backoff handler case + value: Any # present in the on_predicate decorator case exception: Exception # present in the on_giveup handler case @@ -50,17 +43,16 @@ class ContextDetails(_BaseContextDetails, _ContextCallDetails, total=False): T = TypeVar("T") +_AnyLogger = logging.Logger | logging.LoggerAdapter # type: ignore[type-arg] +_AnyLoggerOrName = str | _AnyLogger _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) # ruff:ignore[unused-private-type-var] -_Handler: TypeAlias = ( - Callable[[Details], None] | Callable[[Details], Coroutine[Any, Any, None]] -) -_ContextHandler: TypeAlias = ( +_ContextHandler = ( Callable[[ContextDetails], None] | Callable[[ContextDetails], Coroutine[Any, Any, None]] ) +_Handler = Callable[[Details], None] | Callable[[Details], Coroutine[Any, Any, None]] _Jitterer = Callable[[float], float] -_MaybeCallable: TypeAlias = T | Callable[[], T] -_MaybeLogger: TypeAlias = str | logging.Logger | logging.LoggerAdapter | None -_MaybeTuple: TypeAlias = T | tuple[T, ...] +_MaybeCallable: TypeAlias = T | Callable[[], T] # ruff: ignore[unused-private-type-alias] +_MaybeTuple: TypeAlias = T | tuple[T, ...] # ruff: ignore[unused-private-type-alias] _Predicate = Callable[[T], bool] | Callable[[T], Coroutine[Any, Any, bool]] _WaitGenerator = Callable[..., Generator[float, Any, None]] diff --git a/pyproject.toml b/pyproject.toml index ffcb6a9..147099f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,10 @@ env_list = [ ] }, "coverage", "lint", - "typing", + { product = [ + [ "typing" ], + { prefix = "3.", start = 10 }, + ] }, "docs", ] @@ -132,8 +135,11 @@ commands = [ ], ] -[tool.tox.env.typing] -description = "run type checking" +[tool.tox.env_base.typing] +factors = [ + { prefix = "3.", start = 9 }, +] +description = "run type checking on Python {py_dot_ver}" dependency_groups = [ "typing" ] labels = [ "check" ] commands = [ @@ -150,7 +156,7 @@ commands = [ "--strict", "--disallow-any-decorated", "--follow-imports=silent", - "tests/typing_decorators.py", + "tests/typing", ], [ "ty", @@ -170,6 +176,16 @@ commands = [ "tests", ], extend = true }, ], + [ + "pyrefly", + "coverage", + "check", + { replace = "if", condition = "env.GITHUB_ACTIONS == 'true'", then = [ "--output-format=github" ], else = [], extend = true }, + { replace = "posargs", default = [ + "backoff", + "tests", + ], extend = true }, + ], ] [tool.tox.env.docs] @@ -232,6 +248,7 @@ ignore = [ [tool.mypy] check_untyped_defs = true follow_untyped_imports = true +strict = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true diff --git a/tests/test_backoff.py b/tests/test_backoff.py index 4a99adf..d2d031b 100644 --- a/tests/test_backoff.py +++ b/tests/test_backoff.py @@ -7,7 +7,7 @@ import sys import threading import unittest.mock -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Literal import pytest from dirty_equals import IsFloat, IsInstance @@ -16,7 +16,7 @@ from tests.common import EventAppender, _save_target if TYPE_CHECKING: - from collections.abc import Generator + from collections.abc import Callable, Generator from backoff._typing import Details @@ -28,7 +28,7 @@ def _patch_sleep(monkeypatch: pytest.MonkeyPatch) -> None: def test_on_predicate() -> None: @backoff.on_predicate(backoff.expo) - def return_true(log: list[bool], n): + def return_true(log: list[bool], n: int) -> bool: val = len(log) == n - 1 log.append(val) return val @@ -41,7 +41,7 @@ def return_true(log: list[bool], n): def test_on_predicate_max_tries() -> None: @backoff.on_predicate(backoff.expo, jitter=None, max_tries=3) - def return_true(log: list[bool], n): + def return_true(log: list[bool], n: int) -> bool: val = len(log) == n log.append(val) return val @@ -60,17 +60,17 @@ def test_on_predicate_max_time(monkeypatch: pytest.MonkeyPatch) -> None: 0, ] - def monotonic(): + def monotonic() -> float: return nows.pop() monkeypatch.setattr("time.monotonic", monotonic) - def giveup(details): + def giveup(details: Details) -> None: assert details["tries"] == 3 assert details["elapsed"] == pytest.approx(10.000005) @backoff.on_predicate(backoff.expo, jitter=None, max_time=10, on_giveup=giveup) - def return_true(log: list[bool], n): + def return_true(log: list[bool], n: int) -> bool: val = len(log) == n log.append(val) return val @@ -89,16 +89,16 @@ def test_on_predicate_max_time_callable(monkeypatch: pytest.MonkeyPatch) -> None 0, ] - def monotonic(): + def monotonic() -> float: return nows.pop() monkeypatch.setattr("time.monotonic", monotonic) - def giveup(details): + def giveup(details: Details) -> None: assert details["tries"] == 3 assert details["elapsed"] == pytest.approx(10.000005) - def lookup_max_time(): + def lookup_max_time() -> int: return 10 @backoff.on_predicate( @@ -107,7 +107,7 @@ def lookup_max_time(): max_time=lookup_max_time, on_giveup=giveup, ) - def return_true(log: list[bool], n): + def return_true(log: list[bool], n: int) -> bool: val = len(log) == n log.append(val) return val @@ -120,7 +120,7 @@ def return_true(log: list[bool], n): def test_on_exception() -> None: @backoff.on_exception(backoff.expo, KeyError) - def keyerror_then_true(log: list[Exception], n): + def keyerror_then_true(log: list[Exception], n: int) -> Literal[True]: if len(log) == n: return True e = KeyError() @@ -134,7 +134,7 @@ def keyerror_then_true(log: list[Exception], n): def test_on_exception_tuple() -> None: @backoff.on_exception(backoff.expo, (KeyError, ValueError)) - def keyerror_valueerror_then_true(log: list[Exception]): + def keyerror_valueerror_then_true(log: list[Exception]) -> Literal[True]: e: Exception if len(log) == 2: return True @@ -154,7 +154,11 @@ def keyerror_valueerror_then_true(log: list[Exception]): def test_on_exception_max_tries() -> None: @backoff.on_exception(backoff.expo, KeyError, jitter=None, max_tries=3) - def keyerror_then_true(log: list[Exception], n, foo=None): + def keyerror_then_true( + log: list[Exception], + n: int, + foo: str | None = None, + ) -> Literal[True]: if len(log) == n: return True e = KeyError() @@ -170,7 +174,11 @@ def keyerror_then_true(log: list[Exception], n, foo=None): def test_on_exception_max_tries_callable() -> None: @backoff.on_exception(backoff.expo, KeyError, jitter=None, max_tries=lambda: 3) - def keyerror_then_true(log: list[Exception], n, foo=None): + def keyerror_then_true( + log: list[Exception], + n: int, + foo: str | None = None, + ) -> Literal[True]: if len(log) == n: return True e = KeyError() @@ -187,7 +195,7 @@ def keyerror_then_true(log: list[Exception], n, foo=None): def test_on_exception_constant_iterable() -> None: appender = EventAppender() - def on_backoff(details: Details): + def on_backoff(details: Details) -> None: backoffs = appender.events["backoff"] assert details["tries"] == len(backoffs) + 1 assert "exception" in details @@ -195,7 +203,7 @@ def on_backoff(details: Details): backoffs.append(details) - def on_giveup(details: Details): + def on_giveup(details: Details) -> None: giveups = appender.events["giveup"] assert details["tries"] == 4 assert "exception" in details @@ -203,12 +211,12 @@ def on_giveup(details: Details): giveups.append(details) - def on_success(details: Details): + def on_success(details: Details) -> None: successes = appender.events["success"] successes.append(details) - def on_try(details: Details): + def on_try(details: Details) -> None: tries = appender.events["try"] tries.append(details) @@ -222,7 +230,7 @@ def on_try(details: Details): on_success=on_success, on_try=on_try, ) - def endless_exceptions(): + def endless_exceptions() -> None: raise KeyError("foo") with pytest.raises(KeyError): @@ -248,7 +256,7 @@ def test_on_exception_success_random_jitter(appender: EventAppender) -> None: factor=0.5, ) @_save_target - def succeeder(*args, **kwargs): + def succeeder(*args: Any, **kwargs: Any) -> None: # succeed after we've backed off twice if len(appender.events["backoff"]) < 2: raise ValueError("catch me") @@ -280,7 +288,7 @@ def test_on_exception_success_full_jitter(appender: EventAppender) -> None: factor=0.5, ) @_save_target - def succeeder(*args, **kwargs): + def succeeder(*args: Any, **kwargs: Any) -> None: # succeed after we've backed off twice if len(appender.events["backoff"]) < 2: raise ValueError("catch me") @@ -312,7 +320,7 @@ def test_on_exception_success(appender: EventAppender) -> None: interval=0, ) @_save_target - def succeeder(*args, **kwargs): + def succeeder(*args: Any, **kwargs: Any) -> None: # succeed after we've backed off twice if len(appender.events["backoff"]) < 2: raise ValueError("catch me") @@ -364,7 +372,7 @@ def test_on_exception_on_try_runs_before_attempt() -> None: interval=0, max_tries=3, ) - def fails(): + def fails() -> None: calls.append("call") raise ValueError("nope") @@ -396,7 +404,7 @@ def test_on_exception_giveup(raise_on_giveup: bool, appender: EventAppender) -> interval=0, ) @_save_target - def exceptor(*args, **kwargs): + def exceptor(*args: Any, **kwargs: Any) -> None: raise ValueError("catch me") if raise_on_giveup: @@ -425,13 +433,13 @@ def exceptor(*args, **kwargs): def test_on_exception_giveup_predicate() -> None: - def on_baz(e): + def on_baz(e: Exception) -> bool: return str(e) == "baz" vals = ["baz", "bar", "foo"] @backoff.on_exception(backoff.constant, ValueError, giveup=on_baz) - def foo_bar_baz(): + def foo_bar_baz() -> None: raise ValueError(vals.pop()) with pytest.raises(ValueError, match=r"(baz|bar|foo)"): @@ -451,7 +459,7 @@ def test_on_predicate_success(appender: EventAppender) -> None: interval=0, ) @_save_target - def success(*args, **kwargs): + def success(*args: Any, **kwargs: Any) -> bool: # succeed after we've backed off twice return len(appender.events["backoff"]) == 2 @@ -503,7 +511,7 @@ def test_on_predicate_on_try_runs_before_attempt() -> None: interval=0, max_tries=3, ) - def falsey(): + def falsey() -> Literal[False]: calls.append("call") return False @@ -531,7 +539,7 @@ def test_on_predicate_giveup(appender: EventAppender) -> None: interval=0, ) @_save_target - def emptiness(*args, **kwargs): + def emptiness(*args: Any, **kwargs: Any) -> None: pass emptiness(1, 2, 3, foo=1, bar=2) @@ -557,7 +565,7 @@ def emptiness(*args, **kwargs): def test_on_predicate_iterable_handlers() -> None: class Logger: - def __init__(self): + def __init__(self) -> None: self.appender = EventAppender() loggers = [Logger() for _ in range(3)] @@ -573,7 +581,7 @@ def __init__(self): interval=0, ) @_save_target - def emptiness(*args, **kwargs): + def emptiness(*args: Any, **kwargs: Any) -> None: pass emptiness(1, 2, 3, foo=1, bar=2) @@ -609,7 +617,7 @@ def test_on_exception_jitter(appender: EventAppender) -> None: interval=0, ) @_save_target - def succeeder(*args, **kwargs): + def succeeder(*args: Any, **kwargs: Any) -> None: # succeed after we've backed off twice if len(appender.events["backoff"]) < 2: raise ValueError("catch me") @@ -657,7 +665,7 @@ def test_on_predicate_jitter(appender: EventAppender) -> None: interval=0, ) @_save_target - def success(*args, **kwargs): + def success(*args: Any, **kwargs: Any) -> bool: # succeed after we've backed off twice return len(appender.events["backoff"]) == 2 @@ -698,7 +706,7 @@ def test_on_exception_callable_max_tries() -> None: log: list[bool] = [] @backoff.on_exception(backoff.constant, ValueError, max_tries=lambda: 3) - def exceptor(): + def exceptor() -> None: log.append(True) raise ValueError("aah") @@ -711,12 +719,12 @@ def exceptor(): def test_on_exception_callable_max_tries_reads_every_time() -> None: lookups = [] - def lookup_max_tries(): + def lookup_max_tries() -> int: lookups.append(True) return 3 @backoff.on_exception(backoff.constant, ValueError, max_tries=lookup_max_tries) - def exceptor(): + def exceptor() -> None: raise ValueError("aah") with pytest.raises(ValueError, match="aah"): @@ -728,11 +736,14 @@ def exceptor(): assert len(lookups) == 2 -def test_on_exception_callable_gen_kwargs(): - def lookup_foo(): +def test_on_exception_callable_gen_kwargs() -> None: + def lookup_foo() -> Literal["foo"]: return "foo" - def wait_gen(foo=None, bar=None) -> Generator[float, None, None]: + def wait_gen( + foo: str | None = None, + bar: str | None = None, + ) -> Generator[float, None, None]: assert foo == "foo" assert bar == "bar" @@ -740,7 +751,7 @@ def wait_gen(foo=None, bar=None) -> Generator[float, None, None]: yield 0 @backoff.on_exception(wait_gen, ValueError, max_tries=2, foo=lookup_foo, bar="bar") - def exceptor(): + def exceptor() -> None: raise ValueError("aah") with pytest.raises(ValueError, match="aah"): @@ -750,11 +761,11 @@ def exceptor(): def test_on_predicate_in_thread() -> None: result: list[Exception | str] = [] - def check(): + def check() -> None: try: @backoff.on_predicate(backoff.expo) - def return_true(log: list[bool], n): + def return_true(log: list[bool], n: int) -> bool: val = len(log) == n - 1 log.append(val) return val @@ -789,7 +800,7 @@ def test_on_predicate_constant_iterable(appender: EventAppender) -> None: on_try=appender.on_event("try"), jitter=None, ) - def falsey(): + def falsey() -> Literal[False]: return False assert not falsey() @@ -807,11 +818,11 @@ def falsey(): def test_on_exception_in_thread() -> None: result: list[Exception | str] = [] - def check(): + def check() -> None: try: @backoff.on_exception(backoff.expo, KeyError) - def keyerror_then_true(log: list[Exception], n): + def keyerror_then_true(log: list[Exception], n: int) -> Literal[True]: if len(log) == n: return True e = KeyError() @@ -835,13 +846,13 @@ def keyerror_then_true(log: list[Exception], n): assert result[0] == "success" -def test_on_exception_logger_default(caplog: pytest.LogCaptureFixture): +def test_on_exception_logger_default(caplog: pytest.LogCaptureFixture) -> None: logger = logging.getLogger("backoff") handler = logging.StreamHandler(sys.stdout) logger.addHandler(handler) @backoff.on_exception(backoff.expo, KeyError, max_tries=3) - def key_error(): + def key_error() -> None: raise KeyError() with caplog.at_level(logging.INFO), pytest.raises(KeyError): @@ -852,13 +863,13 @@ def key_error(): assert record.name == "backoff" -def test_on_exception_logger_none(caplog: pytest.LogCaptureFixture): +def test_on_exception_logger_none(caplog: pytest.LogCaptureFixture) -> None: logger = logging.getLogger("backoff") handler = logging.StreamHandler(sys.stdout) logger.addHandler(handler) @backoff.on_exception(backoff.expo, KeyError, max_tries=3, logger=None) - def key_error(): + def key_error() -> None: raise KeyError() with caplog.at_level(logging.INFO), pytest.raises(KeyError): @@ -867,13 +878,13 @@ def key_error(): assert not caplog.records -def test_on_exception_logger_user(caplog: pytest.LogCaptureFixture): +def test_on_exception_logger_user(caplog: pytest.LogCaptureFixture) -> None: logger = logging.getLogger("my-logger") handler = logging.StreamHandler(sys.stdout) logger.addHandler(handler) @backoff.on_exception(backoff.expo, KeyError, max_tries=3, logger=logger) - def key_error(): + def key_error() -> None: raise KeyError() with caplog.at_level(logging.INFO), pytest.raises(KeyError): @@ -884,13 +895,13 @@ def key_error(): assert record.name == "my-logger" -def test_on_exception_logger_user_str(caplog: pytest.LogCaptureFixture): +def test_on_exception_logger_user_str(caplog: pytest.LogCaptureFixture) -> None: logger = logging.getLogger("my-logger") handler = logging.StreamHandler(sys.stdout) logger.addHandler(handler) @backoff.on_exception(backoff.expo, KeyError, max_tries=3, logger="my-logger") - def key_error(): + def key_error() -> None: raise KeyError() with caplog.at_level(logging.INFO), pytest.raises(KeyError): @@ -902,10 +913,10 @@ def key_error(): def _on_exception_factory( - backoff_log_level, - giveup_log_level, - max_tries, -): + backoff_log_level: int, + giveup_log_level: int, + max_tries: int, +) -> Callable[[], None]: @backoff.on_exception( backoff.expo, ValueError, @@ -913,10 +924,10 @@ def _on_exception_factory( backoff_log_level=backoff_log_level, giveup_log_level=giveup_log_level, ) - def value_error(): + def value_error() -> None: raise ValueError("aah") - def func(): + def func() -> None: with pytest.raises(ValueError, match="aah"): value_error() @@ -924,17 +935,17 @@ def func(): def _on_predicate_factory( - backoff_log_level, - giveup_log_level, - max_tries, -): + backoff_log_level: int, + giveup_log_level: int, + max_tries: int, +) -> Callable[[], Literal[False]]: @backoff.on_predicate( backoff.expo, max_tries=max_tries, backoff_log_level=backoff_log_level, giveup_log_level=giveup_log_level, ) - def func(): + def func() -> Literal[False]: return False return func @@ -962,11 +973,11 @@ def func(): ], ) def test_event_log_levels( - caplog, - func_factory, - backoff_log_level, - giveup_log_level, -): + caplog: pytest.LogCaptureFixture, + func_factory: Callable[[int, int, int], Callable[[], Any]], + backoff_log_level: int, + giveup_log_level: int, +) -> None: max_tries = 3 func = func_factory(backoff_log_level, giveup_log_level, max_tries) @@ -1017,7 +1028,7 @@ def monotonic() -> float: max_time=max_time, jitter=None, ) - def on_exception(): + def on_exception() -> None: patch_sleep(function_runtime) # ruff: ignore[function-uses-loop-variable] raise RuntimeError @@ -1037,7 +1048,7 @@ def on_exception(): max_time=max_time, jitter=None, ) - def on_predicate(): + def on_predicate() -> None: patch_sleep(function_runtime) # ruff: ignore[function-uses-loop-variable] on_predicate() diff --git a/tests/test_backoff_async.py b/tests/test_backoff_async.py index 134681d..36e0a63 100644 --- a/tests/test_backoff_async.py +++ b/tests/test_backoff_async.py @@ -3,7 +3,7 @@ import asyncio import contextlib import itertools -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Literal import pytest from dirty_equals import IsFloat, IsInstance @@ -18,7 +18,7 @@ asyncio_sleep = asyncio.sleep -async def _await_none(x): +async def _await_none(x: Any) -> None: return None @@ -30,7 +30,7 @@ def _patch_sleep(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.mark.asyncio async def test_on_predicate() -> None: @backoff.on_predicate(backoff.expo) - async def return_true(log, n): + async def return_true(log: list[bool], n: int) -> bool: val = len(log) == n - 1 log.append(val) return val @@ -44,7 +44,7 @@ async def return_true(log, n): @pytest.mark.asyncio async def test_on_predicate_max_tries() -> None: @backoff.on_predicate(backoff.expo, jitter=None, max_tries=3) - async def return_true(log, n): + async def return_true(log: list[bool], n: int) -> bool: val = len(log) == n log.append(val) return val @@ -58,7 +58,7 @@ async def return_true(log, n): @pytest.mark.asyncio async def test_on_predicate_max_tries_callable() -> None: @backoff.on_predicate(backoff.expo, jitter=None, max_tries=lambda: 3) - async def return_true(log, n): + async def return_true(log: list[bool], n: int) -> bool: val = len(log) == n log.append(val) return val @@ -72,7 +72,7 @@ async def return_true(log, n): @pytest.mark.asyncio async def test_on_exception() -> None: @backoff.on_exception(backoff.expo, KeyError) - async def keyerror_then_true(log, n): + async def keyerror_then_true(log: list[Exception], n: int) -> bool: if len(log) == n: return True e = KeyError() @@ -87,7 +87,7 @@ async def keyerror_then_true(log, n): @pytest.mark.asyncio async def test_on_exception_tuple() -> None: @backoff.on_exception(backoff.expo, (KeyError, ValueError)) - async def keyerror_valueerror_then_true(log: list[Exception]): + async def keyerror_valueerror_then_true(log: list[Exception]) -> Literal[True]: e: Exception if len(log) == 2: return True @@ -108,7 +108,11 @@ async def keyerror_valueerror_then_true(log: list[Exception]): @pytest.mark.asyncio async def test_on_exception_max_tries() -> None: @backoff.on_exception(backoff.expo, KeyError, jitter=None, max_tries=3) - async def keyerror_then_true(log, n, foo=None): + async def keyerror_then_true( + log: list[Exception], + n: int, + foo: str | None = None, + ) -> bool: if len(log) == n: return True e = KeyError() @@ -125,7 +129,11 @@ async def keyerror_then_true(log, n, foo=None): @pytest.mark.asyncio async def test_on_exception_max_tries_callable() -> None: @backoff.on_exception(backoff.expo, KeyError, jitter=None, max_tries=lambda: 3) - async def keyerror_then_true(log, n, foo=None): + async def keyerror_then_true( + log: list[Exception], + n: int, + foo: str | None = None, + ) -> bool: if len(log) == n: return True e = KeyError() @@ -150,7 +158,7 @@ async def test_on_exception_constant_iterable(appender: EventAppender) -> None: on_success=appender.on_event("success"), on_try=appender.on_event("try"), ) - async def endless_exceptions(): + async def endless_exceptions() -> None: raise KeyError("foo") with pytest.raises(KeyError): @@ -177,7 +185,7 @@ async def test_on_exception_success_random_jitter(appender: EventAppender) -> No factor=0.5, ) @_save_target - async def succeeder(*args, **kwargs): + async def succeeder(*args: Any, **kwargs: Any) -> None: # succeed after we've backed off twice if len(appender.events["backoff"]) < 2: raise ValueError("catch me") @@ -210,7 +218,7 @@ async def test_on_exception_success_full_jitter(appender: EventAppender) -> None factor=0.5, ) @_save_target - async def succeeder(*args, **kwargs): + async def succeeder(*args: Any, **kwargs: Any) -> None: # succeed after we've backed off twice if len(appender.events["backoff"]) < 2: raise ValueError("catch me") @@ -243,7 +251,7 @@ async def test_on_exception_success(appender: EventAppender) -> None: interval=0, ) @_save_target - async def succeeder(*args, **kwargs): + async def succeeder(*args: Any, **kwargs: Any) -> None: # succeed after we've backed off twice if len(appender.events["backoff"]) < 2: raise ValueError("catch me") @@ -296,7 +304,7 @@ async def test_on_exception_on_try_runs_before_attempt() -> None: interval=0, max_tries=3, ) - async def fails(): + async def fails() -> None: calls.append("call") raise ValueError("nope") @@ -332,7 +340,7 @@ async def test_on_exception_giveup( interval=0, ) @_save_target - async def exceptor(*args, **kwargs): + async def exceptor(*args: Any, **kwargs: Any) -> None: raise ValueError("catch me") if raise_on_giveup: @@ -362,13 +370,13 @@ async def exceptor(*args, **kwargs): @pytest.mark.asyncio async def test_on_exception_giveup_predicate() -> None: - def on_baz(e): + def on_baz(e: Exception) -> bool: return str(e) == "baz" vals = ["baz", "bar", "foo"] @backoff.on_exception(backoff.constant, ValueError, giveup=on_baz) - async def foo_bar_baz(): + async def foo_bar_baz() -> None: raise ValueError(vals.pop()) with pytest.raises(ValueError, match=r"(baz|bar|foo)"): @@ -385,7 +393,7 @@ async def on_baz(e: Exception) -> bool: vals = ["baz", "bar", "foo"] @backoff.on_exception(backoff.constant, ValueError, giveup=on_baz) - async def foo_bar_baz(): + async def foo_bar_baz() -> None: raise ValueError(vals.pop()) with pytest.raises(ValueError, match=r"(baz|bar|foo)"): @@ -406,7 +414,7 @@ async def test_on_predicate_success(appender: EventAppender) -> None: interval=0, ) @_save_target - async def success(*args, **kwargs): + async def success(*args: Any, **kwargs: Any) -> bool: # succeed after we've backed off twice return len(appender.events["backoff"]) == 2 @@ -458,7 +466,7 @@ async def test_on_predicate_on_try_runs_before_attempt() -> None: interval=0, max_tries=3, ) - async def falsey(): + async def falsey() -> Literal[False]: calls.append("call") return False @@ -487,7 +495,7 @@ async def test_on_predicate_giveup(appender: EventAppender) -> None: interval=0, ) @_save_target - async def emptiness(*args, **kwargs): + async def emptiness(*args: Any, **kwargs: Any) -> None: pass await emptiness(1, 2, 3, foo=1, bar=2) @@ -526,7 +534,7 @@ async def test_on_predicate_iterable_handlers() -> None: interval=0, ) @_save_target - async def emptiness(*args, **kwargs): + async def emptiness(*args: Any, **kwargs: Any) -> None: pass await emptiness(1, 2, 3, foo=1, bar=2) @@ -563,7 +571,7 @@ async def test_on_predicate_constant_iterable(appender: EventAppender) -> None: on_try=appender.on_event("try"), jitter=None, ) - async def falsey(): + async def falsey() -> Literal[False]: return False assert not await falsey() @@ -591,7 +599,7 @@ async def test_on_exception_jitter(appender: EventAppender) -> None: interval=0, ) @_save_target - async def succeeder(*args, **kwargs): + async def succeeder(*args: Any, **kwargs: Any) -> None: # succeed after we've backed off twice if len(appender.events["backoff"]) < 2: raise ValueError("catch me") @@ -640,7 +648,7 @@ async def test_on_predicate_jitter(appender: EventAppender) -> None: interval=0, ) @_save_target - async def success(*args, **kwargs): + async def success(*args: Any, **kwargs: Any) -> bool: # succeed after we've backed off twice return len(appender.events["backoff"]) == 2 @@ -679,13 +687,13 @@ async def success(*args, **kwargs): @pytest.mark.asyncio async def test_on_exception_callable_max_tries() -> None: - def lookup_max_tries(): + def lookup_max_tries() -> int: return 3 log = [] @backoff.on_exception(backoff.constant, ValueError, max_tries=lookup_max_tries) - async def exceptor(): + async def exceptor() -> None: log.append(True) raise ValueError("aah") @@ -700,12 +708,12 @@ async def test_on_exception_callable_max_tries_reads_every_time() -> None: lookups = [] - def lookup_max_tries(): + def lookup_max_tries() -> int: lookups.append(True) return 3 @backoff.on_exception(backoff.constant, ValueError, max_tries=lookup_max_tries) - async def exceptor(): + async def exceptor() -> None: raise ValueError("aah") with pytest.raises(ValueError, match="aah"): @@ -719,10 +727,13 @@ async def exceptor(): @pytest.mark.asyncio async def test_on_exception_callable_gen_kwargs() -> None: - def lookup_foo(): + def lookup_foo() -> Literal["foo"]: return "foo" - def wait_gen(foo=None, bar=None) -> Generator[float, None, None]: + def wait_gen( + foo: str | None = None, + bar: str | None = None, + ) -> Generator[float, None, None]: assert foo == "foo" assert bar == "bar" @@ -730,7 +741,7 @@ def wait_gen(foo=None, bar=None) -> Generator[float, None, None]: yield 0 @backoff.on_exception(wait_gen, ValueError, max_tries=2, foo=lookup_foo, bar="bar") - async def exceptor(): + async def exceptor() -> None: raise ValueError("aah") with pytest.raises(ValueError, match="aah"): @@ -743,7 +754,7 @@ async def test_on_exception_coro_cancelling(monkeypatch: pytest.MonkeyPatch) -> sleep_started_event = asyncio.Event() @backoff.on_predicate(backoff.expo) - async def coro(): + async def coro() -> bool: sleep_started_event.set() try: @@ -763,14 +774,14 @@ async def coro(): @pytest.mark.asyncio -async def test_max_time(monkeypatch: pytest.MonkeyPatch): +async def test_max_time(monkeypatch: pytest.MonkeyPatch) -> None: elapsed: float = 0 - async def patch_sleep(n: float): + async def patch_sleep(n: float) -> None: nonlocal elapsed elapsed += n - def monotonic(): + def monotonic() -> float: return elapsed monkeypatch.setattr("asyncio.sleep", patch_sleep) @@ -786,7 +797,7 @@ def monotonic(): max_time=max_time, jitter=None, ) - async def on_exception(): + async def on_exception() -> None: await patch_sleep(function_runtime) # ruff: ignore[function-uses-loop-variable] raise RuntimeError @@ -806,7 +817,7 @@ async def on_exception(): max_time=max_time, jitter=None, ) - async def on_predicate(): + async def on_predicate() -> None: await patch_sleep(function_runtime) # ruff: ignore[function-uses-loop-variable] await on_predicate() diff --git a/tests/test_integration.py b/tests/test_integration.py index fd1c828..9d729c6 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -16,7 +16,7 @@ def test_on_predicate_runtime(monkeypatch: pytest.MonkeyPatch) -> None: log = [] - def sleep(seconds): + def sleep(seconds: float) -> None: log.append(seconds) monkeypatch.setattr("time.sleep", sleep) @@ -34,7 +34,7 @@ def sleep(seconds): value=lambda r: int(r.headers.get("Retry-After")), jitter=None, ) - def get_url(): + def get_url() -> requests.Response: return requests.get(url) resp = get_url() @@ -47,7 +47,7 @@ def get_url(): def test_on_exception_runtime(monkeypatch: pytest.MonkeyPatch) -> None: log = [] - def sleep(seconds): + def sleep(seconds: float) -> None: log.append(seconds) monkeypatch.setattr("time.sleep", sleep) @@ -65,7 +65,7 @@ def sleep(seconds): value=lambda e: int(e.response.headers.get("Retry-After")), jitter=None, ) - def get_url(): + def get_url() -> requests.Response: resp = requests.get(url) resp.raise_for_status() return resp diff --git a/tests/test_retry_context.py b/tests/test_retry_context.py index 8b472ee..d896c19 100644 --- a/tests/test_retry_context.py +++ b/tests/test_retry_context.py @@ -292,7 +292,7 @@ async def run() -> None: async def test_aretry_context_async_handlers() -> None: backoffs = [] - async def on_backoff(details) -> None: + async def on_backoff(details: ContextDetails) -> None: backoffs.append(details) calls = [] diff --git a/tests/typing/__init__.py b/tests/typing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_typing.py b/tests/typing/basic.py similarity index 91% rename from tests/test_typing.py rename to tests/typing/basic.py index 646a558..5e4fca5 100644 --- a/tests/test_typing.py +++ b/tests/typing/basic.py @@ -10,7 +10,7 @@ jitter=None, max_tries=3, ) -def foo(): +def foo() -> None: raise ValueError() @@ -20,7 +20,7 @@ def foo(): interval=1, max_tries=3, ) -def bar(): +def bar() -> None: raise ValueError() @@ -30,5 +30,5 @@ def bar(): value=lambda r: int(r.headers.get("Retry-After")), jitter=None, ) -def baz(): +def baz() -> None: pass diff --git a/tests/typing_decorators.py b/tests/typing/decorators.py similarity index 100% rename from tests/typing_decorators.py rename to tests/typing/decorators.py diff --git a/tests/typing_pyrefly.py b/tests/typing/pyrefly.py similarity index 62% rename from tests/typing_pyrefly.py rename to tests/typing/pyrefly.py index 7b5ae27..53335c0 100644 --- a/tests/typing_pyrefly.py +++ b/tests/typing/pyrefly.py @@ -1,17 +1,21 @@ +from typing_extensions import assert_type + import backoff @backoff.on_exception(backoff.expo, ValueError) -def fetch_with_exception(value: str): +def fetch_with_exception(value: str) -> str: return value @backoff.on_predicate(backoff.expo) -def fetch_with_predicate(value: str): +def fetch_with_predicate(value: str) -> str: return value def consume_sync_results() -> str: exception_result = fetch_with_exception("exception") + assert_type(exception_result, str) predicate_result = fetch_with_predicate("predicate") + assert_type(predicate_result, str) return exception_result.upper() + predicate_result.upper()