Skip to content

Commit

Permalink
Merge pull request #17 from ZeroIntensity/code-cleanup
Browse files Browse the repository at this point in the history
Release candidate 1
  • Loading branch information
ZeroIntensity authored Jun 22, 2024
2 parents 31cafee + aabdede commit 21240aa
Show file tree
Hide file tree
Showing 38 changed files with 1,767 additions and 741 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ on:
push:
tags:
- v*
pull_request:
branches:
- master

concurrency:
group: build-${{ github.head_ref }}
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/memory_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Memory Check

on:
push:
branches:
- master
pull_request:
branches:
- master

env:
PYTHONUNBUFFERED: "1"
FORCE_COLOR: "1"
PYTHONIOENCODING: "utf8"

jobs:
run:
name: Valgrind on Ubuntu
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Set up Python 3.12
uses: actions/setup-python@v2
with:
python-version: 3.12

- name: Install Pytest
run: |
pip install pytest pytest-asyncio typing_extensions
shell: bash

- name: Build PyAwaitable
run: pip install .

- name: Build PyAwaitable Test Package
run: pip install setuptools wheel && pip install tests/extension/ --no-build-isolation

- name: Install Valgrind
run: sudo apt-get update && sudo apt-get -y install valgrind

- name: Run tests with Valgrind
run: valgrind --suppressions=valgrind-python.supp --error-exitcode=1 pytest -x
33 changes: 18 additions & 15 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,26 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install PyTest
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pip install pytest pytest-asyncio typing_extensions
else
pip install pytest pytest-asyncio pytest-memray typing_extensions
fi
- name: Install Pytest
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pip install pytest pytest-asyncio typing_extensions
else
pip install pytest pytest-asyncio pytest-memray typing_extensions
fi
shell: bash

- name: Build PyAwaitable
run: pip install .

- name: Run tests
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pytest
else
pytest --memray
fi
- name: Build PyAwaitable Test Package
run: pip install setuptools wheel && pip install ./tests/extension/ --no-build-isolation

- name: Run tests
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pytest -W error
else
python3 -m pytest -W error --memray
fi
shell: bash
15 changes: 11 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# Python
__pycache__/
.venv/
compile_flags.txt

*.egg-info
build/
test.py
test/
dist/

# LSP
compile_flags.txt
build/
.vscode/
.vs/
*.user
*.sln
*.user
*.vcxproj*

# Misc
test.py
vgcore*
67 changes: 67 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Contributing to PyAwaitable

Lucky for you, the internals of PyAwaitable are extremely well documented, since it was originally designed to be part of the CPython API.

Before you get started, it's a good idea to read the following discussions, or at least skim through them:

