Skip to content

Commit 21240aa

Browse files
Merge pull request #17 from ZeroIntensity/code-cleanup
Release candidate 1
2 parents 31cafee + aabdede commit 21240aa

38 files changed

+1767
-741
lines changed

.github/workflows/build.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ on:
44
push:
55
tags:
66
- v*
7-
pull_request:
8-
branches:
9-
- master
107

118
concurrency:
129
group: build-${{ github.head_ref }}

.github/workflows/memory_check.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Memory Check
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
branches:
9+
- master
10+
11+
env:
12+
PYTHONUNBUFFERED: "1"
13+
FORCE_COLOR: "1"
14+
PYTHONIOENCODING: "utf8"
15+
16+
jobs:
17+
run:
18+
name: Valgrind on Ubuntu
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- uses: actions/checkout@v2
23+
24+
- name: Set up Python 3.12
25+
uses: actions/setup-python@v2
26+
with:
27+
python-version: 3.12
28+
29+
- name: Install Pytest
30+
run: |
31+
pip install pytest pytest-asyncio typing_extensions
32+
shell: bash
33+
34+
- name: Build PyAwaitable
35+
run: pip install .
36+
37+
- name: Build PyAwaitable Test Package
38+
run: pip install setuptools wheel && pip install tests/extension/ --no-build-isolation
39+
40+
- name: Install Valgrind
41+
run: sudo apt-get update && sudo apt-get -y install valgrind
42+
43+
- name: Run tests with Valgrind
44+
run: valgrind --suppressions=valgrind-python.supp --error-exitcode=1 pytest -x

.github/workflows/tests.yml

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,26 @@ jobs:
3535
with:
3636
python-version: ${{ matrix.python-version }}
3737

38-
- name: Install PyTest
39-
run: |
40-
if [ "$RUNNER_OS" == "Windows" ]; then
41-
pip install pytest pytest-asyncio typing_extensions
42-
else
43-
pip install pytest pytest-asyncio pytest-memray typing_extensions
44-
fi
38+
- name: Install Pytest
39+
run: |
40+
if [ "$RUNNER_OS" == "Windows" ]; then
41+
pip install pytest pytest-asyncio typing_extensions
42+
else
43+
pip install pytest pytest-asyncio pytest-memray typing_extensions
44+
fi
4545
shell: bash
46-
46+
4747
- name: Build PyAwaitable
4848
run: pip install .
4949

50-
- name: Run tests
51-
run: |
52-
if [ "$RUNNER_OS" == "Windows" ]; then
53-
pytest
54-
else
55-
pytest --memray
56-
fi
50+
- name: Build PyAwaitable Test Package
51+
run: pip install setuptools wheel && pip install ./tests/extension/ --no-build-isolation
52+
53+
- name: Run tests
54+
run: |
55+
if [ "$RUNNER_OS" == "Windows" ]; then
56+
pytest -W error
57+
else
58+
python3 -m pytest -W error --memray
59+
fi
5760
shell: bash

.gitignore

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
# Python
12
__pycache__/
23
.venv/
3-
compile_flags.txt
4+
45
*.egg-info
5-
build/
6-
test.py
76
test/
87
dist/
8+
9+
# LSP
10+
compile_flags.txt
11+
build/
912
.vscode/
1013
.vs/
11-
*.user
1214
*.sln
15+
*.user
1316
*.vcxproj*
17+
18+
# Misc
19+
test.py
20+
vgcore*

