Skip to content

Commit 2605934

Browse files
Add integer values. (#23)
* Add integer value functions. * Add proper memory tracking to tests. * Add tests for integer values. * Fix unit test. * Switch to Py_ssize_t * Fix leak in backport of PyErr_SetRaisedException * Add back cast. * Switch to longs. * Bump version to 1.1.0 * Switch to Py_CLEAR * Refactor error return condition. * Fix variable name in refactor. * Explicitly use c_long in unit test. * Fix use of c_ssize_t to c_long * Fix reference leaks in error callbacks. * Remove unneeded cleanup test. * Don't hold strong reference to the gen wrapper. * Nevermind. * Fix double reference count. * Remove memray files. * Update changelog with previous fix. * Apparently that broke everything. * Remove changelog entry. * Revert debug change. * Some small reference fixes. * Use preallocated callbacks for faster awaits. * Fix reference leak with coroutine send(). * Update security policy. * Add test for asyncio.gather() * Update changelog. * Add -W error to Memray tests. * Remove -x flag from tests. * Remove tests for gathering. * Fix missing reference increase in backport of PyErr_SetRaisedException
1 parent ff21d50 commit 2605934

File tree

17 files changed

+315
-210
lines changed

17 files changed

+315
-210
lines changed

.github/workflows/memory_check.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ env:
1515

1616
jobs:
1717
run:
18-
name: Valgrind on Ubuntu
18+
name: Check for memory leaks and errors
1919
runs-on: ubuntu-latest
2020

2121
steps:
@@ -28,7 +28,7 @@ jobs:
2828

2929
- name: Install Pytest
3030
run: |
31-
pip install pytest pytest-asyncio typing_extensions
31+
pip install pytest pytest-asyncio pytest-memray typing_extensions
3232
shell: bash
3333

3434
- name: Build PyAwaitable
@@ -39,6 +39,9 @@ jobs:
3939

4040
- name: Install Valgrind
4141
run: sudo apt-get update && sudo apt-get -y install valgrind
42+
43+
- name: Run tests with Memray tracking
44+
run: pytest --enable-leak-tracking -W error
4245

4346
- name: Run tests with Valgrind
4447
run: valgrind --suppressions=valgrind-python.supp --error-exitcode=1 pytest -x

.github/workflows/tests.yml

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
strategy:
2525
fail-fast: false
2626
matrix:
27-
os: [ubuntu-latest, windows-latest, macos-12]
27+
os: [ubuntu-latest, windows-latest, macos-latest]
2828
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
2929

3030
steps:
@@ -36,13 +36,7 @@ jobs:
3636
python-version: ${{ matrix.python-version }}
3737

3838
- 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
45-
shell: bash
39+
run: pip install pytest pytest-asyncio typing_extensions
4640

4741
- name: Build PyAwaitable
4842
run: pip install .
@@ -51,10 +45,4 @@ jobs:
5145
run: pip install setuptools wheel && pip install ./tests/extension/ --no-build-isolation
5246

5347
- 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
60-
shell: bash
48+
run: pytest -W error

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10-
- Fix coroutine iterator reference leak.
11-
- Fix early exit of `pyawaitable_unpack_arb` if a `NULL` value was saved.
10+
- Changed error message when attempting to await a non-awaitable object (*i.e.*, it has no `__await__`).
11+
- Fixed coroutine iterator reference leak.
12+
- Fixed reference leak in error callbacks.
13+
- Fixed early exit of `pyawaitable_unpack_arb` if a `NULL` value was saved.
14+
- Added integer value saving and unpacking (`pyawaitable_save_int` and `pyawaitable_unpack_int`).
15+
- Callbacks are now preallocated for better performance.
16+
- Fixed reference leak in the coroutine `send()` method.
1217

1318
## [1.0.0] - 2024-06-24
1419

SECURITY.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44

55
Breaking API changes are made *only* between major versions. Deprecations may be made in between minor versions, but functions will not be removed until the next major version.
66

7-
| Version | Supported |
8-
| ------- | ------------------ |
9-
| 1.0.x | :white_check_mark: |
10-
117
## Reporting a Vulnerability
128

139
Depending on the severity of the vulnerability, you can make an issue on the [issue tracker](https://github.com/ZeroIntensity/pyawaitable/issues), or send an email explaining the vulnerability to <zintensitydev@gmail.com>

include/pyawaitable/awaitableobject.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct _PyAwaitableObject
2222
PyObject_HEAD
2323

2424
// Callbacks
25-
pyawaitable_callback *aw_callbacks[CALLBACK_ARRAY_SIZE];
25+
pyawaitable_callback aw_callbacks[CALLBACK_ARRAY_SIZE];
2626
Py_ssize_t aw_callback_index;
2727

2828
// Stored Values
@@ -33,6 +33,10 @@ struct _PyAwaitableObject
3333
void *aw_arb_values[VALUE_ARRAY_SIZE];
3434
Py_ssize_t aw_arb_values_index;
3535

36+
// Integer Values
37+
long aw_int_values[VALUE_ARRAY_SIZE];
38+
Py_ssize_t aw_int_values_index;
39+
3640
// Awaitable State
3741
Py_ssize_t aw_state;
3842
bool aw_done;

include/pyawaitable/values.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#ifndef PYAWAITABLE_VALUES_H
22
#define PYAWAITABLE_VALUES_H
33

4-
#include <Python.h> // PyObject
4+
#include <Python.h> // PyObject, Py_ssize_t
55

66
PyObject *pyawaitable_new_impl(void);
77

@@ -13,4 +13,8 @@ int pyawaitable_save_impl(PyObject *awaitable, Py_ssize_t nargs, ...);
1313

1414
int pyawaitable_unpack_impl(PyObject *awaitable, ...);
1515

16+
int pyawaitable_save_int_impl(PyObject *awaitable, Py_ssize_t nargs, ...);
17+
18+
int pyawaitable_unpack_int_impl(PyObject *awaitable, ...);
19+
1620
#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.0.1",
8+
version = "1.1.0",
99
ext_modules=[
1010
Extension(
1111
"_pyawaitable",

src/_pyawaitable/awaitable.c

Lines changed: 25 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,18 @@ PyObject *
3838
awaitable_next(PyObject *self)
3939
{
4040
PyAwaitableObject *aw = (PyAwaitableObject *)self;
41-
aw->aw_awaited = true;
42-
43-
if (aw->aw_done)
41+
if (aw->aw_awaited)
4442
{
4543
PyErr_SetString(
4644
PyExc_RuntimeError,
4745
"pyawaitable: cannot reuse awaitable"
4846
);
4947
return NULL;
5048
}
51-
49+
aw->aw_awaited = true;
5250
PyObject *gen = genwrapper_new(aw);
53-
54-
if (gen == NULL)
55-
{
56-
return NULL;
57-
}
58-
59-
aw->aw_gen = gen;
60-
return Py_NewRef(gen);
51+
aw->aw_gen = Py_XNewRef(gen);
52+
return gen;
6153
}
6254

6355
static void
@@ -76,13 +68,22 @@ awaitable_dealloc(PyObject *self)
7668

7769
for (int i = 0; i < CALLBACK_ARRAY_SIZE; ++i)
7870
{
79-
pyawaitable_callback *cb = aw->aw_callbacks[i];
71+
pyawaitable_callback *cb = &aw->aw_callbacks[i];
8072
if (cb == NULL)
8173
break;
8274

83-
if (!cb->done)
84-
Py_DECREF(cb->coro);
85-
PyMem_Free(cb);
75+
if (cb->done)
76+
{
77+
if (cb->coro != NULL)
78+
{
79+
PyErr_SetString(
80+
PyExc_SystemError,
81+
"sanity check: coro was not cleared"
82+
);
83+
PyErr_WriteUnraisable(self);
84+
}
85+
} else
86+
Py_XDECREF(cb->coro);
8687
}
8788

8889
if (!aw->aw_done && aw->aw_used)
@@ -106,23 +107,20 @@ void
106107
pyawaitable_cancel_impl(PyObject *aw)
107108
{
108109
assert(aw != NULL);
109-
Py_INCREF(aw);
110-
111110
PyAwaitableObject *a = (PyAwaitableObject *) aw;
112111

113112
for (int i = 0; i < CALLBACK_ARRAY_SIZE; ++i)
114113
{
115-
pyawaitable_callback *cb = a->aw_callbacks[i];
114+
pyawaitable_callback *cb = &a->aw_callbacks[i];
116115
if (!cb)
117116
break;
118117

119-
if (!cb->done)
120-
Py_DECREF(cb->coro);
121-
122-
a->aw_callbacks[i] = NULL;
118+
// Reset the callback
119+
Py_CLEAR(cb->coro);
120+
cb->done = false;
121+
cb->callback = NULL;
122+
cb->err_callback = NULL;
123123
}
124-
125-
Py_DECREF(aw);
126124
}
127125

128126
int
@@ -133,10 +131,6 @@ pyawaitable_await_impl(
133131
awaitcallback_err err
134132
)
135133
{
136-
assert(aw != NULL);
137-
assert(coro != NULL);
138-
Py_INCREF(coro);
139-
Py_INCREF(aw);
140134
PyAwaitableObject *a = (PyAwaitableObject *) aw;
141135
if (a->aw_callback_index == CALLBACK_ARRAY_SIZE)
142136
{
@@ -147,31 +141,18 @@ pyawaitable_await_impl(
147141
return -1;
148142
}
149143

150-
pyawaitable_callback *aw_c = PyMem_Malloc(sizeof(pyawaitable_callback));
151-
if (aw_c == NULL)
152-
{
153-
Py_DECREF(aw);
154-
Py_DECREF(coro);
155-
PyErr_NoMemory();
156-
return -1;
157-
}
158-
159-
aw_c->coro = coro; // Steal our own reference
144+
pyawaitable_callback *aw_c = &a->aw_callbacks[a->aw_callback_index++];
145+
aw_c->coro = Py_NewRef(coro);
160146
aw_c->callback = cb;
161147
aw_c->err_callback = err;
162148
aw_c->done = false;
163-
a->aw_callbacks[a->aw_callback_index++] = aw_c;
164-
Py_DECREF(aw);
165149

166150
return 0;
167151
}
168152

169153
int
170154
pyawaitable_set_result_impl(PyObject *awaitable, PyObject *result)
171155
{
172-
assert(awaitable != NULL);
173-
assert(result != NULL);
174-
175156
PyAwaitableObject *aw = (PyAwaitableObject *) awaitable;
176157
aw->aw_result = Py_NewRef(result);
177158
return 0;

src/_pyawaitable/backport.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ PyErr_GetRaisedException(void)
4141
void
4242
PyErr_SetRaisedException(PyObject *err)
4343
{
44+
// NOTE: We need to incref the type object here, even though
45+
// this function steals a reference to err.
4446
PyErr_Restore(Py_NewRef((PyObject *) Py_TYPE(err)), err, NULL);
4547
}
4648

src/_pyawaitable/coro.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ awaitable_send_with_arg(PyObject *self, PyObject *value)
1414
if (gen == NULL)
1515
return NULL;
1616

17+
Py_DECREF(gen);
1718
Py_RETURN_NONE;
1819
}
1920

@@ -82,7 +83,7 @@ awaitable_throw(PyObject *self, PyObject *args)
8283
if ((aw->aw_gen != NULL) && (aw->aw_state != 0))
8384
{
8485
GenWrapperObject *gw = (GenWrapperObject *)aw->aw_gen;
85-
pyawaitable_callback *cb = aw->aw_callbacks[aw->aw_state - 1];
86+
pyawaitable_callback *cb = &aw->aw_callbacks[aw->aw_state - 1];
8687
if (cb == NULL)
8788
return NULL;
8889

@@ -101,10 +102,11 @@ awaitable_am_send(PyObject *self, PyObject *arg, PyObject **presult)
101102
PyObject *send_res = awaitable_send_with_arg(self, arg);
102103
if (send_res == NULL)
103104
{
104-
PyObject *occurred = PyErr_Occurred();
105-
if (PyErr_GivenExceptionMatches(occurred, PyExc_StopIteration))
105+
if (PyErr_ExceptionMatches(PyExc_StopIteration))
106106
{
107+
PyObject *occurred = PyErr_GetRaisedException();
107108
PyObject *item = PyObject_GetAttrString(occurred, "value");
109+
Py_DECREF(occurred);
108110

109111
if (item == NULL)
110112
{

0 commit comments

Comments
 (0)