I wanted a mechanism to check if a given Python object was "callable" - could be called like a function - or "async callable" - could be called using await obj()
.
I ended up writing this helper function to help introspect objects and answer those questions:
import asyncio
import types
from typing import NamedTuple, Any
class CallableStatus(NamedTuple):
is_callable: bool
is_async_callable: bool
def check_callable(obj: Any) -> CallableStatus:
if not callable(obj):
return CallableStatus(False, False)
if isinstance(obj, type):
# It's a class
return CallableStatus(True, False)
if isinstance(obj, types.FunctionType):
return CallableStatus(True, asyncio.iscoroutinefunction(obj))
if hasattr(obj, "__call__"):
return CallableStatus(True, asyncio.iscoroutinefunction(obj.__call__))
assert False, "obj {} is somehow callable with no __call__ method".format(repr(obj))
check_callable(obj)
returns a CallableStatus
named tuple, with an is_callable
boolean saying if it can be caled and an is_async_callable
boolean specifying if you need to use await
with it.
I wrote these pytest
tests to exercise the check_callable()
function:
import pytest
class AsyncClass:
async def __call__(self):
pass
class NotAsyncClass:
def __call__(self):
pass
class ClassNoCall:
pass
async def async_func():
pass
def non_async_func():
pass
@pytest.mark.parametrize(
"obj,expected_is_callable,expected_is_async_callable",
(
(async_func, True, True),
(non_async_func, True, False),
(AsyncClass(), True, True),
(NotAsyncClass(), True, False),
(ClassNoCall(), False, False),
(AsyncClass, True, False),
(NotAsyncClass, True, False),
(ClassNoCall, True, False),
("", False, False),
(1, False, False),
(str, True, False),
),
)
def test_check_callable(obj, expected_is_callable, expected_is_async_callable):
status = check_callable(obj)
assert status.is_callable == expected_is_callable
assert status.is_async_callable == expected_is_async_callable
See this commit for my initial implementation.
Created 2023-08-04T16:40:33-07:00 · Edit