diff --git a/backoff/_async.py b/backoff/_async.py index f677424..b578050 100644 --- a/backoff/_async.py +++ b/backoff/_async.py @@ -84,7 +84,7 @@ async def _call_handlers( def retry_predicate( - target: Callable[P, T], + target: Callable[P, Coroutine[object, object, T]], wait_gen: _WaitGenerator, predicate: _Predicate[T], *, @@ -96,7 +96,7 @@ def retry_predicate( on_backoff: Iterable[_Handler], on_giveup: Iterable[_Handler], wait_gen_kwargs: dict[str, Any], -) -> Callable[P, T]: +) -> Callable[P, Coroutine[object, object, T]]: on_try = _ensure_coroutines(on_try) on_success = _ensure_coroutines(on_success) on_backoff = _ensure_coroutines(on_backoff) @@ -158,7 +158,7 @@ async def retry(*args: P.args, **kwargs: P.kwargs) -> T: return ret - return retry # type: ignore[return-value] # ty:ignore[invalid-return-type] + return retry def _adapt_context_handlers( @@ -181,7 +181,7 @@ async def adapted(details: ContextDetails) -> None: def retry_exception( - target: Callable[P, T], + target: Callable[P, Coroutine[object, object, T]], wait_gen: _WaitGenerator, exception: _MaybeTuple[type[Exception]], *, @@ -195,7 +195,7 @@ def retry_exception( on_giveup: Iterable[_Handler], raise_on_giveup: bool, wait_gen_kwargs: dict[str, Any], -) -> Callable[P, T]: +) -> Callable[P, Coroutine[object, object, T]]: on_try = _ensure_coroutines(on_try) on_success = _ensure_coroutines(on_success) on_backoff = _ensure_coroutines(on_backoff) @@ -228,11 +228,11 @@ async def retry( wait_gen_kwargs=wait_gen_kwargs, ): with attempt: - ret = await target(*args, **kwargs) # type: ignore[misc] # ty:ignore[invalid-await] + ret = await target(*args, **kwargs) return ret - return retry # type: ignore[return-value] # ty:ignore[invalid-return-type] + return retry async def _dispatch_handlers( diff --git a/backoff/_decorator.py b/backoff/_decorator.py index 77889a8..2977215 100644 --- a/backoff/_decorator.py +++ b/backoff/_decorator.py @@ -3,7 +3,7 @@ import inspect import logging import operator -from typing import TYPE_CHECKING, Any, Callable, TypeVar +from typing import TYPE_CHECKING, Any, Callable, cast from backoff import _async, _sync from backoff._common import ( @@ -18,11 +18,11 @@ from backoff._wait_gen import expo if TYPE_CHECKING: - import sys from collections.abc import AsyncGenerator, Generator, Iterable from backoff._common import _Attempt from backoff._typing import ( + _CallableT, _ContextHandler, _Handler, _Jitterer, @@ -33,14 +33,6 @@ _WaitGenerator, ) - if sys.version_info >= (3, 10): - from typing import ParamSpec - else: - from typing_extensions import ParamSpec - - T = TypeVar("T") - P = ParamSpec("P") - def on_predicate( wait_gen: _WaitGenerator, @@ -57,7 +49,7 @@ def on_predicate( backoff_log_level: int = logging.INFO, giveup_log_level: int = logging.ERROR, **wait_gen_kwargs: Any, -) -> Callable[[Callable[P, T]], Callable[P, T]]: +) -> Callable[[_CallableT], _CallableT]: """Returns decorator for backoff and retry triggered by predicate. Args: @@ -105,7 +97,7 @@ def on_predicate( This is useful for runtime configuration. """ - def decorate(target: Callable[P, T]) -> Callable[P, T]: + def decorate(target: _CallableT) -> _CallableT: nonlocal logger, on_try, on_success, on_backoff, on_giveup logger = _prepare_logger(logger) @@ -125,22 +117,38 @@ def decorate(target: Callable[P, T]) -> Callable[P, T]: ) if inspect.iscoroutinefunction(target): - retry = _async.retry_predicate - else: - retry = _sync.retry_predicate - - return retry( - target, - wait_gen, - predicate, - max_tries=max_tries, - max_time=max_time, - jitter=jitter, - on_try=on_try, - on_success=on_success, - on_backoff=on_backoff, - on_giveup=on_giveup, - wait_gen_kwargs=wait_gen_kwargs, + return cast( + "_CallableT", + _async.retry_predicate( + target, + wait_gen, + predicate, + max_tries=max_tries, + max_time=max_time, + jitter=jitter, + on_try=on_try, + on_success=on_success, + on_backoff=on_backoff, + on_giveup=on_giveup, + wait_gen_kwargs=wait_gen_kwargs, + ), + ) + + return cast( + "_CallableT", + _sync.retry_predicate( + target, + wait_gen, + predicate, + max_tries=max_tries, + max_time=max_time, + jitter=jitter, + on_try=on_try, + on_success=on_success, + on_backoff=on_backoff, + on_giveup=on_giveup, + wait_gen_kwargs=wait_gen_kwargs, + ), ) # Return a function which decorates a target with a retry loop. @@ -164,7 +172,7 @@ def on_exception( backoff_log_level: int = logging.INFO, giveup_log_level: int = logging.ERROR, **wait_gen_kwargs: Any, -) -> Callable[[Callable[P, T]], Callable[P, T]]: +) -> Callable[[_CallableT], _CallableT]: """Returns decorator for backoff and retry triggered by exception. Args: @@ -214,7 +222,7 @@ def on_exception( This is useful for runtime configuration. """ - def decorate(target: Callable[P, T]) -> Callable[P, T]: + def decorate(target: _CallableT) -> _CallableT: nonlocal logger, on_try, on_success, on_backoff, on_giveup logger = _prepare_logger(logger) @@ -234,24 +242,42 @@ def decorate(target: Callable[P, T]) -> Callable[P, T]: ) if inspect.iscoroutinefunction(target): - retry = _async.retry_exception - else: - retry = _sync.retry_exception - - return retry( - target, - wait_gen, - exception, - max_tries=max_tries, - max_time=max_time, - jitter=jitter, - giveup=giveup, - on_try=on_try, - on_success=on_success, - on_backoff=on_backoff, - on_giveup=on_giveup, - raise_on_giveup=raise_on_giveup, - wait_gen_kwargs=wait_gen_kwargs, + return cast( + "_CallableT", + _async.retry_exception( + target, + wait_gen, + exception, + max_tries=max_tries, + max_time=max_time, + jitter=jitter, + giveup=giveup, + on_try=on_try, + on_success=on_success, + on_backoff=on_backoff, + on_giveup=on_giveup, + raise_on_giveup=raise_on_giveup, + wait_gen_kwargs=wait_gen_kwargs, + ), + ) + + return cast( + "_CallableT", + _sync.retry_exception( + target, + wait_gen, + exception, + max_tries=max_tries, + max_time=max_time, + jitter=jitter, + giveup=giveup, + on_try=on_try, + on_success=on_success, + on_backoff=on_backoff, + on_giveup=on_giveup, + raise_on_giveup=raise_on_giveup, + wait_gen_kwargs=wait_gen_kwargs, + ), ) # Return a function which decorates a target with a retry loop. diff --git a/pyproject.toml b/pyproject.toml index 5378468..e286435 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,13 @@ commands = [ "tests", ], extend = true }, ], + [ + "mypy", + "--strict", + "--disallow-any-decorated", + "--follow-imports=silent", + "tests/typing_decorators.py", + ], [ "ty", "check", diff --git a/tests/typing_decorators.py b/tests/typing_decorators.py new file mode 100644 index 0000000..5104965 --- /dev/null +++ b/tests/typing_decorators.py @@ -0,0 +1,54 @@ +import asyncio +import sys + +if sys.version_info >= (3, 11): + from typing import assert_type +else: + from typing_extensions import assert_type + +import backoff + + +@backoff.on_exception(backoff.expo, ValueError) +def fetch_sync_with_exception(value: str, *, suffix: str) -> str: + return value + suffix + + +@backoff.on_predicate(backoff.expo) +def fetch_sync_with_predicate(value: str, *, suffix: str) -> str: + return value + suffix + + +@backoff.on_exception(backoff.expo, ValueError) +async def fetch_async_with_exception(value: str, *, suffix: str) -> str: + return value + suffix + + +@backoff.on_predicate(backoff.expo) +async def fetch_async_with_predicate(value: str, *, suffix: str) -> str: + return value + suffix + + +def consume_sync_decorated_functions() -> None: + exception_result = fetch_sync_with_exception("exception", suffix=" result") + predicate_result = fetch_sync_with_predicate("predicate", suffix=" result") + assert_type(exception_result, str) + assert_type(predicate_result, str) + + +async def consume_async_decorated_functions() -> None: + exception_result = await fetch_async_with_exception("exception", suffix=" result") + predicate_result = await fetch_async_with_predicate("predicate", suffix=" result") + assert_type(exception_result, str) + assert_type(predicate_result, str) + + exception_task = asyncio.create_task( + fetch_async_with_exception("exception", suffix=" task") + ) + predicate_task = asyncio.create_task( + fetch_async_with_predicate("predicate", suffix=" task") + ) + assert_type(exception_task, asyncio.Task[str]) + assert_type(predicate_task, asyncio.Task[str]) + assert_type(await exception_task, str) + assert_type(await predicate_task, str) diff --git a/tests/typing_pyrefly.py b/tests/typing_pyrefly.py new file mode 100644 index 0000000..7b5ae27 --- /dev/null +++ b/tests/typing_pyrefly.py @@ -0,0 +1,17 @@ +import backoff + + +@backoff.on_exception(backoff.expo, ValueError) +def fetch_with_exception(value: str): + return value + + +@backoff.on_predicate(backoff.expo) +def fetch_with_predicate(value: str): + return value + + +def consume_sync_results() -> str: + exception_result = fetch_with_exception("exception") + predicate_result = fetch_with_predicate("predicate") + return exception_result.upper() + predicate_result.upper()