diff --git a/datasette/utils/check_callable.py b/datasette/utils/check_callable.py index e21a769b..e3e809b2 100644 --- a/datasette/utils/check_callable.py +++ b/datasette/utils/check_callable.py @@ -1,5 +1,4 @@ import inspect -import types from typing import Any, NamedTuple @@ -16,8 +15,11 @@ def check_callable(obj: Any) -> CallableStatus: # It's a class return CallableStatus(True, False) - if isinstance(obj, types.FunctionType): - return CallableStatus(True, inspect.iscoroutinefunction(obj)) + if inspect.iscoroutinefunction(obj): + # Inspect the object itself, so bound methods and functools.partial objects + # wrapping an async function are recognised - they are not types.FunctionType, + # and their __call__ is a method wrapper rather than a coroutine function. + return CallableStatus(True, True) if callable(obj): return CallableStatus(True, inspect.iscoroutinefunction(obj.__call__)) diff --git a/tests/test_utils_check_callable.py b/tests/test_utils_check_callable.py index 857b73cd..ebe84764 100644 --- a/tests/test_utils_check_callable.py +++ b/tests/test_utils_check_callable.py @@ -1,3 +1,5 @@ +import functools + import pytest from datasette.utils.check_callable import check_callable @@ -17,6 +19,14 @@ class ClassNoCall: pass +class MethodHolder: + async def async_method(self): + pass + + def sync_method(self): + pass + + async def async_func(): pass @@ -39,6 +49,12 @@ def non_async_func(): ("", False, False), (1, False, False), (str, True, False), + # Bound methods and functools.partial objects are not types.FunctionType, + # but they are still async when the function they wrap is + (MethodHolder().async_method, True, True), + (MethodHolder().sync_method, True, False), + (functools.partial(async_func), True, True), + (functools.partial(non_async_func), True, False), ), ) def test_check_callable(obj, expected_is_callable, expected_is_async_callable):