Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Unreleased

Nothing yet!
- Added `PyAwaitable_AddExpr`.

## [2.0.1] - 2025-06-15

Expand Down
17 changes: 17 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,23 @@ Coroutines
set on failure.


.. c:function:: int PyAwaitable_AddExpr(PyObject *awaitable, PyObject *expr, PyAwaitable_Callback result_callback, PyAwaitable_Error error_callback)

Similar to :c:func:`PyAwaitable_AddAwait`, but designed for convenience.

If *expr* is ``NULL``, this function returns ``-1`` without an exception
set. If *expr* is non-``NULL``, this function calls
:c:func:`PyAwaitable_AddAwait` with all the provided arguments, and then
steals a reference to *expr*.

This behavior allows you to use other C API functions directly with this
one. For example, if you had an ``async def`` function ``coro``, it could
be added to the PyAwaitable object with
``PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(coro), NULL, NULL)``.

.. versionadded:: 2.1


Value Storage
-------------

Expand Down
91 changes: 69 additions & 22 deletions docs/usage/adding_awaits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,73 @@ hidden; you give PyAwaitable your coroutine, and it handles the rest!

Yay! We called an asynchronous function from C!

Simpler ``PyAwaitable_AddAwait`` Calls
--------------------------------------

But, what if we wanted to call the ``async def`` function from the C API?
With our current knowledge, that would look like this:

.. code-block:: c

static PyObject *
trampoline(PyObject *self, PyObject *func) // METH_O
{
PyObject *awaitable = PyAwaitable_New();
if (awaitable == NULL) {
return NULL;
}

PyObject *coro = PyObject_CallNoArgs(func);
if (coro == NULL) {
Py_DECREF(awaitable);
return NULL;
}

if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) {
Py_DECREF(awaitable);
Py_DECREF(coro);
return NULL;
}

Py_DECREF(coro);
return awaitable;
}

Ouch, that's a lot of boilerplate. Luckily, PyAwaitable provides a convenience
function for this case: :c:func:`PyAwaitable_AddExpr`. This function is very
similar to :c:func:`PyAwaitable_AddAwait`, but it has two additional semantics
for the passed coroutine:

- If the coroutine is ``NULL``, it returns ``-1`` without setting an
exception.
- If the coroutine is non-``NULL``, it passes it to
:c:func:`PyAwaitable_AddAwait` and then decrements its reference count
("stealing a reference").

These properties make it possible to directly use the result of a C API
function without extra boilerplate, because errors will be propagated when
it fails (when the coroutine is ``NULL``) and the reference count will be
decremented, preventing leaks.

So, with that in mind, we can rewrite our example as the following:

.. code-block:: c

static PyObject *
trampoline(PyObject *self, PyObject *func) // METH_O
{
PyObject *awaitable = PyAwaitable_New();
if (awaitable == NULL) {
return NULL;
}

if (PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(func), NULL, NULL) < 0) {
Py_DECREF(awaitable);
return NULL;
}

return awaitable;
}

.. _return-value-callbacks:

Expand Down Expand Up @@ -138,14 +205,7 @@ Now, we can use the result of ``silly()`` in C:
return NULL;
}

// Get the coroutine by calling silly()
PyObject *coro = PyObject_CallNoArgs(silly);
if (coro == NULL) {
Py_DECREF(awaitable);
return NULL;
}

if (PyAwaitable_AddAwait(awaitable, coro, callback, NULL) < 0) {
if (PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(silly), callback, NULL) < 0) {
Py_DECREF(awaitable);
Py_DECREF(coro);
return NULL;
Expand Down Expand Up @@ -279,24 +339,11 @@ In C, all that would be implemented like this:
return NULL;
}

// Remember, this isn't the same as executing the coroutine, so
// the timeout doesn't show up here. But, we still need to handle
// an exception case, because something might have gone wrong
// in getting the coroutine object, e.g., the object isn't callable
// or we're out of memory.
PyObject *coro = PyObject_CallNoArgs(make_request);
if (coro == NULL) {
if (PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(coro), return_true, return_false)) {
Py_DECREF(awaitable);
return NULL;
}

if (PyAwaitable_AddAwait(awaitable, coro, return_true, return_false)) {
Py_DECREF(awaitable);
Py_DECREF(coro);
return NULL;
}

Py_DECREF(coro);
return awaitable;
}

Expand Down
11 changes: 2 additions & 9 deletions docs/usage/value_storage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,12 @@ as such:
return NULL;
}

PyObject *coro = PyObject_CallNoArgs(get_number_io);
if (coro == NULL) {
if (PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(get_number_io),
multiply_callback, NULL) < 0) {
Py_DECREF(awaitable);
return NULL;
}

if (PyAwaitable_AddAwait(awaitable, coro, multiply_callback, NULL) < 0) {
Py_DECREF(awaitable);
Py_DECREF(coro);
return NULL;
}

Py_DECREF(coro);
return awaitable;
}

Expand Down
18 changes: 18 additions & 0 deletions src/_pyawaitable/awaitable.c
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,24 @@ PyAwaitable_AddAwait(
return 0;
}

_PyAwaitable_API(int)
PyAwaitable_AddExpr(
PyObject * self,
PyObject * expr,
PyAwaitable_Callback cb,
PyAwaitable_Error err
)
{
assert(self != NULL);
if (expr == NULL) {
return -1;
}

int res = PyAwaitable_AddAwait(self, expr, cb, err);
Py_DECREF(expr);
return res;
}

_PyAwaitable_API(int)
PyAwaitable_DeferAwait(PyObject * awaitable, PyAwaitable_Defer cb)
{
Expand Down
19 changes: 19 additions & 0 deletions tests/test_awaitable.c
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,31 @@ coroutine_trampoline(PyObject *self, PyObject *coro)
return awaitable;
}

static PyObject *
test_add_await_expr(PyObject *self, PyObject *nothing)
{
PyObject *awaitable = PyAwaitable_New();
if (awaitable == NULL) {
return NULL;
}

int res = PyAwaitable_AddExpr(awaitable, NULL, NULL, NULL);
TEST_ASSERT(res == -1);

res = PyAwaitable_AddExpr(awaitable, PyAwaitable_New(), NULL, NULL);
TEST_ASSERT(res == 0);

PyAwaitable_Cancel(awaitable);
Py_RETURN_NONE;
}

TESTS(awaitable) = {
TEST_UTIL(generic_awaitable),
TEST(test_awaitable_new),
TEST(test_set_result),
TEST_CORO(test_add_await),
TEST_CORO(test_add_await_special_cases),
TEST_UTIL(coroutine_trampoline),
TEST(test_add_await_expr),
{NULL}
};
Loading