I have a set of functions which all accept a value named parameter, plus arbitrary other named parameters.
I have a decorator: lazy. Normally the decorated functions return as normal, but return a partial function if value is None.
How do I type-hint the decorator, whose output depends on the value input?
from functools import partial
def lazy(func):
def wrapper(value=None, **kwargs):
if value is not None:
return func(value=value, **kwargs)
else:
return partial(func, **kwargs)
return wrapper
@lazy
def test_multiply(*, value: float, multiplier: float) -> float:
return value * multiplier
@lazy
def test_format(*, value: float, fmt: str) -> str:
return fmt % value
print('test_multiply 5*2:', test_multiply(value=5, multiplier=2))
print('test_format 7.777 as .2f:', test_format(value=7.777, fmt='%.2f'))
func_mult_11 = test_multiply(multiplier=11) # returns a partial function
print('Type of func_mult_11:', type(func_mult_11))
print('func_mult_11 5*11:', func_mult_11(value=5))
I'm using mypy and I've managed to get most of the way using mypy extensions, but haven't got the value typing working in wrapper:
from typing import Callable, TypeVar, ParamSpec, Any, Optional
from mypy_extensions import DefaultNamedArg, KwArg
R = TypeVar("R")
P = ParamSpec("P")
def lazy(func: Callable[P, R]) -> Callable[[DefaultNamedArg(float, 'value'), KwArg(Any)], Any]:
def wrapper(value = None, **kwargs: P.kwargs) -> R | partial[R]:
if value is not None:
return func(value=value, **kwargs)
else:
return partial(func, **kwargs)
return wrapper
How can I type value? And better still, can I do this without mypy extensions?
I see two possible options here. First is "more formally correct", but way too permissive, approach relying on
partialhint:Last two calls show hat is good and bad about this approach.
partialaccepts any input arguments, so is not sufficiently safe. If you want to override the arguments provided to lazy callable initially, this is probably the best solution.Note that I slightly changed signatures of the input callables: without that you will not be able to use
Concatenate. Note also thatKwArg,DefaultNamedArgand company are all deprecated in favour of protocols. You cannot use paramspec with kwargs only, args must also be present. If you trust your type checker, it is fine to use kwarg-only callables, all unnamed calls will be rejected at the type checking phase.However, I have another alternative to share if you do not want to override default args passed to the initial callable, which is fully safe, but emits false positives if you try to.
You can also mark
valuekw-only inValueOnlyCallabledefinition if you'd like, I just don't think it is reasonable for a function with only one argument.You can compare both approaches in playground.
If you do not want to use an ignore comment, the verbose option below should work. However, I do not think that verbosity is worth removing one ignore comment - it's up to you to decide.
Here's also Pyright playground, because
mypyfailed to find a mistake in my original answer and Pyright did.