Hello everyone!
Similar topics of mine have gone through discussion previously in January and last year. I would recommend reading these two to fully understand this discussion.
Over the two weeks or so, I’ve gone back and forth with @encukou on developing a PEP on this topic. My reference implementation only uses the public API, and would be a better fit as a library rather than an addition to CPython. For this reason, my previous draft PEP was scrapped in favor of turning it into a library. The details of my previous implementation are long and complicated, but in short, PyAwaitableObject *
is an object that stores coroutines, then uses __await__
and __next__
to yield them to the event loop. For exact information on how this works, see my scrapped PEP.
I’ve come up with a new design that proposes a METH_ASYNC
flag (inspired by this discussion) but it certainly requires some discussion before a PEP could be written. Ideally, if an API like this was to be added to CPython, my reference implementation would exist as a backport library to allow others to pick it up immediately (similar to what asyncio
did).
The redesigned API looks like this:
static PyObject *
hello(PyObject *self, PyObject *awaitable, PyObject *args)
{
PyObject *coro;
if (!PyArg_ParseTuple(args, "O", &coro))
return NULL;
if (PyAwaitable_AWAIT(awaitable, coro) < 0)
return NULL;
Py_RETURN_NONE; // equivalent to PyAwaitable_SetResult(awaitable, Py_None)
}
static PyMethodDef HelloMethods[] = {
{"hello", hello, METH_VARARGS | METH_ASYNC, NULL},
{NULL, NULL, 0, NULL}
}
The two key upgrades in the new design are:
- The user does not have to make any call to
PyAwaitable_New
, nor worry aboutPy_DECREF
ing the awaitable on an error check. This simple change eliminates a lot of boilerplate ofPy_DECREF
calls. - The user may return any
PyObject *
instead of just the awaitable, which may be more clear for setting return values.
I have written a prototype for this here. This can go a step further and use PyObject *
return values in callbacks as well (this is not included in the prototype implementation):
static PyObject *
hello_callback(PyObject *awaitable, PyObject *result)
{
// Assume that result is an int
long value = PyLong_AsLong(result);
if ((value == -1) && PyErr_Occurred())
return NULL;
if (value == 24)
return PyLong_FromLong(42);
PyAwaitable_RETURN_IGNORE; // This indicates that the return value should not be updated, and will remain as None
}
static PyObject *
hello(PyObject *self, PyObject *awaitable, PyObject *args)
{
PyObject *coro;
if (!PyArg_ParseTuple(args, "O", &coro))
return NULL;
if (PyAwaitable_AddAwait(awaitable, coro, hello_callback, NULL) < 0)
return NULL;
Py_RETURN_NONE; // equivalent to PyAwaitable_SetResult(awaitable, Py_None)
}
static PyMethodDef HelloMethods[] = {
{"hello", hello, METH_VARARGS | METH_ASYNC, NULL},
{NULL, NULL, 0, NULL}
}
A few notes:
- My prototype implementation is simply a thin wrapper on top of my previous reference implementation, but this would not be the case in an actual CPython version.
- The previous implementation, as stated before, uses a new
PyAwaitableObject *
, but perhaps a new object is not needed? Maybe thePyObject *awaitable
in the above example could refer to aPyCoroObject *
, but that would obviously require some modifications to the coroutine implementation. - I chose
PyAwaitable_
as the prefix previously as that was the object’s name. In this new design, perhaps this prefix could change (such as maybe something likePyAsync_
). Alternatively, if the coroutine object just gets reused, thePyCoro_
prefix should be chosen instead.
Nonetheless, I’m not totally sure whether METH_ASYNC
would be needed in the first place if my library exists, but that’s what this discussion is for