diff --git a/CHANGELOG.md b/CHANGELOG.md index ea960a8..53fb3e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -Nothing yet! +- Added `PyAwaitable_AddExpr`. ## [2.0.1] - 2025-06-15 diff --git a/docs/reference.rst b/docs/reference.rst index 190ee94..d2bf218 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -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 ------------- diff --git a/docs/usage/adding_awaits.rst b/docs/usage/adding_awaits.rst index 3dda75f..0958934 100644 --- a/docs/usage/adding_awaits.rst +++ b/docs/usage/adding_awaits.rst @@ -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: @@ -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; @@ -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; } diff --git a/docs/usage/value_storage.rst b/docs/usage/value_storage.rst index a77e343..661e4c3 100644 --- a/docs/usage/value_storage.rst +++ b/docs/usage/value_storage.rst @@ -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; } diff --git a/src/_pyawaitable/awaitable.c b/src/_pyawaitable/awaitable.c index ec3879b..a415603 100644 --- a/src/_pyawaitable/awaitable.c +++ b/src/_pyawaitable/awaitable.c @@ -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) { diff --git a/tests/test_awaitable.c b/tests/test_awaitable.c index 32923ed..a4a9809 100644 --- a/tests/test_awaitable.c +++ b/tests/test_awaitable.c @@ -157,6 +157,24 @@ 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), @@ -164,5 +182,6 @@ TESTS(awaitable) = { TEST_CORO(test_add_await), TEST_CORO(test_add_await_special_cases), TEST_UTIL(coroutine_trampoline), + TEST(test_add_await_expr), {NULL} };