Skip to content

Commit

Permalink
Document the utility functions (#35)
Browse files Browse the repository at this point in the history
This documents `pyawaitable_async_with` and
`pyawaitable_await_function`.
  • Loading branch information
ZeroIntensity authored Oct 29, 2024
1 parent 30b23a3 commit 5db66ba
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 8 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
## [1.3.0] - 2024-10-26

- Added support for `async with` via `pyawaitable_async_with`.

Expand Down
2 changes: 2 additions & 0 deletions docs/awaiting.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,6 @@ In fact, we don't actually `await` anything in our function body. Instead, we ma

For example, if you wanted to `await` three coroutines: `foo`, `bar`, and `baz`, you would call `pyawaitable_await` on each of them, but they wouldn't actually get executed until _after_ the C function has returned.

## Next Steps

Now, how do we do something with the result of the awaited coroutine? We do that with a callback, which we'll talk about next.
2 changes: 1 addition & 1 deletion docs/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ err_callback(PyObject *awaitable, PyObject *err)
}
```

## Using Values
## Next Steps

Now that you've learned to use callbacks, how do we retain state from our original function (_i.e._, the thing that returned our PyAwaitable object) to a callback? In most other languages, this would be a stupid question, but if you've used callbacks in C before, you may have found yourself wondering what to do with the lack of closures.

Expand Down
4 changes: 1 addition & 3 deletions docs/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,4 @@ Great! We increment our state for each call!

## Next Steps

Congratuilations, you now know how to use PyAwaitable! If you're interested in reading about the internals, be sure to take a look at the [scrapped PEP draft](https://gist.github.com/ZeroIntensity/8d32e94b243529c7e1c27349e972d926), where this was originally designed to be part of CPython.

Moreover, this project was conceived due to being needed in [view.py](https://github.com/ZeroIntensity/view.py). If you would like to see some very complex examples of PyAwaitable usage, take a look at their [C ASGI implementation](https://github.com/ZeroIntensity/view.py/blob/master/src/_view/app.c#L273), which is powered by PyAwaitable.
So, now we know how to use PyAwaitable, but there's quite a bit of boilerplate here. PyAwaitable comes with a few utility functions to help ease boilerplate, which we'll talk about next.
92 changes: 92 additions & 0 deletions docs/utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
hide:
- navigation
---

# Useful Utilities

## Introduction

So far, it might seem like this is a lot of boilerplate. That's unfortunately decently common for the C API, but you get used to it. With that being said, how can we help eliminate at least _some_ of this boilerplate?

## Calling

In general, calling functions in the C API is a lot of work--is there any way to make it prettier in PyAwaitable? CPython has `PyObject_CallFunction`, which allows you to call a function with a format string similar to `Py_BuildValue`. For example:

```c
static PyObject *
test(PyObject *self, PyObject *my_func)
{
// Equivalent to my_func(42, -10)
PyObject *result = PyObject_CallFunction(my_func, "ii", 42, -10);
/* ... */
}
```
For convenience, PyAwaitable has an analogue of this function, called `pyawaitable_await_function`, which calls a function with a format string _and_ marks the result (as in, the returned coroutine) for execution via `pyawaitable_await`. For example, if `my_func` from above was asynchronous:
```c
static PyObject *
test(PyObject *self, PyObject *my_func)
{
PyObject *awaitable = pyawaitable_new();
// Equivalent to await my_func(42, -10)
if (pyawaitable_await_function(awaitable, my_func, "ii", NULL, NULL, 42, -10) < 0)
{
Py_DECREF(awaitable);
return NULL;
}
/* ... */
}
```

Much nicer, right?

## Asynchronous Contexts

What about using `async with` from C? Well, asynchronous context managers are sort of simple, you just have to deal with calling `__aenter__` and `__aexit__`. But that's no fun--can we do it automatically? Yes you can!

PyAwaitable supports asynchronous context managers via `pyawaitable_async_with`. To start, let's start with some Python code that we want to replicate in C:

```py
async def my_function(async_context):
async with async_context as value:
print(f"My async value is {value}")
```

`pyawaitable_async_with` is pretty similiar to `pyawaitable_await`, but instead of taking a callback with the result of an awaited coroutine, it takes a callback that is ran when inside the context. So, translating the above snippet in C would look like:

```c
static int
inner(PyObject *awaitable, PyObject *value)
{
// Inside the context!
printf("My async value is: ");
PyObject_Print(value, stdout, Py_PRINT_RAW);
return 0;
}

static PyObject *
my_function(PyObject *self, PyObject *async_context)
{
PyObject *awaitable = pyawaitable_new();

// Equivalent to async with async_context
if (pyawaitable_async_with(awaitable, async_context, inner, NULL) < 0)
{
Py_DECREF(awaitable);
return NULL;
}

return awaitable;
}
```
Again, the `NULL` parameter here is an error callback. It's equivalent to what would happen if you wrapped a `try` block around an `async with`.
## Next Steps
Congratulations, you now know how to fully use PyAwaitable! If you're interested in reading about the internals, be sure to take a look at the [scrapped PEP draft](https://gist.github.com/ZeroIntensity/8d32e94b243529c7e1c27349e972d926), where this was originally designed to be part of CPython.
Moreover, this project was conceived due to being needed in [view.py](https://github.com/ZeroIntensity/view.py). If you would like to see some very complex examples of PyAwaitable usage, take a look at their [C ASGI implementation](https://github.com/ZeroIntensity/view.py/blob/master/src/_view/app.c#L273), which is powered by PyAwaitable.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ nav:
- Awaiting Coroutines: awaiting.md
- Callbacks: callbacks.md
- Storing and Fetching Values: storage.md
- Useful Utilities: utilities.md

theme:
name: material
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pyawaitable"
description = "Call asynchronous code from an extension module."
authors = [
{ name = "ZeroIntensity", email = "zintensitydev@gmail.com" },
]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
setup(
name="pyawaitable",
license="MIT",
version="1.3.0-dev",
version="1.3.0",
ext_modules=[
Extension(
"_pyawaitable",
Expand Down
2 changes: 1 addition & 1 deletion src/pyawaitable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from . import abi

__all__ = "PyAwaitable", "include", "abi"
__version__ = "1.3.0-dev"
__version__ = "1.3.0"

PyAwaitable: Type = _PyAwaitableType

Expand Down
2 changes: 1 addition & 1 deletion src/pyawaitable/pyawaitable.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#define PYAWAITABLE_MINOR_VERSION 3
#define PYAWAITABLE_MICRO_VERSION 0
/* Per CPython Conventions: 0xA for alpha, 0xB for beta, 0xC for release candidate or 0xF for final. */
#define PYAWAITABLE_RELEASE_LEVEL 0xA
#define PYAWAITABLE_RELEASE_LEVEL 0xF

typedef int (*awaitcallback)(PyObject *, PyObject *);
typedef int (*awaitcallback_err)(PyObject *, PyObject *);
Expand Down

0 comments on commit 5db66ba

Please sign in to comment.