Skip to content

Commit 69737b8

Browse files
Add pyawaitable_set* and pyawaitable_get* functions (#28)
Needed for [view.py](https://github.com/ZeroIntensity/view.py) to switch to the PyPI copy instead of a vendor. `set` functions are generally used for mutating state between calls, while `get` functions are there because it's kind of weird to have `set` without `get`.
1 parent 3d1db73 commit 69737b8

File tree

13 files changed

+466
-45
lines changed

13 files changed

+466
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ __pycache__/
77
test/
88
dist/
99
pyawaitable-vendor/
10+
*.so
1011

1112
# LSP
1213
compile_flags.txt

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.0] - 2024-08-06
9+
10+
- Added getting and setting of value storage.
11+
812
## [1.1.0] - 2024-08-03
913

1014
- Changed error message when attempting to await a non-awaitable object (*i.e.*, it has no `__await__`).

docs/installation.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,18 +172,24 @@ The complete mapping of names to their Python API counterparts are:
172172
| ---------------------------- | ----------------------------- |
173173
| `pyawaitable_new` | `PyAwaitable_New` |
174174
| `pyawaitable_await` | `PyAwaitable_AddAwait` |
175+
| `pyawaitable_await_function` | `PyAwaitable_AwaitFunction` |
175176
| `pyawaitable_cancel` | `PyAwaitable_Cancel` |
176177
| `pyawaitable_set_result` | `PyAwaitable_SetResult` |
177178
| `pyawaitable_save` | `PyAwaitable_SaveValues` |
178179
| `pyawaitable_save_arb` | `PyAwaitable_SaveArbValues` |
180+
| `pyawaitable_save_int` | `PyAwaitable_SaveIntValues` |
179181
| `pyawaitable_unpack` | `PyAwaitable_UnpackValues` |
180182
| `pyawaitable_unpack_arb` | `PyAwaitable_UnpackArbValues` |
183+
| `pyawaitable_unpack_int` | `PyAwaitable_UnpackIntValues` |
184+
| `pyawaitable_set` | `PyAwaitable_SetValue` |
185+
| `pyawaitable_set_arb` | `PyAwaitable_SetArbValue` |
186+
| `pyawaitable_set_int` | `PyAwaitable_SetIntValue` |
187+
| `pyawaitable_get` | `PyAwaitable_GetValue` |
188+
| `pyawaitable_get_arb` | `PyAwaitable_GetArbValue` |
189+
| `pyawaitable_get_int` | `PyAwaitable_GetIntValue` |
181190
| `pyawaitable_init` | `PyAwaitable_Init` |
182191
| `pyawaitable_abi` | `PyAwaitable_ABI` |
183192
| `PyAwaitableType` | `PyAwaitable_Type` |
184-
| `pyawaitable_await_function` | `PyAwaitable_AwaitFunction` |
185-
| `pyawaitable_save_int` | `PyAwaitable_SaveIntValues` |
186-
| `pyawaitable_unpack_int` | `PyAwaitable_UnpackIntValues` |
187193
188194
## Vendored Copies
189195

docs/storage.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,110 @@ test(PyObject *self, PyObject *coro) // We're back to METH_O!
304304
}
305305
```
306306
307+
## Getting and Setting
308+
309+
In some cases, you might want to overwrite existing saved values (e.g. in a recursive callback). For example, we could have some state information, and want to change that between calls.
310+
311+
Let's start in a callback that uses itself recursively:
312+
313+
```c
314+
static int
315+
callback(PyObject *aw, PyObject *result)
316+
{
317+
if (!PyCoro_CheckExact(result)) {
318+
return 0;
319+
}
320+
321+
if (pyawaitable_await(aw, result, callback, NULL) < 0)
322+
return -1;
323+
324+
return 0;
325+
}
326+
327+
static PyObject *
328+
test(PyObject *self, PyObject *coro) // We're back to METH_O!
329+
{
330+
PyObject *aw = pyawaitable_new();
331+
332+
if (pyawaitable_await(aw, coro, callback, NULL) < 0)
333+
{
334+
Py_DECREF(aw);
335+
return NULL;
336+
}
337+
338+
return aw;
339+
}
340+
```
341+
342+
Theoretically, `callback` could be called an infinite number of times for the same PyAwaitable object, so we don't know what the state of the call is!
343+
344+
OK, let's start by saving an integer value:
345+
346+
```c
347+
static PyObject *
348+
test(PyObject *self, PyObject *coro) // We're back to METH_O!
349+
{
350+
PyObject *aw = pyawaitable_new();
351+
352+
if (pyawaitable_await(aw, coro, callback, NULL) < 0)
353+
{
354+
Py_DECREF(aw);
355+
return NULL;
356+
}
357+
358+
if (pyawaitable_save_int(aw, 1, 1) < 0)
359+
{
360+
Py_DECREF(aw);
361+
return NULL;
362+
}
363+
364+
return aw;
365+
}
366+
```
367+
368+
But, so far, we've only learned about _appending_ to the values array, not mutating in place. So, how do we do that? Each of the value arrays have their own get and set functions.
369+
370+
In this case, we want `pyawaitable_set_int`:
371+
372+
```c
373+
static int
374+
callback(PyObject *aw, PyObject *result)
375+
{
376+
long value = pyawaitable_get_int(aw, 0);
377+
if (value == -1 && PyErr_Occurred())
378+
{
379+
return -1;
380+
}
381+
382+
if (pyawaitable_set_int(aw, 0, value + 1) < 0)
383+
{
384+
return -1;
385+
}
386+
387+
if (!PyCoro_CheckExact(result))
388+
{
389+
return 0;
390+
}
391+
392+
if (pyawaitable_await(aw, result, callback, NULL) < 0)
393+
return -1;
394+
395+
return 0;
396+
}
397+
```
398+
399+
Great! We increment our state for each call!
400+
401+
!!! warning "Prefer `unpack` over `get`!"
402+
403+
In the above, `pyawaitable_get_int` was used for teaching purposes. `pyawaitable_get_*` functions exist for very niche cases, they shouldn't be preferred over `pyawaitable_unpack_*`!
404+
307405
## Next Steps
308406

309407
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.
310408

311409
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.
410+
411+
```
412+
413+
```

include/pyawaitable/values.h

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,44 @@
33

44
#include <Python.h> // PyObject, Py_ssize_t
55

6-
PyObject *pyawaitable_new_impl(void);
7-
8-
int pyawaitable_save_arb_impl(PyObject *awaitable, Py_ssize_t nargs, ...);
9-
10-
int pyawaitable_unpack_arb_impl(PyObject *awaitable, ...);
11-
12-
int pyawaitable_save_impl(PyObject *awaitable, Py_ssize_t nargs, ...);
13-
14-
int pyawaitable_unpack_impl(PyObject *awaitable, ...);
15-
16-
int pyawaitable_save_int_impl(PyObject *awaitable, Py_ssize_t nargs, ...);
17-
18-
int pyawaitable_unpack_int_impl(PyObject *awaitable, ...);
6+
#define SAVE(name) int name(PyObject * awaitable, Py_ssize_t nargs, ...)
7+
#define UNPACK(name) int name(PyObject * awaitable, ...)
8+
#define SET(name, tp) \
9+
int name( \
10+
PyObject * awaitable, \
11+
Py_ssize_t index, \
12+
tp new_value \
13+
)
14+
#define GET(name, tp) \
15+
tp name( \
16+
PyObject * awaitable, \
17+
Py_ssize_t index \
18+
)
19+
20+
// Normal values
21+
22+
SAVE(pyawaitable_save_impl);
23+
UNPACK(pyawaitable_unpack_impl);
24+
SET(pyawaitable_set_impl, PyObject *);
25+
GET(pyawaitable_get_impl, PyObject *);
26+
27+
// Arbitrary values
28+
29+
SAVE(pyawaitable_save_arb_impl);
30+
UNPACK(pyawaitable_unpack_arb_impl);
31+
SET(pyawaitable_set_arb_impl, void *);
32+
GET(pyawaitable_get_arb_impl, void *);
33+
34+
// Integer values
35+
36+
SAVE(pyawaitable_save_int_impl);
37+
UNPACK(pyawaitable_unpack_int_impl);
38+
SET(pyawaitable_set_int_impl, long);
39+
GET(pyawaitable_get_int_impl, long);
40+
41+
#undef SAVE
42+
#undef UNPACK
43+
#undef GET
44+
#undef SET
1945

2046
#endif

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
setup(
66
name="pyawaitable",
77
license="MIT",
8-
version = "1.1.0",
8+
version = "1.2.0",
99
ext_modules=[
1010
Extension(
1111
"_pyawaitable",

src/_pyawaitable/mod.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,13 @@ static PyAwaitableABI _abi_interface =
4747
&_PyAwaitableType,
4848
pyawaitable_await_function_impl,
4949
pyawaitable_save_int_impl,
50-
pyawaitable_unpack_int_impl
50+
pyawaitable_unpack_int_impl,
51+
pyawaitable_set_impl,
52+
pyawaitable_set_arb_impl,
53+
pyawaitable_set_int_impl,
54+
pyawaitable_get_impl,
55+
pyawaitable_get_arb_impl,
56+
pyawaitable_get_int_impl
5157
};
5258

5359
static void

src/_pyawaitable/values.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@
5858
return 0; \
5959
} while (0)
6060

61+
#define INDEX_HEAD(arr, idx, ret) \
62+
PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; \
63+
if ((index >= idx) || (index < 0)) { \
64+
PyErr_Format( \
65+
PyExc_IndexError, \
66+
"pyawaitable: index %ld out of range for %ld stored values", \
67+
index, \
68+
idx \
69+
); \
70+
return ret; \
71+
}
72+
73+
6174
#define NOTHING
6275

6376
/* Normal Values */
@@ -74,6 +87,28 @@ pyawaitable_save_impl(PyObject *awaitable, Py_ssize_t nargs, ...)
7487
SAVE(aw->aw_values, aw->aw_values_index, PyObject *, "values", Py_NewRef);
7588
}
7689

90+
int
91+
pyawaitable_set_impl(
92+
PyObject *awaitable,
93+
Py_ssize_t index,
94+
PyObject *new_value
95+
)
96+
{
97+
INDEX_HEAD(aw->aw_values, aw->aw_values_index, -1);
98+
Py_SETREF(aw->aw_values[index], Py_NewRef(new_value));
99+
return 0;
100+
}
101+
102+
PyObject *
103+
pyawaitable_get_impl(
104+
PyObject *awaitable,
105+
Py_ssize_t index
106+
)
107+
{
108+
INDEX_HEAD(aw->aw_values, aw->aw_values_index, NULL);
109+
return aw->aw_values[index];
110+
}
111+
77112
/* Arbitrary Values */
78113

79114
int
@@ -99,6 +134,28 @@ pyawaitable_save_arb_impl(PyObject *awaitable, Py_ssize_t nargs, ...)
99134
);
100135
}
101136

137+
int
138+
pyawaitable_set_arb_impl(
139+
PyObject *awaitable,
140+
Py_ssize_t index,
141+
void *new_value
142+
)
143+
{
144+
INDEX_HEAD(aw->aw_arb_values, aw->aw_arb_values_index, -1);
145+
aw->aw_arb_values[index] = new_value;
146+
return 0;
147+
}
148+
149+
void *
150+
pyawaitable_get_arb_impl(
151+
PyObject *awaitable,
152+
Py_ssize_t index
153+
)
154+
{
155+
INDEX_HEAD(aw->aw_arb_values, aw->aw_arb_values_index, NULL);
156+
return aw->aw_arb_values[index];
157+
}
158+
102159
/* Integer Values */
103160

104161
int
@@ -123,3 +180,25 @@ pyawaitable_save_int_impl(PyObject *awaitable, Py_ssize_t nargs, ...)
123180
NOTHING
124181
);
125182
}
183+
184+
int
185+
pyawaitable_set_int_impl(
186+
PyObject *awaitable,
187+
Py_ssize_t index,
188+
long new_value
189+
)
190+
{
191+
INDEX_HEAD(aw->aw_int_values, aw->aw_int_values_index, -1);
192+
aw->aw_int_values[index] = new_value;
193+
return 0;
194+
}
195+
196+
long
197+
pyawaitable_get_int_impl(
198+
PyObject *awaitable,
199+
Py_ssize_t index
200+
)
201+
{
202+
INDEX_HEAD(aw->aw_int_values, aw->aw_int_values_index, -1);
203+
return aw->aw_int_values[index];
204+
}

src/pyawaitable/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
from typing import Type
1111

1212
__all__ = "PyAwaitable", "include", "abi"
13-
__version__ = "1.0.1"
13+
__version__ = "1.2.0"
1414

1515
PyAwaitable: Type = _PyAwaitableType
1616

17+
1718
def include() -> str:
1819
"""
1920
Get the directory containing the `pyawaitable.h` file.
2021
"""
2122
import os
2223

2324
return os.path.dirname(__file__)
24-

src/pyawaitable/abi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from _pyawaitable import abi_v1
22

3-
__all__ = "v1",
3+
__all__ = ("v1",)
44

55
v1 = abi_v1

0 commit comments

Comments
 (0)