When passing around functions, I normally type hint them with typing.Callable.
The docs for collections.abc.Callable state that it has four dunder methods:
class collections.abc.Callable
ABCs for classes that provide respectively the methods
__contains__(),__hash__(),__len__(), and__call__().
At one point, I want to check if there is a __wrapped__ attribute on a function. This works fine at runtime via a check with hasattr(func, "__wrapped__").
When static type checking with mypy, it reports: error: "Callable[..., Any]" has no attribute "__wrapped__" [attr-defined]. This makes sense to me, as Callable isn’t supposed to have a __wrapped__ attribute.
How can I properly type hint a Callable with a __wrapped__ attribute? Is there some other type hint or workaround I can do?
Code Sample
I am using mypy==0.782 and Python==3.8.2:
from functools import wraps
from typing import Callable
def print_int_arg(arg: int) -> None:
"""Print the integer argument."""
print(arg)
@wraps(print_int_arg)
def wrap_print_int_arg(arg: int) -> None:
print_int_arg(arg)
# do other stuff
def print_is_wrapped(func: Callable) -> None:
"""Print if a function is wrapped."""
if hasattr(func, "__wrapped__"):
# error: "Callable[..., Any]" has no attribute "__wrapped__" [attr-defined]
print(f"func named {func.__name__} wraps {func.__wrapped__.__name__}.")
print_is_wrapped(wrap_print_int_arg)
Advertisement
Answer
Obviously the easy answer is to add a # type: ignore comment. However, this isn’t actually solving the problem, IMO.
I decided to make a type stub for a callable with a __wrapped__ attribute. Based on this answer, here is my current solution:
from typing import Callable, cast
class WrapsCallable:
"""Stub for a Callable with a __wrapped__ attribute."""
__wrapped__: Callable
__name__: str
def __call__(self, *args, **kwargs):
...
def print_is_wrapped(func: Callable) -> None:
"""Print if a function is wrapped."""
if hasattr(func, "__wrapped__"):
func = cast(WrapsCallable, func)
print(f"func named {func.__name__} wraps {func.__wrapped__.__name__}.")
And mypy now reports Success: no issues found in 1 source file.
I feel as if this is a lot of boiler-plate code, and would love a more streamlined answer.