Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Added `PyAwaitable_AddExpr`.
- Fix assertion failures when running in debug mode.
- `PyAwaitable_AsyncWith` now returns `-1` when `aw` or `ctx` is `NULL`.

## [2.0.1] - 2025-06-15

Expand Down
112 changes: 112 additions & 0 deletions docs/usage/adding_awaits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,118 @@ So, with that in mind, we can rewrite our example as the following:

.. _return-value-callbacks:

Using ``async with`` from C
---------------------------
Unlike the :c:func:`PyAwaitable_AddAwait` and the :c:func:`PyAwaitable_AddExpr`
functions there exists :c:func:`PyAwaitable_AsyncWith` that will call the
`__aenter__` and `__aexit__` members of a class if it implements the Async Context
Comment thread
AraHaan marked this conversation as resolved.
Outdated
Comment thread
AraHaan marked this conversation as resolved.
Outdated
Manager.

Using this on a type that is designed with `async with` in mind that automatically
cleans up resources when the scope leaves the context manager can be much easier
than manually making those calls to clean them up.

:c:func:`PyAwaitable_AsyncWith` takes four arguments:

- The PyAwaitable object.
- An instance of the class that implements the async context manager.
- A callback.
- An error callback.

.. note::

This example uses the `asqlite` library created by Rapptz to allow using the
`sqlite3` module from within Asynchronous code safely. It also uses a helper header
file called `awaitfunc.h` from the https://github.com/AraHaan/awaitfunc github
repository.

.. code-block:: c
Comment thread
AraHaan marked this conversation as resolved.

static int
add_or_delete_items_cursor_cb(PyObject *awaitable, PyObject *cursor) {
if (cursor != NULL && !Py_IsNone(cursor)) {
PyObject *args;
PyObject *connection;
if (PyAwaitable_UnpackValues(awaitable, &args, &connection) < 0) {
return -1;
}

if (PyAwaitable_AwaitFunction(awaitable, PyObject_GetCallableMethodString(cursor, "executemany"), "OO", NULL, NULL, PySequence_GetItem(args, 0), PySequence_GetItem(args, 1)) < 0) {
return -1;
}

if (PyAwaitable_AwaitFunctionNoArgs(awaitable, PyObject_GetCallableMethodString(connection, "commit"), NULL, NULL) < 0) {
return -1;
}

// No need to call "close" here for both cursor and connection because "PyAwaitable_AsyncWith" did it for us.

return 0;
}

return -1;
}

static int
add_or_delete_items_connect_cb(PyObject *awaitable, PyObject *connection) {
if (connection != NULL && !Py_IsNone(connection)) {
PyObject *args;
if (PyAwaitable_UnpackValues(awaitable, &args) < 0) {
return -1;
}

if (PyAwaitable_SaveValues(awaitable, 1, connection) < 0) {
return -1;
}

if (PyAwaitable_AsyncWithFunctionNoArgs(awaitable, PyObject_GetCallableMethodString(connection, "cursor"), add_or_delete_items_cursor_cb, NULL) < 0) {
return -1;
}

return 0;
}

return -1;
}

static PyObject *
_DiscordBot___add_or_delete_items(PyObject *mod, PyObject *args) {
PyObject *awaitable = PyAwaitable_New();
if (!awaitable) {
return NULL;
}

if (PyAwaitable_SaveValues(awaitable, 1, args) < 0) {
Py_XDECREF(awaitable);
return NULL;
}

PyObject *dbString = PyUnicode_FromString("Bot.db");
if (!dbString) {
Py_XDECREF(awaitable);
return NULL;
}

DiscordBot_State *state = get_DiscordBot_state(mod);
if (PyAwaitable_AsyncWithFunction(awaitable, PyObject_GetCallableMethodString(state->asqliteModule, "connect"), "N", add_or_delete_items_connect_cb, NULL, dbString) < 0) {
Py_XDECREF(awaitable);
return NULL;
}

return awaitable;
}

The above is functionally equivalent to the following in Pure Python Code:

.. code-block:: py

async def __add_or_delete_items(query: str, values: list):
"""Internal API. DO NOT USE."""
async with asqlite.connect('Bot.db') as connection:
async with connection.cursor() as cursor:
await cursor.executemany(query, values)
await connection.commit()

Getting the Return Value in a Callback
--------------------------------------

Expand Down
3 changes: 3 additions & 0 deletions src/_pyawaitable/with.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ PyAwaitable_AsyncWith(
PyAwaitable_Error err
)
{
if (aw == NULL || ctx == NULL) {
return -1;
}
Comment thread
AraHaan marked this conversation as resolved.
Outdated
PyObject *with = PyObject_GetAttrString(ctx, "__aenter__");
if (with == NULL) {
PyErr_Format(
Expand Down
Loading