Summary
sqlspec.utils.sync_tools.ensure_async_ is annotated as returning Callable[P, Awaitable[R]], but every path it can take returns a callable whose invocation produces a Coroutine. Callers that need a Callable[..., Coroutine[Any, Any, R]] — a common requirement for APIs that store and later await a callable — cannot accept the result without a suppression.
Coroutine is a subtype of Awaitable, so tightening the annotation is backward compatible for existing callers.
Detail
Current signature:
def ensure_async_(
function: "Callable[ParamSpecT, Awaitable[ReturnT] | ReturnT]",
) -> "Callable[ParamSpecT, Awaitable[ReturnT]]":
if inspect.iscoroutinefunction(function):
return function
return _EnsureAsyncWrapper(function)
Both branches yield a coroutine on call:
- the passthrough branch returns the original coroutine function, whose call produces a coroutine;
- the wrapping branch returns
_EnsureAsyncWrapper, whose __call__ is declared async def and therefore also produces a coroutine.
So the declared Awaitable is wider than what the function can actually return.
Confirmed awaitable at runtime on 0.58.2:
def f(x: int) -> int: return x
w = ensure_async_(f)
inspect.isawaitable(w(1)) # True
asyncio.run(ensure_async_(f)(7)) # 7
Suggested fix
) -> "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]":
Aside, possibly worth a separate look
On a compiled build, inspect.iscoroutinefunction() returns False for the returned wrapper and inspect.iscoroutine() returns False for its result — the call yields a __call____EnsureAsyncWrapper_gen object rather than a native coroutine. It is awaitable and behaves correctly, so this is not a bug in itself, but any code that branches on inspect.iscoroutinefunction will take the sync path for a wrapper produced here. Mentioning it in case that interacts with the passthrough check above, which is itself an iscoroutinefunction test.
Summary
sqlspec.utils.sync_tools.ensure_async_is annotated as returningCallable[P, Awaitable[R]], but every path it can take returns a callable whose invocation produces aCoroutine. Callers that need aCallable[..., Coroutine[Any, Any, R]]— a common requirement for APIs that store and later await a callable — cannot accept the result without a suppression.Coroutineis a subtype ofAwaitable, so tightening the annotation is backward compatible for existing callers.Detail
Current signature:
Both branches yield a coroutine on call:
_EnsureAsyncWrapper, whose__call__is declaredasync defand therefore also produces a coroutine.So the declared
Awaitableis wider than what the function can actually return.Confirmed awaitable at runtime on 0.58.2:
Suggested fix
Aside, possibly worth a separate look
On a compiled build,
inspect.iscoroutinefunction()returnsFalsefor the returned wrapper andinspect.iscoroutine()returnsFalsefor its result — the call yields a__call____EnsureAsyncWrapper_genobject rather than a native coroutine. It is awaitable and behaves correctly, so this is not a bug in itself, but any code that branches oninspect.iscoroutinefunctionwill take the sync path for a wrapper produced here. Mentioning it in case that interacts with the passthrough check above, which is itself aniscoroutinefunctiontest.