Here is my function:
def call_func(func, *args):
return func(*args)
I think I have two options here:
Using TypeVarTuple
-> in Callable[[*Ts], Any]
form.
Ts = TypeVarTuple("Ts")
T = TypeVar("T")
def call_func(func: Callable[[*Ts], T], *args: *Ts) -> T:
return func(*args)
Currently Mypy has problem with [*Ts]
part. it says: Invalid type comment or annotation
. (I also enabled --enable-incomplete-feature=TypeVarTuple
.)
Using ParamSpec
-> in Callable[P, Any]
form.
P = ParamSpec("P")
T = TypeVar("T")
def call_func(func: Callable[P, T], *args: P.args) -> T:
return func(*args)
This time Mypy says: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs"
. It looks like it wants me to also specify kwargs
.
What is the correct way of doing it? Is there any technical difference between using TypeVarTuple
and ParamSec
in Callable
?
The only way to fully type-check something that only accepts positional-only arguments is with PEP 646, which mypy (as of the time of this answer) does not fully implement. However, you can manipulate typing.ParamSpec
and typing.Protocol
to make positional-only and keyword-only callable types which throw errors at a call site if someone tries to pass keyword and positional arguments, respectively.
The core idea is to form a union with Callable[P, R]
with some Protocol
CallbackProto
such that:
Callable[P, R]
unioned with CallbackProto::__call__(self, *args: typing.Any) -> R
rejects all attempts to pass any keyword arguments;Callable[P, R]
unioned with CallbackProto::__call__(self, **kwargs: typing.Any) -> R
rejects all attempts to pass any positional arguments.In your case, let's say that this was the desired output:
P = ParamSpec("P")
R = TypeVar("R")
def call_func(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
return func(*args)
>>> def function_(a: int, b: str) -> None:
... ...
...
>>> call_func(function_, 1, "") # OK
>>> call_func(function_, 1, b="") # Unexpected keyword argument "b"
This can be done by making call_func
reject all attempts to pass keyword arguments:
from __future__ import annotations
import typing as t
if t.TYPE_CHECKING:
from typing_extensions import Never as UnionReturnTypePlaceholder
import collections.abc as cx
P = t.ParamSpec("P")
R = t.TypeVar("R")
CallFuncT = t.TypeVar("CallFuncT", bound="CallFunc")
class PositionalOnlyCallable(t.Protocol):
def __call__(self, *args: t.Any) -> UnionReturnTypePlaceholder:
...
class CallFunc(t.Protocol):
def __call__(self, func: cx.Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
...
def asPositionalOnlyCallable(f: CallFuncT, /) -> CallFuncT | PositionalOnlyCallable:
"""
No-op decorator which manipulates the typing signature. Unions
a given callable's type with something which only accepts
positional arguments.
"""
return f
@asPositionalOnlyCallable
def call_func(func: cx.Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
return func(*args)
>>> def function_(a: int, b: str) -> None:
... ...
...
>>> call_func(function_, 1, "") # OK
>>> call_func(function_, 1, b="") # mypy: Unexpected keyword argument "b" for "__call__" of "PositionalOnlyCallable" [call-arg]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With