- [Adding a C API for coroutines/awaitables](https://discuss.python.org/t/adding-a-c-api-for-coroutines-awaitables/22786)
- [C API for asynchronous functions](https://discuss.python.org/t/c-api-for-asynchronous-functions/42842)
- [Revisiting a C API for asynchronous functions](https://discuss.python.org/t/revisiting-a-c-api-for-asynchronous-functions/50792)

Then, for all the details of the underlying implementation, read the [scrapped PEP](https://gist.github.com/ZeroIntensity/8d32e94b243529c7e1c27349e972d926).

## Development Workflow

You'll first want to find an [issue](https://github.com/ZeroIntensity/pyawaitable/issues) that you want to implement. Make sure not to choose an issue that already has someone assigned to it!

Once you've chosen something you would like to work on, be sure to make a comment requesting that the issue be assigned to you. You can start working on the issue before you've been officially assigned to it on GitHub, as long as you made a comment first.

After you're done, make a [pull request](https://github.com/ZeroIntensity/pyawaitable/pulls) merging your code to the master branch. A successful pull request will have all of the following:

- A link to the issue that it's implementing.
- New and passing tests.
- Updated docs and changelog.
- Code following the style guide, mentioned below.

## Style Guide

PyAwaitable follows [PEP 7](https://peps.python.org/pep-0007/), so if you've written any code in the CPython core, you'll feel right at home writing code for PyAwaitable.

However, don't bother trying to format things yourself! PyAwaitable provides an [uncrustify](https://github.com/uncrustify/uncrustify) configuration file for you.

## Project Setup

If you haven't already, clone the project.

```
$ git clone https://github.com/ZeroIntensity/pyawaitable
$ cd pyawaitable
```

To build PyAwaitable locally, simple run `pip`:

```
$ pip install .
```

It's highly recommended to do this inside of a [virtual environment](https://docs.python.org/3/library/venv.html).

## Running Tests

PyAwaitable uses three libraries for unit testing:

- [pytest](https://docs.pytest.org/en/8.2.x/), as the general testing framework.
- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/en/latest/), for asynchronous tests.
- [pytest-memray](https://pytest-memray.readthedocs.io/en/latest/), for detection of memory leaks. Note this isn't available for Windows, so simply omit this in your installation.

Installation is trivial:

```
$ pip install pytest pytest-asyncio pytest-memray
```

Tests generally access the PyAwaitable API functions using [ctypes](https://docs.python.org/3/library/ctypes.html), but there's also an extension module solely built for tests called `_pyawaitable_test`. You can install this with the following command:

```
$ pip install setuptools wheel && pip install ./test/extension/ --no-build-isolation
```
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include src/pyawaitable/*.h
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ build-backend = "setuptools.build_meta"
## Example

```c
#include <awaitable.h>
#include <pyawaitable.h>

// Assuming that this is using METH_O
static PyObject *
hello(PyObject *self, PyObject *coro) {
// Make our awaitable object
PyObject *awaitable = awaitable_new();
PyObject *awaitable = pyawaitable_new();

if (!awaitable)
return NULL;

// Mark the coroutine for being awaited
if (awaitable_await(awaitable, coro, NULL, NULL) < 0) {
if (pyawaitable_await(awaitable, coro, NULL, NULL) < 0) {
Py_DECREF(awaitable);
return NULL;
}
Expand All @@ -55,7 +55,7 @@ async def coro():
await asyncio.sleep(1)
print("awaited from C!")
# Use our C function to await coro
# Use our C function to await it
await hello(coro())
```

Expand Down
82 changes: 55 additions & 27 deletions docs/adding_coros.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
---
hide:
- navigation
- navigation
---

# Adding Coroutines

## Basics

The public interface for adding a coroutine to be executed by the event loop is ``awaitable_await``, which takes four parameters:
The public interface for adding a coroutine to be executed by the event loop is `pyawaitable_await`, which takes four parameters:

```c
// Signature of awaitable_await, for reference
// Signature of pyawaitable_await, for reference
int
awaitable_await(
pyawaitable_await(
PyObject *aw,
PyObject *coro,
awaitcallback cb,
Expand All @@ -22,21 +22,20 @@ awaitable_await(

!!! warning

If you are using the `PyAwaitable_` prefix, the function is ``PyAwaitable_AddAwait`` instead of ``PyAwaitable_Await``, per previous implementations of PyAwaitable.
If you are using the Python API names, the function is ``PyAwaitable_AddAwait`` instead of ``PyAwaitable_Await``, per previous implementations of PyAwaitable.

- ``aw`` is the ``AwaitableObject*``.
- ``coro`` is the coroutine (or again, any object supporting ``__await__``).
- ``cb`` is the callback that will be run with the result of ``coro``. This may be ``NULL``, in which case the result will be discarded.
- ``err`` is a callback in the event that an exception occurs during the execution of ``coro``. This may be ``NULL``, in which case the error is simply raised.

`awaitable_await` may return `0`, indicating a success, or `-1`.
- `aw` is the `PyAwaitableObject*`.
- `coro` is the coroutine (or again, any object supporting `__await__`).
- `cb` is the callback that will be run with the result of `coro`. This may be `NULL`, in which case the result will be discarded.
- `err` is a callback in the event that an exception occurs during the execution of `coro`. This may be `NULL`, in which case the error is simply raised.

`pyawaitable_await` may return `0`, indicating a success, or `-1`.

!!! note

The awaitable is guaranteed to yield (or ``await``) each coroutine in the order they were added to the awaitable. For example, if ``foo`` was added, then ``bar``, then ``baz``, first ``foo`` would be awaited (with its respective callbacks), then ``bar``, and finally ``baz``.

The `coro` parameter is not a *function* defined with `async def`, but instead an object supporting `__await__`. In the case of an `async def`, that would be a coroutine. In the example below, you would pass `bar` to `awaitable_await`, **not** `foo`:
The `coro` parameter is not a _function_ defined with `async def`, but instead an object supporting `__await__`. In the case of an `async def`, that would be a coroutine. In the example below, you would pass `bar` to `pyawaitable_await`, **not** `foo`:

```py
async def foo():
Expand All @@ -45,7 +44,11 @@ async def foo():
bar = foo()
```

`awaitable_await` does *not* check that the object supports the await protocol, but instead stores the object, and then checks it once the `AwaitableObject*` begins yielding it. This behavior prevents an additional lookup, and also allows you to pass another `AwaitableObject*` to `awaitable_await`, making it possible to chain `AwaitableObject*`'s. Note that even after the object is finished awaiting, the `AwaitableObject*` will still hold a reference to it (*i.e.*, it will not be deallocated until the `AwaitableObject*` gets deallocated).
`pyawaitable_await` does _not_ check that the object supports the await protocol, but instead stores the object, and then checks it once the `PyAwaitableObject*` begins yielding it.

This behavior prevents an additional lookup, and also allows you to pass another `PyAwaitableObject*` to `pyawaitable_await`, making it possible to chain `PyAwaitableObject*`'s.

Note that even after the object is finished awaiting, the `PyAwaitableObject*` will still hold a reference to it (_i.e._, it will not be deallocated until the `PyAwaitableObject*` gets deallocated).

!!! danger

Expand All @@ -55,12 +58,12 @@ bar = foo()
static PyObject *
spam(PyObject *self, PyObject *args)
{
PyObject *awaitable = awaitable_new();
PyObject *awaitable = pyawaitable_new();
if (awaitable == NULL)
return NULL;

// DO NOT DO THIS
if (awaitable_await(awaitable, awaitable, NULL, NULL) < 0)
if (pyawaitable_await(awaitable, awaitable, NULL, NULL) < 0)
{
Py_DECREF(awaitable);
return NULL;
Expand All @@ -70,7 +73,6 @@ bar = foo()
}
```


Here's an example of awaiting a coroutine from C:

```c
Expand All @@ -79,47 +81,73 @@ spam(PyObject *self, PyObject *args)
{
PyObject *foo;
// In this example, this is a coroutines, not an asynchronous function

if (!PyArg_ParseTuple(args, "O", &foo))
return NULL;

PyObject *awaitable = awaitable_new();
PyObject *awaitable = pyawaitable_new();

if (awaitable == NULL)
return NULL;

if (awaitable_await(awaitable, foo, NULL, NULL) < 0)
if (pyawaitable_await(awaitable, foo, NULL, NULL) < 0)
{
Py_DECREF(awaitable);
return NULL;
}

return awaitable;
}
```
This would be equivalent to `await foo` from Python.
## Return Values
Alternatively, you can use `pyawaitable_await_function` (`PyAwaitable_AwaitFunction` with the Python API prefixes), which behaves similarly to `PyObject_CallFunction`, in the sense that arguments are generated from a format string.
You can set a return value (the thing that `await c_func()` will evaluate to) via `awaitable_set_result` (`PyAwaitable_SetResult` in the Python prefixes). By default, the return value is `None`.
Note that unlike, `pyawaitable_await`, `pyawaitable_await_function` takes a *callable* object, instead of a coroutine. For example:
!!! warning
```c
static PyObject *
spam(PyObject *self, PyObject *func) // METH_O
{
PyObject *awaitable = pyawaitable_new();
`awaitable_set_result` can *only* be called from a callback. Otherwise, a `TypeError` is raised.
if (awaitable == NULL)
return NULL;
if (pyawaitable_await_function(awaitable, func, "s", NULL, NULL, "hello, world!") < 0)
{
Py_DECREF(awaitable);
return NULL;
}
return awaitable;
}
```

This would be equivalent to the following Python code:

```py
async def func(data: str) -> Any:
...

await func("hello, world!")
```

## Return Values

You can set a return value (the thing that `await c_func()` will evaluate to) via `pyawaitable_set_result` (`PyAwaitable_SetResult` in the Python prefixes). By default, the return value is `None`.

For example:

```c
static int
callback(PyObject *awaitable, PyObject *result)
{
if (awaitable_set_result(awaitable, result) < 0)
if (pyawaitable_set_result(awaitable, Py_True) < 0)
return -1;

// Do something with the result...
return 0;
}
```


Loading

0 comments on commit 21240aa

Please sign in to comment.