have you tried your function in a type checker? Awaitable[R] | R doesn’t work very well.
from collections.abc import Callable, Awaitable
import inspect
from typing import Any
async def call[**P, R](
coro_or_func: Callable[P, Awaitable[R] | R],
*args: P.args,
**kwargs: P.kwargs,
) -> R:
c = coro_or_func(*args, **kwargs)
if isinstance(c, Awaitable):
return await c
return c
async def ham(spam: str) -> int:
return 1
async def main() -> None:
v = await call(ham, spam="eggs")
reveal_type(v)
results in:
demo.py:21: error: Need type annotation for "v" [var-annotated]
demo.py:21: error: Argument 1 to "call" has incompatible type "Callable[[str], Coroutine[Any, Any, int]]"; expected "Callable[[str], Awaitable[Never]]" [arg-type]
demo.py:22: note: Revealed type is "Any"
It probably shouldn’t be common to support receiving either and not know which. You can always provide two separate interfaces and let the user call with the one matching what they are writing. This removes any runtime overhead for checking what you have and vastly simplifies code flow when reviewing. checking yourself with inspect is “fine” if you have no other option, or this is only done during application setup such as with route handlers, but your unified calling won’t work there. For example, FastAPI wants sync function to run in a thread as a route handler. It also has runtime overhead to check each time. Putting this overhead in the standard library instead as an intended pattern will just lead people to worse solutions IMO.
Making asyncio.run work from an async context will be full of other issues, and I think it’s a good thing that asyncio event loops aren’t reentrant.
@mikeshardmind thank you for your review! The performance argument makes sense to me. For the asyncio.run I didn’t mean reentrant loop, but rather a different naming for the call() function from original post. Like asyncio.run behaves differently in async context. This was probably a bad idea from the typing point of view, asking the same function to return a regular value or awaitable depending on the sync/async context where it was called from.