Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typing Decorator with Parameters in MyPy with TypeVar yields expected uninhabited type

MyPy has some issues with Callable *args and **kwargs, particularly concerning decorators as detailed in: https://github.com/python/mypy/issues/1927

Specifically, for a decorator with no parameters which only wraps a function (and does not change its signature), you need the following:

from typing import Any, Callable, cast, TypeVar

FuncT = TypeVar('FuncT', bound=Callable[..., Any])

def print_on_call(func: FuncT) -> FuncT:
    def wrapped(*args, **kwargs):
        print("Running", func.__name__)
        return func(*args, **kwargs)
    return cast(FuncT, wrapped)

The cast() at the end should be unnecessary (MyPy should be able to derive that by the invocation of func at the end of wrapped that wrapped is indeed FuncT -> FuncT). I can live with this until it is fixed.

However this breaks horribly when you introduce decorators with parameters. Consider the decorator:

def print_on_call(foo):
    def decorator(func):
        def wrapped(*args, **kwargs):
            print("Running", foo)
            return func(*args, **kwargs)
        return wrapped
    return decorator

Which is used as so:

@print_on_call('bar')
def stuff(a, b):
    return a + b

We may attempt to type it (using the parameterless example endorsed by Guido as a guide) like so:

from typing import Any, Callable, Dict, List, TypeVar

FuncT = TypeVar('FuncT', bound=Callable[..., Any])

def print_on_call(foo: str) -> Callable[[FuncT], FuncT]:
    def decorator(func: FuncT) -> FuncT:
        def wrapped(*args: List[Any], **kwargs: Dict[str, Any]) -> Any:
            print("Running", foo)
            return func(*args, **kwargs)
        return cast(FuncT, wrapped)
    return cast(Callable[[FuncT], FuncT], decorator)

This appears to typecheck, but when we use it:

@print_on_call('bar')
def stuff(a: int, b: int) -> int:
    return a + b

We get a nasty error:

error: Argument 1 has incompatible type Callable[[int, int], int]; expected <uninhabited>

I'm a little confused as to how this is possible. As discussed in PEP 484, it seems that Callable[[int, int], int] should be a subtype of Callable[..., Any].

I assumed that maybe this was a bad iteration between using the generic across the return type of print_on_call and a a parameter and return type to decorator, so I shaved my example down to the bare minimum (although no longer a working decorator, it still should typecheck):

from typing import Any, Callable, Dict, List, TypeVar

FuncT = TypeVar('FuncT', bound=Callable[..., Any])

def print_on_call(foo: str) -> Callable[[FuncT], FuncT]:
    return cast(Callable[[FuncT], FuncT], None)

However this still results in the above error. This would be something I'd be okay with just #type: ignoreing away, but unfortunately as a result of this problem, any function decorated with this decorator has type <uninhabited>, so you start losing type safety everywhere.

That all said (tl;dr):

How do you type decorators (which do not modify the function's signature) with parameters? Is the above a bug? Can it be worked around?

MyPy version: 0.501 (the latest as of this posting)

like image 500
Bailey Parker Avatar asked Mar 25 '17 05:03

Bailey Parker


2 Answers

nowadays, this is supported by mypy directly:

https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators

i.e.

FuncT = TypeVar("FuncT", bound=Callable[..., Any]) 

def my_decorator(func: FuncT) -> FuncT:
    @wraps(func)
    def wrapped(*args: Any, **kwargs: Any) -> Any:
        print("something")
        return func(*args, **kwargs)
    return cast(FuncT, wrapped)
like image 162
Sebastian Wagner Avatar answered Nov 09 '22 11:11

Sebastian Wagner


Oops! Looks like I didn't search hard enough. There is already an issue and a workaround for this: https://github.com/python/mypy/issues/1551#issuecomment-253978622

like image 44
Bailey Parker Avatar answered Nov 09 '22 11:11

Bailey Parker