Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions backoff/_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ async def _call_handlers(


def retry_predicate(
target: Callable[P, T],
target: Callable[P, Coroutine[Any, Any, T]],
wait_gen: _WaitGenerator,
predicate: _Predicate[T],
*,
Expand All @@ -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[Any, Any, T]]:
on_try = _ensure_coroutines(on_try)
on_success = _ensure_coroutines(on_success)
on_backoff = _ensure_coroutines(on_backoff)
Expand Down Expand Up @@ -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(
Expand All @@ -181,7 +181,7 @@ async def adapted(details: ContextDetails) -> None:


def retry_exception(
target: Callable[P, T],
target: Callable[P, Coroutine[Any, Any, T]],
wait_gen: _WaitGenerator,
exception: _MaybeTuple[type[Exception]],
*,
Expand All @@ -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[Any, Any, T]]:
on_try = _ensure_coroutines(on_try)
on_success = _ensure_coroutines(on_success)
on_backoff = _ensure_coroutines(on_backoff)
Expand Down Expand Up @@ -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(
Expand Down
51 changes: 37 additions & 14 deletions backoff/_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

T = TypeVar("T")
P = ParamSpec("P")
CallableT = TypeVar("CallableT", bound=Callable[..., Any])


def on_predicate(
Expand All @@ -57,7 +58,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:
Expand Down Expand Up @@ -125,11 +126,21 @@ def decorate(target: Callable[P, T]) -> Callable[P, T]:
)

if inspect.iscoroutinefunction(target):
retry = _async.retry_predicate
else:
retry = _sync.retry_predicate

return retry(
return _async.retry_predicate( # type: ignore[return-value] # ty:ignore[invalid-return-type]
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 _sync.retry_predicate(
target,
wait_gen,
predicate,
Expand All @@ -144,7 +155,7 @@ def decorate(target: Callable[P, T]) -> Callable[P, T]:
)

# Return a function which decorates a target with a retry loop.
return decorate
return decorate # type: ignore[return-value]


def on_exception(
Expand All @@ -164,7 +175,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:
Expand Down Expand Up @@ -234,11 +245,23 @@ def decorate(target: Callable[P, T]) -> Callable[P, T]:
)

if inspect.iscoroutinefunction(target):
retry = _async.retry_exception
else:
retry = _sync.retry_exception

return retry(
return _async.retry_exception( # type: ignore[return-value] # ty:ignore[invalid-return-type]
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 _sync.retry_exception(
target,
wait_gen,
exception,
Expand All @@ -255,7 +278,7 @@ def decorate(target: Callable[P, T]) -> Callable[P, T]:
)

# Return a function which decorates a target with a retry loop.
return decorate
return decorate # type: ignore[return-value]


def retry_context(
Expand Down
1 change: 0 additions & 1 deletion backoff/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ class ContextDetails(_BaseContextDetails, _ContextCallDetails, total=False):

T = TypeVar("T")

_CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) # ruff:ignore[unused-private-type-var]
_Handler = Union[
Callable[[Details], None],
Callable[[Details], Coroutine[Any, Any, None]],
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true

[[tool.mypy.overrides]]
module = "tests.test_typing"
disallow_any_decorated = true

[tool.ruff.lint.per-file-ignores]
"**/doccmd_*.py" = [
"redefined-while-unused",
Expand Down
34 changes: 31 additions & 3 deletions tests/test_typing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import asyncio
import sys
from collections.abc import Callable, Coroutine
from typing import Any

if sys.version_info >= (3, 11):
from typing import assert_type
else:
from typing_extensions import assert_type

import backoff

# No pyunit tests are defined here yet, but the following decorator calls will
Expand All @@ -10,7 +20,7 @@
jitter=None,
max_tries=3,
)
def foo():
def foo() -> None:
raise ValueError()


Expand All @@ -20,7 +30,7 @@ def foo():
interval=1,
max_tries=3,
)
def bar():
def bar() -> None:
raise ValueError()


Expand All @@ -30,5 +40,23 @@ def bar():
value=lambda r: int(r.headers.get("Retry-After")),
jitter=None,
)
def baz():
def baz() -> None:
pass


# Regression test for https://github.com/python-backoff/backoff/issues/200:
# decorating an annotated async function must preserve its parameter and
# awaited result types, without reconstructing it as `Callable[P,
# Coroutine[Any, Any, T]]` (which mypy's `disallow_any_decorated` flags as
# leaking `Any`, since an async function is already typed that way).
@backoff.on_exception(backoff.expo, ValueError)
async def fetch(x: int) -> str:
return str(x)


_typed_fetch: Callable[[int], Coroutine[Any, Any, str]] = fetch


async def _use_fetch() -> None:
result = await asyncio.create_task(fetch(1))
assert_type(result, str)
Loading