Skip to content

Commit 34dcee2

Browse files
Core: Add PyAwaitable_AddExpr for convenience (#81)
Inspired by `PyModule_Add`. This function is equivalent to `PyAwaitable_AddAwait`, but returns `NULL` without setting an exception when given a `NULL` coroutine, and steals a reference to the coroutine when it is non-`NULL`. This makes it possible to directly use C API functions in the single `PyAwaitable_AddExpr` call.
1 parent 8404886 commit 34dcee2

File tree

6 files changed

+126
-32
lines changed

6 files changed

+126
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Unreleased
44

5-
Nothing yet!
5+
- Added `PyAwaitable_AddExpr`.
66

77
## [2.0.1] - 2025-06-15
88

docs/reference.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ Coroutines
9797
set on failure.
9898
9999
100+
.. c:function:: int PyAwaitable_AddExpr(PyObject *awaitable, PyObject *expr, PyAwaitable_Callback result_callback, PyAwaitable_Error error_callback)
101+
102+
Similar to :c:func:`PyAwaitable_AddAwait`, but designed for convenience.
103+
104+
If *expr* is ``NULL``, this function returns ``-1`` without an exception
105+
set. If *expr* is non-``NULL``, this function calls
106+
:c:func:`PyAwaitable_AddAwait` with all the provided arguments, and then
107+
steals a reference to *expr*.
108+
109+
This behavior allows you to use other C API functions directly with this
110+
one. For example, if you had an ``async def`` function ``coro``, it could
111+
be added to the PyAwaitable object with
112+
``PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(coro), NULL, NULL)``.
113+
114+
.. versionadded:: 2.1
115+
116+
100117
Value Storage
101118
-------------
102119

docs/usage/adding_awaits.rst

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,73 @@ hidden; you give PyAwaitable your coroutine, and it handles the rest!
8686
8787
Yay! We called an asynchronous function from C!
8888

89+
Simpler ``PyAwaitable_AddAwait`` Calls
90+
--------------------------------------
91+
92+
But, what if we wanted to call the ``async def`` function from the C API?
93+
With our current knowledge, that would look like this:
94+
95+
.. code-block:: c
96+
97+
static PyObject *
98+
trampoline(PyObject *self, PyObject *func) // METH_O
99+
{
100+
PyObject *awaitable = PyAwaitable_New();
101+
if (awaitable == NULL) {
102+
return NULL;
103+
}
104+
105+
PyObject *coro = PyObject_CallNoArgs(func);
106+
if (coro == NULL) {
107+
Py_DECREF(awaitable);
108+
return NULL;
109+
}
110+
111+
if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) {
112+
Py_DECREF(awaitable);
113+
Py_DECREF(coro);
114+
return NULL;
115+
}
116+
117+
Py_DECREF(coro);
118+
return awaitable;
119+
}
120+
121+
Ouch, that's a lot of boilerplate. Luckily, PyAwaitable provides a convenience
122+
function for this case: :c:func:`PyAwaitable_AddExpr`. This function is very
123+
similar to :c:func:`PyAwaitable_AddAwait`, but it has two additional semantics
124+
for the passed coroutine:
125+
126+
- If the coroutine is ``NULL``, it returns ``-1`` without setting an
127+
exception.
128+
- If the coroutine is non-``NULL``, it passes it to
129+
:c:func:`PyAwaitable_AddAwait` and then decrements its reference count
130+
("stealing a reference").
131+
132+
These properties make it possible to directly use the result of a C API
133+
function without extra boilerplate, because errors will be propagated when
134+
it fails (when the coroutine is ``NULL``) and the reference count will be
135+
decremented, preventing leaks.
136+
137+
So, with that in mind, we can rewrite our example as the following:
138+
139+
.. code-block:: c
140+
141+
static PyObject *
142+
trampoline(PyObject *self, PyObject *func) // METH_O
143+
{
144+
PyObject *awaitable = PyAwaitable_New();
145+
if (awaitable == NULL) {
146+
return NULL;
147+
}
148+
149+
if (PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(func), NULL, NULL) < 0) {
150+
Py_DECREF(awaitable);
151+
return NULL;
152+
}
153+
154+
return awaitable;
155+
}
89156
90157
.. _return-value-callbacks:
91158

