Typing: declaring classmethod-like decorators

Viewed 170

mypy has the binding semantics for @classmethod baked into it (in fact from looking at the source code I believe it is hard-coded for builtins.classmethod) so this works:

from __future__ import annotations
from typing import Any


class Foo:
    def __init__(self, x: Any) -> None:
        self.x = x

    @classmethod
    def create(cls, x: Any) -> Foo:
        return cls(x)


if __name__ == '__main__':
    foo = Foo.create(1)
    print(foo)
$ mypy a0.py
Success: no issues found in 1 source file

But say, for valid reasons, I have a decorator that has classmethod-like binding behavior on the first argument (expecting it to be a class). To give a simple example that approximates classmethod itself:

class myclassmethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, cls):
        return partial(self.func, cls)


class Foo:
    def __init__(self, x: Any) -> None:
        self.x = x

    @myclassmethod
    def create(cls, x: Any) -> Foo:
        return cls(x)

Unsurprisingly this does not check out; mypy assumes the first argument is bound to a Foo instance, and errors out at the call site of cls(x):

$ mypy a1.py
a1.py:20: error: "Foo" not callable

If I try to explicitly give the "cls" a type in contradicts the default bind_self behavior for instance methods:

class Foo:
    def __init__(self, x: Any) -> None:
        self.x = x

    @myclassmethod
    def create(cls: Type[Foo], x: Any) -> Foo:
        return cls(x)
$ mypy a2.py 
a2.py:19: error: The erased type of self "Type[a2.Foo]" is not a supertype of its class "a2.Foo"

The only workaround I've found is to not declare the type of cls in the signature, but instead to cast it at the call site:

class Foo:
    def __init__(self, x: Any) -> None:
        self.x = x

    @myclassmethod
    def create(cls, x: Any) -> Foo:
        return cast(Type[Foo], cls)(x)

This passes as far as type checking goes, but is a little inconvenient and error-prone since this will have to be done by any user of @myclassmethod.

I see that there is a is_classmethod argument to mypy's bind_self, but this is really only applied in the specific case of functions decorated with builtins.classmethod. Subclassing classmethod doesn't help either (I guess mypy is making the conservative assumption that a subclass of classmethod may have arbitrary behavior).

I just wonder if there's a way, without digging deep into mypy internals, to explicitly override the self-binding behavior of functions that are arguments to a decorator.

0 Answers
Related