I have a bunch of functions that all have a certain keyword argument, let's call it kw
, which is mandatory and has a specific type T
. However, I want to be able to call the functions without necessarily providing that keyword argument, and if it's not present I want it to be generated, probably through a decorator. Furthermore, I want my IDE to show its signature as if kw
were optional.
In other words, I have functions that have a signature kind of like this:
def f(*args, kw: T, **kwargs): ...
I want their tooltip signature as if it were defined like this:
def f(*args, kw: T | None=None, **kwargs): ...
Note, that each function might have other positional and/or keyword arguments.
It seems to me like I should be able to have some decorator sort of like this:
def decorator(func):
sig = inspect.signature(func)
hints = get_type_hints(func)
orig_ann = hints.get("kw")
new_ann = Optional[orig_ann]
new_params = []
for param in sig.parameters.values():
if param.name == "kw":
new_params.append(
param.replace(
annotation=new_ann,
default=None,
)
)
else:
new_params.append(param)
new_sig = sig.replace(parameters=new_params)
@wraps(func)
def wrapper(*args, **kwargs):
if kwargs.get("kw") is None:
kwargs["kw"] = some_function_or_something()
return func(*args, **kwargs)
"do something to update the signature"
return wrapper
But I cannot figure out what I would need to do to update the static signature so that the tooltip changes. Is it possible? How would I do it if so?
With Pylance, which appears the one your are using in your screenshot this is currently not possible in the way you imagine.
To modify a unspecific signature you need a ParamSpec
, i.e. to catch and forward a signature you need to use (*args:P.args, **kwargs:P.kwargs)
the problem is you are not allowed to put new types in between, i.e.:
def outer[**P](*args:P.args, **kwargs:P.kwargs):
def inner_invalid(*args:P.args, kw: int, **kwargs:P.kwargs): ... # not allowed
def inner_valid(p: int, *args:P.args, **kwargs:P.kwargs): ... # OK
For kwargs typing you can to some extend use TypedDicts however not as flexible as a ParamSpec
.
TL;DR:
The closest option that I see you can currently get is to use overload
to display two signatures. The decorator used two Protocol
s to transform the input signature to the output signature:
from typing import overload, Protocol, cast
class KwProtocol[T, R, **P](Protocol):
@overload
@staticmethod
def __call__(*args, kw: T, **kwargs) -> R: ...
@overload
@staticmethod
def __call__(*args:P.args, **kwargs:P.kwargs) -> R: ...
# Anything between P.args and P.kwargs is not allowed
#def __call__(*args:P.args, kw: T, **kwargs:P.kwargs) -> R: ...
class KwDefaultProtocol[T, R, **P](Protocol):
@overload
@staticmethod
def __call__(*args:P.args, **kwargs:P.kwargs) -> R: ...
@overload
@staticmethod
def __call__(*args, kw: T | None = None, **kwargs) -> R:
"""kw is optional and None by default"""
# This definition is not allowed:
#def __call__(*args:P.args, kw: T | None = None, **kwargs:P.kwargs) -> R: ...
def kw_optional[T, R, **P](func: KwProtocol[T, R, P]) -> KwDefaultProtocol[T, R, P]:
"""A decorator that can be used to wrap a function."""
# Do runtime stuff
return cast(KwDefaultProtocol[T, R, P], func)
@kw_optional
def foo(a: int, *, kw: str, other: int) -> str:
"""Foo doc"""
...
foo