@@ -138,14 +205,7 @@ Now, we can use the result of ``silly()`` in C:
138205
return NULL;
139206
}
140207
141-
// Get the coroutine by calling silly()
142-
PyObject *coro = PyObject_CallNoArgs(silly);
143-
if (coro == NULL) {
144-
Py_DECREF(awaitable);
145-
return NULL;
146-
}
147-
148-
if (PyAwaitable_AddAwait(awaitable, coro, callback, NULL) < 0) {
208+
if (PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(silly), callback, NULL) < 0) {
149209
Py_DECREF(awaitable);
150210
Py_DECREF(coro);
151211
return NULL;
@@ -279,24 +339,11 @@ In C, all that would be implemented like this:
279339
return NULL;
280340
}
281341
282-
// Remember, this isn't the same as executing the coroutine, so
283-
// the timeout doesn't show up here. But, we still need to handle
284-
// an exception case, because something might have gone wrong
285-
// in getting the coroutine object, e.g., the object isn't callable
286-
// or we're out of memory.
287-
PyObject *coro = PyObject_CallNoArgs(make_request);
288-
if (coro == NULL) {
342+
if (PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(coro), return_true, return_false)) {
289343
Py_DECREF(awaitable);
290344
return NULL;
291345
}
292346
293-
if (PyAwaitable_AddAwait(awaitable, coro, return_true, return_false)) {
294-
Py_DECREF(awaitable);
295-
Py_DECREF(coro);
296-
return NULL;
297-
}
298-
299-
Py_DECREF(coro);
300347
return awaitable;
301348
}
302349

docs/usage/value_storage.rst

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,12 @@ as such:
127127
return NULL;
128128
}
129129
130-
PyObject *coro = PyObject_CallNoArgs(get_number_io);
131-
if (coro == NULL) {
130+
if (PyAwaitable_AddExpr(awaitable, PyObject_CallNoArgs(get_number_io),
131+
multiply_callback, NULL) < 0) {
132132
Py_DECREF(awaitable);
133133
return NULL;
134134
}
135135
136-
if (PyAwaitable_AddAwait(awaitable, coro, multiply_callback, NULL) < 0) {
137-
Py_DECREF(awaitable);
138-
Py_DECREF(coro);
139-
return NULL;
140-
}
141-
142-
Py_DECREF(coro);
143136
return awaitable;
144137
}
145138

src/_pyawaitable/awaitable.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,24 @@ PyAwaitable_AddAwait(
209209
return 0;
210210
}
211211

212+
_PyAwaitable_API(int)
213+
PyAwaitable_AddExpr(
214+
PyObject * self,
215+
PyObject * expr,
216+
PyAwaitable_Callback cb,
217+
PyAwaitable_Error err
218+
)
219+
{
220+
assert(self != NULL);
221+
if (expr == NULL) {
222+
return -1;
223+
}
224+
225+
int res = PyAwaitable_AddAwait(self, expr, cb, err);
226+
Py_DECREF(expr);
227+
return res;
228+
}
229+
212230
_PyAwaitable_API(int)
213231
PyAwaitable_DeferAwait(PyObject * awaitable, PyAwaitable_Defer cb)
214232
{

tests/test_awaitable.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,31 @@ coroutine_trampoline(PyObject *self, PyObject *coro)
157157
return awaitable;
158158
}
159159

160+
static PyObject *
161+
test_add_await_expr(PyObject *self, PyObject *nothing)
162+
{
163+
PyObject *awaitable = PyAwaitable_New();
164+
if (awaitable == NULL) {
165+
return NULL;
166+
}
167+
168+
int res = PyAwaitable_AddExpr(awaitable, NULL, NULL, NULL);
169+
TEST_ASSERT(res == -1);
170+
171+
res = PyAwaitable_AddExpr(awaitable, PyAwaitable_New(), NULL, NULL);
172+
TEST_ASSERT(res == 0);
173+
174+
PyAwaitable_Cancel(awaitable);
175+
Py_RETURN_NONE;
176+
}
177+
160178
TESTS(awaitable) = {
161179
TEST_UTIL(generic_awaitable),
162180
TEST(test_awaitable_new),
163181
TEST(test_set_result),
164182
TEST_CORO(test_add_await),
165183
TEST_CORO(test_add_await_special_cases),
166184
TEST_UTIL(coroutine_trampoline),
185+
TEST(test_add_await_expr),
167186
{NULL}
168187
};

0 commit comments

Comments
 (0)