Reputation: 544
Is there any way for me to apply a transformation on a ParamSpec
? I can illustrate the problem with an example:
from typing import Callable
def as_upper(x: str):
return x.upper()
def eventually(f: Callable[P, None], *args: P.args, **kwargs: P.kwargs):
def inner():
def transform(a):
return a() if isinstance(a, Callable) else a
targs = tuple(transform(a) for a in args)
tkwargs = {k: transform(v) for k,v in kwargs.items()}
return f(*targs, **tkwargs)
return inner
eventually(as_upper, lambda: "hello") # type checker complains here
This type checker (pyright in my case) will complain about this. The function eventually
received a callable () -> str
and not a str
which was expected. My question is: is there some way for me to specify that it should expect () -> str
and not the str
itself? And in general, if a function expects a type T
I can transform it to (say) () -> T
?
I'm basically asking if it's possible to transform the ParamSpec
in some way so that a related function does not expect the same parameters, but "almost" the same parameters.
I don't really expect this to be possible, but maybe some with more experience with type checking know a potential solution to this problem. :)
Upvotes: 4
Views: 1969
Reputation: 2380
This decorator cannot be properly typed with currently available tools (Python 3.10).
Two main problems here:
ParamSpec
and Concatenate
for now only allow us to modify a fixed number of parameters.**kwargs: P.kwargs
impossible)However, under these constraints, we can achieve a less elegant solution if the positional parameters are known, taking advantage of currying:
from typing import Callable, Concatenate, ParamSpec, TypeVar
T = TypeVar("T")
RetT = TypeVar("RetT")
P = ParamSpec("P")
def something(a: str, b: int, c: bool):
return f"{a} {b} {c}"
def eventually(
f: Callable[Concatenate[T, P], RetT], a: Callable[[], T]
) -> Callable[P, RetT]:
def inner(*args: P.args, **kwargs: P.kwargs) -> RetT:
transformed = a() if callable(a) else a
return f(transformed, *args, **kwargs)
return inner
something_eventually = eventually(
eventually(eventually(something, lambda: "hello"), lambda: 2), lambda: False
)
something_eventually() # hello 2 False
Notice that it is yet not possible to concatenate keyword parameters (See also).
eventually
can also be applied in a more functional way (but it cannot be properly typed):
from functools import reduce
# Though it unfortunately doesn't type check
something_eventually = reduce(
eventually,
[lambda: "hello", lambda: 2, lambda: False],
something,
)
something_eventually() # hello 2 False
reduce
expects the type of the value to have the same type, while we are changing the type of the function in each iteration. This makes it impossible to type it when we apply eventually
arbitrary times in this manner.
reduce
is just an example. In a more general sense, I doubt if we can properly type something that involves repeatedly applying currying functions like eventually
as proposed above, at least with the current Concatenate
support.
Upvotes: 5