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.