CONTRIBUTING.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Contributing to PyAwaitable
2+
3+
Lucky for you, the internals of PyAwaitable are extremely well documented, since it was originally designed to be part of the CPython API.
4+
5+
Before you get started, it's a good idea to read the following discussions, or at least skim through them:
6+
7+
- [Adding a C API for coroutines/awaitables](https://discuss.python.org/t/adding-a-c-api-for-coroutines-awaitables/22786)
8+
- [C API for asynchronous functions](https://discuss.python.org/t/c-api-for-asynchronous-functions/42842)
9+
- [Revisiting a C API for asynchronous functions](https://discuss.python.org/t/revisiting-a-c-api-for-asynchronous-functions/50792)
10+
11+
Then, for all the details of the underlying implementation, read the [scrapped PEP](https://gist.github.com/ZeroIntensity/8d32e94b243529c7e1c27349e972d926).
12+
13+
## Development Workflow
14+
15+
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!
16+
17+
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.
18+
19+
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:
20+
21+
- A link to the issue that it's implementing.
22+
- New and passing tests.
23+
- Updated docs and changelog.
24+
- Code following the style guide, mentioned below.
25+
26+
## Style Guide
27+
28+
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.
29+
30+
However, don't bother trying to format things yourself! PyAwaitable provides an [uncrustify](https://github.com/uncrustify/uncrustify) configuration file for you.
31+
32+
## Project Setup
33+
34+
If you haven't already, clone the project.
35+
36+
```
37+
$ git clone https://github.com/ZeroIntensity/pyawaitable
38+
$ cd pyawaitable
39+
```
40+
41+
To build PyAwaitable locally, simple run `pip`:
42+
43+
```
44+
$ pip install .
45+
```
46+
47+
It's highly recommended to do this inside of a [virtual environment](https://docs.python.org/3/library/venv.html).
48+
49+
## Running Tests
50+
51+
PyAwaitable uses three libraries for unit testing:
52+
53+
- [pytest](https://docs.pytest.org/en/8.2.x/), as the general testing framework.
54+
- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/en/latest/), for asynchronous tests.
55+
- [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.
56+
57+
Installation is trivial:
58+
59+
```
60+
$ pip install pytest pytest-asyncio pytest-memray
61+
```
62+
63+
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:
64+
65+
```
66+
$ pip install setuptools wheel && pip install ./test/extension/ --no-build-isolation
67+
```

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include src/pyawaitable/*.h

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@ build-backend = "setuptools.build_meta"
2727
## Example
2828

2929
```c
30-
#include <awaitable.h>
30+
#include <pyawaitable.h>
3131

3232
// Assuming that this is using METH_O
3333
static PyObject *
3434
hello(PyObject *self, PyObject *coro) {
3535
// Make our awaitable object
36-
PyObject *awaitable = awaitable_new();
36+
PyObject *awaitable = pyawaitable_new();
3737

3838
if (!awaitable)
3939
return NULL;
4040

4141
// Mark the coroutine for being awaited
42-
if (awaitable_await(awaitable, coro, NULL, NULL) < 0) {
42+
if (pyawaitable_await(awaitable, coro, NULL, NULL) < 0) {
4343
Py_DECREF(awaitable);
4444
return NULL;
4545
}
@@ -55,7 +55,7 @@ async def coro():
5555
await asyncio.sleep(1)
5656
print("awaited from C!")
5757
58-
# Use our C function to await coro
58+
# Use our C function to await it
5959
await hello(coro())
6060
```
6161

docs/adding_coros.md

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
---
22
hide:
3-
- navigation
3+
- navigation
44
---
55

66
# Adding Coroutines
77

88
## Basics
99

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

1212
```c
13-
// Signature of awaitable_await, for reference
13+
// Signature of pyawaitable_await, for reference
1414
int
15-
awaitable_await(
15+
pyawaitable_await(
1616
PyObject *aw,
1717
PyObject *coro,
1818
awaitcallback cb,
@@ -22,21 +22,20 @@ awaitable_await(
2222

2323
!!! warning
2424

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

27-
- ``aw`` is the ``AwaitableObject*``.
28-
- ``coro`` is the coroutine (or again, any object supporting ``__await__``).
29-
- ``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.
30-
- ``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.
31-
32-
`awaitable_await` may return `0`, indicating a success, or `-1`.
27+
- `aw` is the `PyAwaitableObject*`.
28+
- `coro` is the coroutine (or again, any object supporting `__await__`).
29+
- `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.
30+
- `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.
3331

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

3534
!!! note
3635

3736
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``.
3837

39-
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`:
38+
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`:
4039

4140
```py
4241
async def foo():
@@ -45,7 +44,11 @@ async def foo():
4544
bar = foo()
4645
```
4746

48-
`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).
47+
`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.
48+
49+
This behavior prevents an additional lookup, and also allows you to pass another `PyAwaitableObject*` to `pyawaitable_await`, making it possible to chain `PyAwaitableObject*`'s.
50+
51+
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).
4952

5053
!!! danger
5154

@@ -55,12 +58,12 @@ bar = foo()
5558
static PyObject *
5659
spam(PyObject *self, PyObject *args)
5760
{
58-
PyObject *awaitable = awaitable_new();
61+
PyObject *awaitable = pyawaitable_new();
5962
if (awaitable == NULL)
6063
return NULL;
6164

6265
// DO NOT DO THIS
63-
if (awaitable_await(awaitable, awaitable, NULL, NULL) < 0)
66+
if (pyawaitable_await(awaitable, awaitable, NULL, NULL) < 0)
6467
{
6568
Py_DECREF(awaitable);
6669
return NULL;
@@ -70,7 +73,6 @@ bar = foo()
7073
}
7174
```
7275

73-
7476
Here's an example of awaiting a coroutine from C:
7577

7678
```c
@@ -79,47 +81,73 @@ spam(PyObject *self, PyObject *args)
7981
{
8082
PyObject *foo;
8183
// In this example, this is a coroutines, not an asynchronous function
82-
84+
8385
if (!PyArg_ParseTuple(args, "O", &foo))
8486
return NULL;
8587

86-
PyObject *awaitable = awaitable_new();
88+
PyObject *awaitable = pyawaitable_new();
8789

8890
if (awaitable == NULL)
8991
return NULL;
9092

91-
if (awaitable_await(awaitable, foo, NULL, NULL) < 0)
93+
if (pyawaitable_await(awaitable, foo, NULL, NULL) < 0)
9294
{
9395
Py_DECREF(awaitable);
9496
return NULL;
9597
}
96-
98+
9799
return awaitable;
98100
}
99101
```
100102
101103
This would be equivalent to `await foo` from Python.
102104
103-
## Return Values
105+
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.
104106
105-
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`.
107+
Note that unlike, `pyawaitable_await`, `pyawaitable_await_function` takes a *callable* object, instead of a coroutine. For example:
106108
107-
!!! warning
109+
```c
110+
static PyObject *
111+
spam(PyObject *self, PyObject *func) // METH_O
112+
{
113+
PyObject *awaitable = pyawaitable_new();
108114
109-
`awaitable_set_result` can *only* be called from a callback. Otherwise, a `TypeError` is raised.
115+
if (awaitable == NULL)
116+
return NULL;
117+
118+
if (pyawaitable_await_function(awaitable, func, "s", NULL, NULL, "hello, world!") < 0)
119+
{
120+
Py_DECREF(awaitable);
121+
return NULL;
122+
}
123+
124+
return awaitable;
125+
}
126+
```
127+
128+
This would be equivalent to the following Python code:
129+
130+
```py
131+
async def func(data: str) -> Any:
132+
...
133+
134+
await func("hello, world!")
135+
```
136+
137+
## Return Values
138+
139+
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`.
110140

111141
For example:
112142

113143
```c
114144
static int
115145
callback(PyObject *awaitable, PyObject *result)
116146
{
117-
if (awaitable_set_result(awaitable, result) < 0)
147+
if (pyawaitable_set_result(awaitable, Py_True) < 0)
118148
return -1;
119149

120150
// Do something with the result...
121151
return 0;
122152
}
123153
```
124-
125-

0 commit comments

Comments
 (0)