Skip to content

Commit 7f81160

Browse files
committed
Update PyAwaitable_AsyncWith to returns -1 when aw or ctx are NULL.
Also included is documentation for using PyAwaitable_AsyncWith as well. Signed-off-by: AraHaan <seandhunt_7@yahoo.com>
1 parent fb160c7 commit 7f81160

3 files changed

Lines changed: 116 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Added `PyAwaitable_AddExpr`.
6+
- `PyAwaitable_AsyncWith` now returns `-1` when `aw` or `ctx` is `NULL`.
67

78
## [2.0.1] - 2025-06-15
89

docs/usage/adding_awaits.rst

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,118 @@ So, with that in mind, we can rewrite our example as the following:
156156
157157
.. _return-value-callbacks:
158158

159+
Using ``async with`` from C
160+
---------------------------
161+
Unlike the :c:func:`PyAwaitable_AddAwait` and the :c:func:`PyAwaitable_AddExpr`
162+
functions there exists :c:func:`PyAwaitable_AsyncWith` that will call the
163+
`__aenter__` and `__aexit__` members of a class if it implements the Async Context
164+
Manager.
165+
166+
Using this on a type that is designed with `async with` in mind that automatically
167+
cleans up resources when the scope leaves the context manager can be much easier
168+
than manually making those calls to clean them up.
169+
170+
:c:func:`PyAwaitable_AsyncWith` takes four arguments:
171+
172+
- The PyAwaitable object.
173+
- An instance of the class that implements the async context manager.
174+
- A callback.
175+
- An error callback.
176+
177+
.. note::
178+
179+
This example uses the `asqlite` library created by Rapptz to allow using the
180+
`sqlite3` module from within Asynchronous code safely. It also uses a helper header
181+
file called `awaitfunc.h` from the https://github.com/AraHaan/awaitfunc github
182+
repository.
183+
184+
.. code-block:: c
185+
186+
static int
187+
add_or_delete_items_cursor_cb(PyObject *awaitable, PyObject *cursor) {
188+
if (cursor != NULL && !Py_IsNone(cursor)) {
189+
PyObject *args;
190+
PyObject *connection;
191+
if (PyAwaitable_UnpackValues(awaitable, &args, &connection) < 0) {
192+
return -1;
193+
}
194+
195+
if (PyAwaitable_AwaitFunction(awaitable, PyObject_GetCallableMethodString(cursor, "executemany"), "OO", NULL, NULL, PySequence_GetItem(args, 0), PySequence_GetItem(args, 1)) < 0) {
196+
return -1;
197+
}
198+
199+
if (PyAwaitable_AwaitFunctionNoArgs(awaitable, PyObject_GetCallableMethodString(connection, "commit"), NULL, NULL) < 0) {
200+
return -1;
201+
}
202+
203+
// No need to call "close" here for both cursor and connection because "PyAwaitable_AsyncWith" did it for us.
204+
205+
return 0;
206+
}
207+
208+
return -1;
209+
}
210+
211+
static int
212+
add_or_delete_items_connect_cb(PyObject *awaitable, PyObject *connection) {
213+
if (connection != NULL && !Py_IsNone(connection)) {
214+
PyObject *args;
215+
if (PyAwaitable_UnpackValues(awaitable, &args) < 0) {
216+
return -1;
217+
}
218+
219+
if (PyAwaitable_SaveValues(awaitable, 1, connection) < 0) {
220+
return -1;
221+
}
222+
223+
if (PyAwaitable_AsyncWithFunctionNoArgs(awaitable, PyObject_GetCallableMethodString(connection, "cursor"), add_or_delete_items_cursor_cb, NULL) < 0) {
224+
return -1;
225+
}
226+
227+
return 0;
228+
}
229+
230+
return -1;
231+
}
232+
233+
static PyObject *
234+
_DiscordBot___add_or_delete_items(PyObject *mod, PyObject *args) {
235+
PyObject *awaitable = PyAwaitable_New();
236+
if (!awaitable) {
237+
return NULL;
238+
}
239+
240+
if (PyAwaitable_SaveValues(awaitable, 1, args) < 0) {
241+
Py_XDECREF(awaitable);
242+
return NULL;
243+
}
244+
245+
PyObject *dbString = PyUnicode_FromString("Bot.db");
246+
if (!dbString) {
247+
Py_XDECREF(awaitable);
248+
return NULL;
249+
}
250+
251+
DiscordBot_State *state = get_DiscordBot_state(mod);
252+
if (PyAwaitable_AsyncWithFunction(awaitable, PyObject_GetCallableMethodString(state->asqliteModule, "connect"), "N", add_or_delete_items_connect_cb, NULL, dbString) < 0) {
253+
Py_XDECREF(awaitable);
254+
return NULL;
255+
}
256+
257+
return awaitable;
258+
}
259+
260+
The above is functionally equivalent to the following in Pure Python Code:
261+
262+
.. code-block:: py
263+
264+
async def __add_or_delete_items(query: str, values: list):
265+
"""Internal API. DO NOT USE."""
266+
async with asqlite.connect('Bot.db') as connection:
267+
async with connection.cursor() as cursor:
268+
await cursor.executemany(query, values)
269+
await connection.commit()
270+
159271
Getting the Return Value in a Callback
160272
--------------------------------------
161273

src/_pyawaitable/with.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ PyAwaitable_AsyncWith(
9090
PyAwaitable_Error err
9191
)
9292
{
93+
if (aw == NULL || ctx == NULL) {
94+
return -1;
95+
}
9396
PyObject *with = PyObject_GetAttrString(ctx, "__aenter__");
9497
if (with == NULL) {
9598
PyErr_Format(

0 commit comments

Comments
 (0)