python/mypy

Function returning a generic function

JukkaL opened this issue · 13 comments

Mypy doesn't support a function that tries to return a generic callable. I wonder if mypy could make this code work properly:

from typing import TypeVar, Callable, Any

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

def deco() -> Callable[[T], T]:
    def inner(x):
        return x
    return inner

@deco()
def g(x: str) -> int: ...

g('') # should be fine? currently "Argument 1 to "g" has incompatible type "str"; expected None"
g(1) # should be an error

This is related to my comments at #1317, but maybe worth keeping open as a separate issue.

As Jukka and I discussed at PyCon, this situation where a type variable appears only in the result type of a generic callable could be handled by "floating in" the type variable to the result type: instead of

def [T] () -> def (T) -> T

give deco the type

def () -> def [T] (T) -> T

I wonder how often this arises in practice?

The only case where I remember having seen this is in a decorator that is used like this:

@memoize(cache_size=200)
def do_stuff(): ...

The decorator, when called, returns a generic function T -> T that preserves the signature of the decorated function.

At Zulip, we're using such decorator-returning functions a lot. One of these decorator-returning functions, named cache_with_keys, is applied to dozens of functions, many of which are called hundreds of times (see zulip/zulip#1348). It'll be great to see this issue fixed since a lot of our code is not properly type checked because of this.

I have another example from our codebase:

@app.route('/')
@rate_limit(30, timedelta(seconds=60))
def index() -> Response:
    # ...

Both of the decorators returned by app.route() and rate_limit() at lest formally preserve the signature of the callable being decorated.

Sadly I don't have enough knowledge to help with this myself.

This seems like something we'll really want to fix, but it's not immediately clear yet what the fix looks like.

As a workaround, rewriting nested functions to a class seem to work:

from functools import wraps
from typing import TypeVar, Callable, Any, cast

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

class log_calls:
    def __init__(self, prefix: str) -> None:
        self.prefix = prefix

    def __call__(self, f: TFun) -> TFun:
        prefix = self.prefix
        @wraps(f)
        def wrapper(*args, **kwargs) -> Any:
            print('{}: {!r} {!r}'.format(prefix, args, kwargs))
            return f(*args, **kwargs)
        return cast(TFun, wrapper)

@log_calls('Calling foo')
def foo(x: str) -> str:
    return x

reveal_type(foo)

Oooh, very clever! I can't say I personally understand how it works, and we
should still fix the original error, but this looks like a useful
workaround. Thanks!

On Sat, Oct 15, 2016 at 4:28 AM, Yegor Roganov notifications@github.com
wrote:

As a workaround, rewriting nested functions to a class seem to work:

from functools import wrapsfrom typing import TypeVar, Callable, Any, cast

TFun = TypeVar('TFun', bound=Callable[..., Any])
class log_calls:
def init(self, prefix: str) -> None:
self.prefix = prefix

def __call__(self, f: TFun) -> TFun:
    prefix = self.prefix
    @wraps(f)
    def wrapper(*args, **kwargs) -> Any:
        print('{}: {!r} {!r}'.format(prefix, args, kwargs))
        return f(*args, **kwargs)
    return cast(TFun, wrapper)

@log_calls('Calling foo')def foo(x: str) -> str:
return x

reveal_type(foo)


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
#1551 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/ACwrMntwUxGKXBJXiFRnuVKRAye7x5Rxks5q0LjDgaJpZM4IhSDU
.

--Guido van Rossum (python.org/~guido)

This looks fun and important. It'll be a good warm-up to all the rest of the features we need to get decorators working right. claim

I just opened a pull request #3113 that fixes this. It's still WIP, but I should link this issue in.

@sixolet, Is this fixed by your #3113?

This is the email I meant to reply to saying yes I think it's fixed. Sorry, I was getting off a plane on 4h sleep.

NP! We've had quite a busy week. I'll tell you the stories some time.