"""`@retry` and `@timed` — py-04's decorators, typed strictly enough for `mypy --strict`."""

from __future__ import annotations

import functools
import time
from collections.abc import Callable
from typing import ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")


def retry(
    times: int,
    backoff: float = 0.0,
    on: tuple[type[BaseException], ...] = (Exception,),
    *,
    sleep: Callable[[float], None] = time.sleep,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Call the wrapped function up to ``times`` times, re-raising the last error.

    Only exceptions whose type is in ``on`` are retried; anything else leaves on the first
    attempt. Between attempt *i* and *i+1* the wrapper calls ``sleep(backoff * 2 ** i)`` —
    so with ``times=3, backoff=0.1`` the two waits are ``0.1`` then ``0.2``. ``sleep`` is a
    parameter so a test can record the waits instead of taking them.

    The wrapper keeps the wrapped function's ``__name__`` and ``__doc__``.
    """
    ...


def timed(
    sink: Callable[[str, float], None],
    *,
    clock: Callable[[], float] = time.perf_counter,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Time every call and report it exactly once as ``sink(func.__name__, seconds)``.

    The report happens whether the call returned or raised, and an exception still
    propagates. The wrapper keeps ``__name__`` and ``__doc__``.
    """
    ...
