1

For the following file:

from abc import ABC, abstractmethod
from typing import Any, Awaitable, Callable, TypeVar, cast

T = TypeVar('T')


def dec(args: Any) -> Callable[..., Awaitable[T]]:
    def dec2(f: Callable[..., Awaitable[T]]) -> Awaitable[T]:
        async def wrapper(*args:Any, **kwargs:Any) -> T:
            print(args)

            return cast(T, await f(*args, **kwargs))
        return cast(Awaitable[T], wrapper)
    return dec2


class A(ABC):
    @abstractmethod
    async def fn(self) -> 'bool':
        pass

class B(A):
    @dec('hi')
    async def fn(self) -> 'bool':
        return True


class C(A):
    @dec('hi')
    async def fn(self) -> 'bool':
        return False

I am receiving the following mypy errors:

$ mypy typetest.py
typetest.py:24: error: Signature of "fn" incompatible with supertype "A"
typetest.py:30: error: Signature of "fn" incompatible with supertype "A"
Found 2 errors in 1 file (checked 1 source file)

How does the typing need to work in order to preserve the class signature and not receive the mypy error.

This is on python3.7 with mypy 0.790

  • Try using `-> bool` instead of `-> 'bool'`. Return types should not need quotations and that may interfere with the signature. – thshea Jan 11 '21 at 14:02
  • 2
    Quotes are valid. The only difference is that no name lookup will occur at runtime. Using `from __future__ import annotations` causes *all* annotations to be implicitly treated as string literals (and this will be the default in Python 3.10, I believe). – chepner Jan 11 '21 at 14:07

1 Answers1

4

TLDR: A function declaration async def name(parameters) -> R: creates an object of type (parameters) -> Awaitable[R], not Awaitable[R]. This means the cast cast(Awaitable[T], wrapper) is wrong and should be omitted, and the various Callable return types must be adjusted as well.


A naive decorator to show the execution of an async def function (roughly dec2) would look like this:

def show_call(f: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
    async def wrapper(*args: Any, **kwargs: Any) -> T:
        print(f"Executing {f.__name__}")
        return await f(*args, **kwargs)
    return wrapper

This receives an callable matching async def and returns a callable matching async def. In other words, it preserves the general type of "an async function".
Note that there is no cast needed.

Since the Callable argument was specified as ..., the parameter information is lost. This can be fixed with PEP 612 (Python 3.10 / typing_extensions) parameter variables, which are similar to type variables.

from typing import Any, Awaitable, Callable, TypeVar, ParamSpec

T = TypeVar('T')    # the callable/awaitable return type
P = ParamSpec('P')  # the callable parameters


def dec(message: Any) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
    def dec2(f: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
        async def wrapper(*args: Any, **kwargs: Any) -> T:
            print(message)
            return await f(*args, **kwargs)
        return wrapper
    return dec2
MisterMiyagi
  • 44,374
  • 10
  • 104
  • 119
  • Mypy error: `The first argument to Callable must be a list of types or "..."`. Perhaps this is because of the https://github.com/python/mypy/issues/8645 and your code is working. – Nairum Dec 10 '21 at 11:06