See https://github.com/python/typing/issues/270 for a long discussion of this problem. You can achieve this by decorating wrap
with an appropriately typed identity function:
F = TypeVar("F", bound=Callable)
def copy_signature(_: F) -> Callable[..., F]:
return lambda f: f
def s(x: int, y: int) -> int:
return x + y
@copy_signature(s)
def wrap(*args, **kwargs):
s(*args, **kwargs)
reveal_type(wrap) # Revealed type is "def (x: int, y: int) -> int"
As far as I know, the decorator is necessary - it is still not possible to do this using type hints alone, even with PEP612. Since it was already good practice to use the functools.wraps
decorator in this situation (which copies the runtime type information), this is not such a loss - you could instead define
def wraps(f: F) -> Callable[..., F]:
return functools.wraps(f) # type: ignore
and then both the runtime and static type information should be correct so long as you use this decorator. (Sadly the typeshed stubs for functools.wraps
included with mypy aren't quite restrictive enough to get this working out of the box.)
PEP612 adds the ability to add/remove arguments in your wrapper (by combining ParamSpec
with Concatenate
), but it doesn't remove the need for some kind of higher-order function (like a decorator) to let the type system infer the signature of wrap
from that of s
.
__signature__
attribute copied from a wrapped function, for example, and theinspect
module will respect it even if the wrapper function is defined with*args, **kwargs
.functools.wraps()
copies the signature object appropriately. I don't know how widely this is supported in IDEs since it only happens at runtime; PyCharm didn't really "get it" when I used@functools.wraps(sum)
on yourwrap
function.