Skip to content

ensure_async_ return annotation is wider than what it returns (Awaitable vs Coroutine) #682

Description

@cofin

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions