Skip to content

Commit 5a6f996

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 5a6f996

3 files changed

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

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)