Useful Utilities
Introduction
So far, it might seem like this is a lot of boilerplate. That's unfortunately decently common for the C API, but you get used to it. With that being said, how can we help eliminate at least some of this boilerplate?
Calling
In general, calling functions in the C API is a lot of work--is there any way to make it prettier in PyAwaitable? CPython has PyObject_CallFunction
, which allows you to call a function with a format string similar to Py_BuildValue
. For example:
static PyObject *
test(PyObject *self, PyObject *my_func)
{
// Equivalent to my_func(42, -10)
PyObject *result = PyObject_CallFunction(my_func, "ii", 42, -10);
/* ... */
}
For convenience, PyAwaitable has an analogue of this function, called pyawaitable_await_function
, which calls a function with a format string and marks the result (as in, the returned coroutine) for execution via pyawaitable_await
. For example, if my_func
from above was asynchronous:
static PyObject *
test(PyObject *self, PyObject *my_func)
{
PyObject *awaitable = pyawaitable_new();
// Equivalent to await my_func(42, -10)
if (pyawaitable_await_function(awaitable, my_func, "ii", NULL, NULL, 42, -10) < 0)
{
Py_DECREF(awaitable);
return NULL;
}
/* ... */
}
Much nicer, right?
Asynchronous Contexts
What about using async with
from C? Well, asynchronous context managers are sort of simple, you just have to deal with calling __aenter__
and __aexit__
. But that's no fun--can we do it automatically? Yes you can!
PyAwaitable supports asynchronous context managers via pyawaitable_async_with
. To start, let's start with some Python code that we want to replicate in C:
async def my_function(async_context):
async with async_context as value:
print(f"My async value is {value}")
pyawaitable_async_with
is pretty similiar to pyawaitable_await
, but instead of taking a callback with the result of an awaited coroutine, it takes a callback that is ran when inside the context. So, translating the above snippet in C would look like:
static int
inner(PyObject *awaitable, PyObject *value)
{
// Inside the context!
printf("My async value is: ");
PyObject_Print(value, stdout, Py_PRINT_RAW);
return 0;
}
static PyObject *
my_function(PyObject *self, PyObject *async_context)
{
PyObject *awaitable = pyawaitable_new();
// Equivalent to async with async_context
if (pyawaitable_async_with(awaitable, async_context, inner, NULL) < 0)
{
Py_DECREF(awaitable);
return NULL;
}
return awaitable;
}
Again, the NULL
parameter here is an error callback. It's equivalent to what would happen if you wrapped a try
block around an async with
.
Next Steps
Congratulations, you now know how to fully use PyAwaitable! If you're interested in reading about the internals, be sure to take a look at the scrapped PEP draft, where this was originally designed to be part of CPython.
Moreover, this project was conceived due to being needed in view.py. If you would like to see some very complex examples of PyAwaitable usage, take a look at their C ASGI implementation, which is powered by PyAwaitable.