Using Concatenate and ParamSpec with a keyword argument

Hi all,

Do I understand PEP 612 right in that yt allows to annotate a decorator that “removes” the first parameter of the decorate function, but it’s not possible (yet?) to fully annotate a decorator that would act on a KW only parameter?

An example of what I am trying to achieve:

def call_authenticated(
    func: Callable[Concatenate[AuthenticatedClient, P], Awaitable[R]],
    client: AuthenticatedClient,
) -> Callable[P, Awaitable[R]]:
    async def wrapped(*a, **k):
        return await func(*a, **k, client=client)
    return wrapped

The signature of the functions I would like to annotate looks like:

async def(args1: str, arg2: int, *, client: AuthenticatedClient) -> Something:
    ...

But this triggers: Argument "client" has incompatible type "AuthenticatedClient"; expected "P.kwargs" from mypy.

What is the proper way to annotate my decorator here?

Here’s a minimal (non-)working example:

from typing import Callable, TypeVar, ParamSpec, Awaitable, Concatenate

P = ParamSpec("P")
T = TypeVar("T")


async def to_be_wrapped(x: int, y: str, *, z: dict) -> str:
    return str(x) + str(y) + str(z)


def force_z(
    func: Callable[Concatenate[dict, P], Awaitable[T]]
) -> Callable[P, Awaitable[T]]:
    async def wrapped(*args: P.args, **kwargs: P.kwargs):
        # error: Argument 1 has incompatible type "*P.args"; expected "dict[Any, Any]"  [arg-type]
        # error: Argument "z" has incompatible type "dict[str, object]"; expected "P.kwargs"  [arg-type]
        return func(*args, **kwargs, z={"foo": "bar"})

    return wrapped


async def main():
    # error: Argument 1 to "force_z" has incompatible type
    # "Callable[[int, str, NamedArg(dict[Any, Any], 'z')], Coroutine[Any, Any, str]]";
    # expected "Callable[[dict[Any, Any], str, NamedArg(dict[Any, Any], 'z')], Awaitable[str]]"  [arg-type]
    await force_z(to_be_wrapped)(1, "")

I posted this question on SO too.

There is no way to type this using ParamSpec/Concatenate. PEP 612 explicitly omitted this edge case because it added a lot of implementation complexity.

The reason you got an error for your call_authenticated example is because someone could apply that decorator to a function without a client keyword (maybe it has a parameter with type as AuthenticatedClient but with a different name). For example:

@call_authenticated
def foo(not_client: AuthenticatedClient) -> None: ...