From aafac649cb450319220fd5b2802babd8f74dd62b Mon Sep 17 00:00:00 2001 From: makiaveli1 Date: Wed, 30 Sep 2026 15:26:24 +0100 Subject: [PATCH] Fix check_callable() reporting wrapped async callables as synchronous check_callable() only inspected objects that are exactly types.FunctionType, and otherwise looked at obj.__call__. For a bound async method or a functools.partial wrapping an async function, __call__ is a method wrapper rather than a coroutine function, so both were reported as synchronous even though inspect.iscoroutinefunction() agrees they are async. Inspect the object itself first, which covers functions, bound methods and partial objects, and keep the __call__ check for callable instances. --- datasette/utils/check_callable.py | 8 +++++--- tests/test_utils_check_callable.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) 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):