Skip to content

Typing of method decorators not working properly [question] #10805

@lonelyenvoy

Description

@lonelyenvoy

Hi. It seems that the typing of decorators is problematic with functions inside classes.

Suppose we want to see how many times a function has been called, we can write a simple decorator:

def counter(func):
    def wrapper(*args, **kwargs):
        wrapper.count += 1
        return func(*args, **kwargs)
    wrapper.count = 0
    return wrapper

and use it like:

@counter
def greet(name: str) -> str:
    return f'hello, {name}'

print(greet('John'))
print(f'greet() called {greet.count} times')

To type this counter decorator, we can:

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

class CountedFunction(Protocol[T]):
    __call__: T
    count: int

def counter(func: T) -> CountedFunction[T]: ...

This is fine with functions outside classes. However, it is not with a class function method:

class A:
    @counter
    def greet(self, name: str) -> str:
        return f'hello, {name}'

a = A()
print(a.greet('John'))  # error
print(f'greet() called {a.greet.count} times')

We get static errors from mypy, although the code can be run without any runtime errors:

test.py:37: error: Too few arguments for "greet" of "A"
test.py:37: error: Argument 1 to "greet" of "A" has incompatible type "str"; expected "A"
Full code (click to expand)
from typing import Any, Protocol, TypeVar, Callable, cast

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


class CountedFunction(Protocol[T]):
    __call__: T
    count: int


def counter(func: T) -> CountedFunction[T]:
    def wrapper(*args, **kwargs):
        wrapper.count += 1
        return func(*args, **kwargs)
    wrapper.count = 0  # type: ignore
    return cast(CountedFunction[T], wrapper)


# usage with a normal function
@counter
def greet(name: str) -> str:
    return f'hello, {name}'


print(greet('John'))  # OK
print(f'greet() called {greet.count} times')


# usage with a class function
class A:
    @counter
    def greet(self, name: str) -> str:
        return f'hello, {name}'


a = A()
print(a.greet('John'))  # error
print(f'greet() called {a.greet.count} times')

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions