I'm struggling to get this code to properly type check with mypy
:
from typing import Any
class Decorator:
def __init__(self, func) -> None:
self.func = func
def __call__(self, *args: Any, **kwargs: Any) -> Any:
return self.func(*args, **kwargs)
class Foo:
@property
@Decorator
def foo(self) -> int:
return 42
f = Foo()
# reveal_type(f.foo)
a: int = f.foo
print(a)
This gives me this error, even though running it correctly prints 42
(note I've uncommented the reveal_type
when running mypy
):
$ mypy test.py
test.py:17: note: Revealed type is "mypy_test.test.Decorator"
test.py:18: error: Incompatible types in assignment (expression has type "Decorator", variable has type "int") [assignment]
Found 1 error in 1 file (checked 1 source file)
This code properly type checks with mypy
:
from typing import Any
class Decorator:
def __init__(self, func) -> None:
self.func = func
def __call__(self, *args: Any, **kwargs: Any) -> Any:
return self.func(*args, **kwargs)
class Foo:
@property
@Decorator
def foo(self) -> int:
return 42
f = Foo()
# reveal_type(f.foo)
a: int = f.foo()
print(a)
But this obviously won't run correctly:
$ python3 test.py
Traceback (most recent call last):
File "/home/gsgx/code/tmp/mypy_test/test.py", line 18, in <module>
a: int = f.foo()
TypeError: 'int' object is not callable
Note that if instead of using a class decorator I use a function decorator, everything works fine (runs correctly and no typecheck errors, reveal_type
now shows f.foo
is Any
):
def Decorator(f):
return f
How can I get this to work using a class decorator? I'm using Python 3.10.6 and mypy 0.991.
Decorator.__call__
hides the type hints that you provided for foo
, and replaces them with uselessly general hints.
You need to use typing.ParamSpec
and typing.TypeVar
to capture the hints of the decorated function to reuse for the new one. I'm not entirely sure how to do that for your class, but here's an example using ordinary functions:
from typing import ParamSpec, TypeVar
P = ParamSpec('P')
RV = TypeVar('R')
def Decorator(f: Callable[P, RV]) -> Callable[P, RV]:
def _(*args: P.args, **kwargs: P.kwargs) -> RV:
return f(*args, **kwargs)
return _
typing.ParamSpec
is available starting in Python 3.11, though you can get it from the 3rd-party typing-extensions
package as well.
The problem with the class is that you seem to need to make Decorator
generic in order to associate the return value of the argument to __init__
with the return value of __call__
:
from typing import ParamSpec, TypeVar, Generic
P = ParamSpec('P')
RV = TypeVar('R')
class Decorator(Generic[RV]):
def __init__(self, func: Callable[P, RV]) -> None:
self.func = func
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> RV:
return self.func(*args, **kwargs)
This "works", but requires you to use typing.cast
with
a: int = cast(int, f.foo)
to avoid
tmp.py:27: error: Incompatible types in assignment (expression has type "Decorator[int]", variable has type "int") [assignment]
Perhaps there's a simpler solution for the class itself that I'm overlooking. It seems to me, though, that mypy
simply isn't recognizing that f.foo
and Foo.foo
should have different types.
On the other hand, a seemingly trivial change
class Foo:
@Decorator
def foo2(self) -> int:
return 42
foo = property(foo2)
changes the revealed type of f.foo
to Any
, so this again may be a problem with how mypy
handles property
. property
itself may not be correctly typed to handle the more precise typing in Decorator
, and only when property
is used "statically" as a decorator itself do the hints on foo
carry through.
You can implement a very easy hack by doing this:
import typing as t
import collections.abc as cx
if t.TYPE_CHECKING:
F = t.TypeVar("F")
Decorator: cx.Callable[[F], F]
else:
class Decorator:
"""Your runtime implementation here"""
If you want to do this properly, it's a bit more verbose.
property
is generally special-cased across all the type checker implementations. For mypy
, you need to write anything that interacts with property
as if @property
was not even there. This means re-implementing property
's descriptor protocol , if only for static type-checking purposes:
import collections.abc as cx
import typing as t
Self = t.TypeVar("Self")
P = t.ParamSpec("P")
R = t.TypeVar("R", covariant=True)
class Decorator(t.Generic[P, R]):
func: cx.Callable[P, R]
def __init__(self, func: cx.Callable[P, R]) -> None:
self.func = func
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
return self.func(*args, **kwargs)
if t.TYPE_CHECKING:
# Type-check-only descriptor protocol access. No runtime implementation.
@t.overload # type: ignore[no-overload-impl]
def __get__(self, instance: None, type_: type) -> property:
"""Descriptor access from the class (instance is `None`)"""
@t.overload
def __get__(self, instance: t.Any, type_: type) -> R:
"""Descriptor access from the instance"""
class Foo:
@property
@Decorator
def foo(self) -> int:
return 42
f = Foo()
# reveal_type(f.foo) # mypy: Revealed type is "builtins.int"
# reveal_type(Foo.foo) # mypy: Revealed type is "builtins.property"
a: int = f.foo
print(a)
However, if I were you, I would write class Decorator
to be completely independent of needing property
. This means providing a runtime implementation to the descriptor protocol's __get__
:
from __future__ import annotations
import collections.abc as cx
import typing as t
ObjT = t.TypeVar("ObjT")
P = t.ParamSpec("P")
R = t.TypeVar("R", covariant=True)
class Decorator(t.Generic[P, R]):
func: cx.Callable[P, R]
def __init__(self, func: cx.Callable[P, R]) -> None:
self.func = func
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
return self.func(*args, **kwargs)
@t.overload
def __get__(self, instance: None, type_: type[ObjT]) -> Decorator[[ObjT], R]:
...
@t.overload
def __get__(self, instance: ObjT, type_: type[ObjT]) -> R:
...
def __get__(
self: Decorator[[ObjT], R], instance: ObjT | None, type_: type[ObjT]
) -> Decorator[[ObjT], R] | R:
if instance is None:
return self
return self.func(instance)
class Foo:
@Decorator
def foo(self) -> int:
return 42
f = Foo()
# reveal_type(f.foo) # mypy: Revealed type is "builtins.int"
# reveal_type(Foo.foo) # mypy: Revealed type is "Decorator[[call.Foo], builtins.int]"
a: int = f.foo
print(a)
Note that any descriptor-like access through the instance can only pass the instance object to Decorator.func
; you can't have arbitrary arguments passed through Foo().foo
, as Python's syntax doesn't allow you to do that.
The Generic[P, R]
and __call__
on Decorator[...]
still allows you to use @Decorator
over free functions ( not methods in class bodies ) which don't need property
-like implementations, whatever your use case for that may be.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.