From 2605934a91fe24e060b201c4fa2ef23150381a3a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 3 Aug 2024 21:34:42 -0400 Subject: [PATCH] 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 --- .github/workflows/memory_check.yml | 7 +- .github/workflows/tests.yml | 18 +- CHANGELOG.md | 9 +- SECURITY.md | 4 - include/pyawaitable/awaitableobject.h | 6 +- include/pyawaitable/values.h | 6 +- setup.py | 2 +- src/_pyawaitable/awaitable.c | 69 +++----- src/_pyawaitable/backport.c | 2 + src/_pyawaitable/coro.c | 8 +- src/_pyawaitable/genwrapper.c | 238 ++++++++++++++------------ src/_pyawaitable/mod.c | 4 +- src/_pyawaitable/values.c | 25 +++ src/pyawaitable/bindings.py | 5 + src/pyawaitable/pyawaitable.h | 12 +- tests/conftest.py | 42 +++++ tests/test_awaitable.py | 68 +++++--- 17 files changed, 315 insertions(+), 210 deletions(-) create mode 100644 tests/conftest.py diff --git a/.github/workflows/memory_check.yml b/.github/workflows/memory_check.yml index 1e96019..79abd01 100644 --- a/.github/workflows/memory_check.yml +++ b/.github/workflows/memory_check.yml @@ -15,7 +15,7 @@ env: jobs: run: - name: Valgrind on Ubuntu + name: Check for memory leaks and errors runs-on: ubuntu-latest steps: @@ -28,7 +28,7 @@ jobs: - name: Install Pytest run: | - pip install pytest pytest-asyncio typing_extensions + pip install pytest pytest-asyncio pytest-memray typing_extensions shell: bash - name: Build PyAwaitable @@ -39,6 +39,9 @@ jobs: - name: Install Valgrind run: sudo apt-get update && sudo apt-get -y install valgrind + + - name: Run tests with Memray tracking + run: pytest --enable-leak-tracking -W error - name: Run tests with Valgrind run: valgrind --suppressions=valgrind-python.supp --error-exitcode=1 pytest -x diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d354db..0833185 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-12] + os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: @@ -36,13 +36,7 @@ jobs: 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 - shell: bash + run: pip install pytest pytest-asyncio typing_extensions - name: Build PyAwaitable run: pip install . @@ -51,10 +45,4 @@ jobs: 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 + run: pytest -W error diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d09e75..97cef79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased -- Fix coroutine iterator reference leak. -- Fix early exit of `pyawaitable_unpack_arb` if a `NULL` value was saved. +- Changed error message when attempting to await a non-awaitable object (*i.e.*, it has no `__await__`). +- Fixed coroutine iterator reference leak. +- Fixed reference leak in error callbacks. +- Fixed early exit of `pyawaitable_unpack_arb` if a `NULL` value was saved. +- Added integer value saving and unpacking (`pyawaitable_save_int` and `pyawaitable_unpack_int`). +- Callbacks are now preallocated for better performance. +- Fixed reference leak in the coroutine `send()` method. ## [1.0.0] - 2024-06-24 diff --git a/SECURITY.md b/SECURITY.md index e043e95..1633f0a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,10 +4,6 @@ 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. -| Version | Supported | -| ------- | ------------------ | -| 1.0.x | :white_check_mark: | - ## Reporting a Vulnerability 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 diff --git a/include/pyawaitable/awaitableobject.h b/include/pyawaitable/awaitableobject.h index e730f51..c35951c 100644 --- a/include/pyawaitable/awaitableobject.h +++ b/include/pyawaitable/awaitableobject.h @@ -22,7 +22,7 @@ struct _PyAwaitableObject PyObject_HEAD // Callbacks - pyawaitable_callback *aw_callbacks[CALLBACK_ARRAY_SIZE]; + pyawaitable_callback aw_callbacks[CALLBACK_ARRAY_SIZE]; Py_ssize_t aw_callback_index; // Stored Values @@ -33,6 +33,10 @@ struct _PyAwaitableObject void *aw_arb_values[VALUE_ARRAY_SIZE]; Py_ssize_t aw_arb_values_index; + // Integer Values + long aw_int_values[VALUE_ARRAY_SIZE]; + Py_ssize_t aw_int_values_index; + // Awaitable State Py_ssize_t aw_state; bool aw_done; diff --git a/include/pyawaitable/values.h b/include/pyawaitable/values.h index 4df2d70..bdbf2bc 100644 --- a/include/pyawaitable/values.h +++ b/include/pyawaitable/values.h @@ -1,7 +1,7 @@ #ifndef PYAWAITABLE_VALUES_H #define PYAWAITABLE_VALUES_H -#include // PyObject +#include // PyObject, Py_ssize_t PyObject *pyawaitable_new_impl(void); @@ -13,4 +13,8 @@ int pyawaitable_save_impl(PyObject *awaitable, Py_ssize_t nargs, ...); int pyawaitable_unpack_impl(PyObject *awaitable, ...); +int pyawaitable_save_int_impl(PyObject *awaitable, Py_ssize_t nargs, ...); + +int pyawaitable_unpack_int_impl(PyObject *awaitable, ...); + #endif diff --git a/setup.py b/setup.py index 6086494..a3169ff 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="pyawaitable", license="MIT", - version = "1.0.1", + version = "1.1.0", ext_modules=[ Extension( "_pyawaitable", diff --git a/src/_pyawaitable/awaitable.c b/src/_pyawaitable/awaitable.c index 3acbfe0..e16e19b 100644 --- a/src/_pyawaitable/awaitable.c +++ b/src/_pyawaitable/awaitable.c @@ -38,9 +38,7 @@ PyObject * awaitable_next(PyObject *self) { PyAwaitableObject *aw = (PyAwaitableObject *)self; - aw->aw_awaited = true; - - if (aw->aw_done) + if (aw->aw_awaited) { PyErr_SetString( PyExc_RuntimeError, @@ -48,16 +46,10 @@ awaitable_next(PyObject *self) ); return NULL; } - + aw->aw_awaited = true; PyObject *gen = genwrapper_new(aw); - - if (gen == NULL) - { - return NULL; - } - - aw->aw_gen = gen; - return Py_NewRef(gen); + aw->aw_gen = Py_XNewRef(gen); + return gen; } static void @@ -76,13 +68,22 @@ awaitable_dealloc(PyObject *self) for (int i = 0; i < CALLBACK_ARRAY_SIZE; ++i) { - pyawaitable_callback *cb = aw->aw_callbacks[i]; + pyawaitable_callback *cb = &aw->aw_callbacks[i]; if (cb == NULL) break; - if (!cb->done) - Py_DECREF(cb->coro); - PyMem_Free(cb); + if (cb->done) + { + if (cb->coro != NULL) + { + PyErr_SetString( + PyExc_SystemError, + "sanity check: coro was not cleared" + ); + PyErr_WriteUnraisable(self); + } + } else + Py_XDECREF(cb->coro); } if (!aw->aw_done && aw->aw_used) @@ -106,23 +107,20 @@ void pyawaitable_cancel_impl(PyObject *aw) { assert(aw != NULL); - Py_INCREF(aw); - PyAwaitableObject *a = (PyAwaitableObject *) aw; for (int i = 0; i < CALLBACK_ARRAY_SIZE; ++i) { - pyawaitable_callback *cb = a->aw_callbacks[i]; + pyawaitable_callback *cb = &a->aw_callbacks[i]; if (!cb) break; - if (!cb->done) - Py_DECREF(cb->coro); - - a->aw_callbacks[i] = NULL; + // Reset the callback + Py_CLEAR(cb->coro); + cb->done = false; + cb->callback = NULL; + cb->err_callback = NULL; } - - Py_DECREF(aw); } int @@ -133,10 +131,6 @@ pyawaitable_await_impl( awaitcallback_err err ) { - assert(aw != NULL); - assert(coro != NULL); - Py_INCREF(coro); - Py_INCREF(aw); PyAwaitableObject *a = (PyAwaitableObject *) aw; if (a->aw_callback_index == CALLBACK_ARRAY_SIZE) { @@ -147,21 +141,11 @@ pyawaitable_await_impl( return -1; } - pyawaitable_callback *aw_c = PyMem_Malloc(sizeof(pyawaitable_callback)); - if (aw_c == NULL) - { - Py_DECREF(aw); - Py_DECREF(coro); - PyErr_NoMemory(); - return -1; - } - - aw_c->coro = coro; // Steal our own reference + pyawaitable_callback *aw_c = &a->aw_callbacks[a->aw_callback_index++]; + aw_c->coro = Py_NewRef(coro); aw_c->callback = cb; aw_c->err_callback = err; aw_c->done = false; - a->aw_callbacks[a->aw_callback_index++] = aw_c; - Py_DECREF(aw); return 0; } @@ -169,9 +153,6 @@ pyawaitable_await_impl( int pyawaitable_set_result_impl(PyObject *awaitable, PyObject *result) { - assert(awaitable != NULL); - assert(result != NULL); - PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; aw->aw_result = Py_NewRef(result); return 0; diff --git a/src/_pyawaitable/backport.c b/src/_pyawaitable/backport.c index a80fd0e..f3a1545 100644 --- a/src/_pyawaitable/backport.c +++ b/src/_pyawaitable/backport.c @@ -41,6 +41,8 @@ PyErr_GetRaisedException(void) void PyErr_SetRaisedException(PyObject *err) { + // NOTE: We need to incref the type object here, even though + // this function steals a reference to err. PyErr_Restore(Py_NewRef((PyObject *) Py_TYPE(err)), err, NULL); } diff --git a/src/_pyawaitable/coro.c b/src/_pyawaitable/coro.c index 5dbe451..9d767d0 100644 --- a/src/_pyawaitable/coro.c +++ b/src/_pyawaitable/coro.c @@ -14,6 +14,7 @@ awaitable_send_with_arg(PyObject *self, PyObject *value) if (gen == NULL) return NULL; + Py_DECREF(gen); Py_RETURN_NONE; } @@ -82,7 +83,7 @@ awaitable_throw(PyObject *self, PyObject *args) if ((aw->aw_gen != NULL) && (aw->aw_state != 0)) { GenWrapperObject *gw = (GenWrapperObject *)aw->aw_gen; - pyawaitable_callback *cb = aw->aw_callbacks[aw->aw_state - 1]; + pyawaitable_callback *cb = &aw->aw_callbacks[aw->aw_state - 1]; if (cb == NULL) return NULL; @@ -101,10 +102,11 @@ awaitable_am_send(PyObject *self, PyObject *arg, PyObject **presult) PyObject *send_res = awaitable_send_with_arg(self, arg); if (send_res == NULL) { - PyObject *occurred = PyErr_Occurred(); - if (PyErr_GivenExceptionMatches(occurred, PyExc_StopIteration)) + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyObject *occurred = PyErr_GetRaisedException(); PyObject *item = PyObject_GetAttrString(occurred, "value"); + Py_DECREF(occurred); if (item == NULL) { diff --git a/src/_pyawaitable/genwrapper.c b/src/_pyawaitable/genwrapper.c index 6b5ecef..f6eed35 100644 --- a/src/_pyawaitable/genwrapper.c +++ b/src/_pyawaitable/genwrapper.c @@ -3,7 +3,15 @@ #include #include #include - +#define DONE(cb) \ + do { cb->done = true; \ + Py_CLEAR(cb->coro); \ + Py_CLEAR(g->gw_current_await); } while (0) +#define AW_DONE() \ + do { \ + aw->aw_done = true; \ + Py_CLEAR(g->gw_aw); \ + } while (0) static PyObject * gen_new(PyTypeObject *tp, PyObject *args, PyObject *kwds) @@ -28,8 +36,22 @@ static void gen_dealloc(PyObject *self) { GenWrapperObject *g = (GenWrapperObject *) self; - Py_XDECREF(g->gw_current_await); - Py_XDECREF(g->gw_aw); + if (g->gw_current_await != NULL) + { + PyErr_SetString( + PyExc_SystemError, + "sanity check: gw_current_await was not cleared!" + ); + PyErr_WriteUnraisable(self); + } + if (g->gw_aw != NULL) + { + PyErr_SetString( + PyExc_SystemError, + "sanity check: gw_aw was not cleared!" + ); + PyErr_WriteUnraisable(self); + } Py_TYPE(self)->tp_free(self); } @@ -61,18 +83,15 @@ genwrapper_fire_err_callback( if (!cb->err_callback) { cb->done = true; - Py_DECREF(cb->coro); - Py_XDECREF(await); return -1; } - Py_INCREF(self); PyObject *err = PyErr_GetRaisedException(); + Py_INCREF(self); int e_res = cb->err_callback(self, err); - cb->done = true; - Py_DECREF(self); + cb->done = true; if (e_res < 0) { @@ -84,8 +103,6 @@ genwrapper_fire_err_callback( } else Py_DECREF(err); - Py_DECREF(cb->coro); - Py_XDECREF(await); return -1; } @@ -115,24 +132,23 @@ genwrapper_next(PyObject *self) PyExc_SystemError, "pyawaitable: object cannot handle more than 255 coroutines" ); + AW_DONE(); return NULL; } if (g->gw_current_await == NULL) { - if (aw->aw_callbacks[aw->aw_state] == NULL) + if (aw->aw_callbacks[aw->aw_state].coro == NULL) { - aw->aw_done = true; PyErr_SetObject( PyExc_StopIteration, aw->aw_result ? aw->aw_result : Py_None ); - Py_DECREF(g->gw_aw); - g->gw_aw = NULL; + AW_DONE(); return NULL; } - cb = aw->aw_callbacks[aw->aw_state++]; + cb = &aw->aw_callbacks[aw->aw_state++]; if ( Py_TYPE(cb->coro)->tp_as_async == NULL || @@ -141,9 +157,11 @@ genwrapper_next(PyObject *self) { PyErr_Format( PyExc_TypeError, - "pyawaitable: %R has no __await__", + "pyawaitable: %R is not awaitable", cb->coro ); + DONE(cb); + AW_DONE(); return NULL; } @@ -160,136 +178,134 @@ genwrapper_next(PyObject *self) ) < 0 ) { + DONE(cb); + AW_DONE(); return NULL; } + DONE(cb); return genwrapper_next(self); } } else { - cb = aw->aw_callbacks[aw->aw_state - 1]; + cb = &aw->aw_callbacks[aw->aw_state - 1]; } PyObject *result = Py_TYPE( g->gw_current_await )->tp_iternext(g->gw_current_await); - if (result == NULL) + if (result != NULL) { - PyObject *occurred = PyErr_Occurred(); - if (!occurred) + return result; + } + + PyObject *occurred = PyErr_Occurred(); + if (!occurred) + { + // Coro is done, no result. + if (!cb->callback) { - // Coro is done - if (!cb->callback) - { - Py_DECREF(g->gw_current_await); - g->gw_current_await = NULL; - return genwrapper_next(self); - } + // No callback, skip that step. + DONE(cb); + return genwrapper_next(self); } + } + // TODO: I wonder if the occurred check is needed here. + if ( + occurred && !PyErr_ExceptionMatches(PyExc_StopIteration) + ) + { if ( - occurred && !PyErr_GivenExceptionMatches( - occurred, - PyExc_StopIteration - ) + genwrapper_fire_err_callback( + (PyObject *) aw, + g->gw_current_await, + cb + ) < 0 ) { - if ( - genwrapper_fire_err_callback( - (PyObject *) aw, - g->gw_current_await, - cb - ) < 0 - ) - { - return NULL; - } - - Py_DECREF(g->gw_current_await); - g->gw_current_await = NULL; - return genwrapper_next(self); + DONE(cb); + AW_DONE(); + return NULL; } - if (cb->callback == NULL) - { - // Coroutine is done, but with a result. - // We can disregard the result if theres no callback. - Py_DECREF(g->gw_current_await); - g->gw_current_await = NULL; - PyErr_Clear(); - return genwrapper_next(self); - } + DONE(cb); + return genwrapper_next(self); + } - PyObject *value; - if (occurred) - { - PyObject *type, *traceback; - PyErr_Fetch(&type, &value, &traceback); - PyErr_NormalizeException(&type, &value, &traceback); - Py_XDECREF(type); - Py_XDECREF(traceback); + if (cb->callback == NULL) + { + // Coroutine is done, but with a result. + // We can disregard the result if theres no callback. + DONE(cb); + PyErr_Clear(); + return genwrapper_next(self); + } - if (value == NULL) - { - value = Py_NewRef(Py_None); - } else - { - assert(PyObject_IsInstance(value, PyExc_StopIteration)); - PyObject *tmp = PyObject_GetAttrString(value, "value"); - if (tmp == NULL) - { - Py_DECREF(value); - return NULL; - } - value = tmp; - } - } else + PyObject *value; + if (occurred) + { + value = PyErr_GetRaisedException(); + assert(value != NULL); + assert(PyObject_IsInstance(value, PyExc_StopIteration)); + PyObject *tmp = PyObject_GetAttrString(value, "value"); + if (tmp == NULL) { - value = Py_NewRef(Py_None); + Py_DECREF(value); + DONE(cb); + AW_DONE(); + return NULL; } - - Py_INCREF(aw); - int result = cb->callback((PyObject *) aw, value); - Py_DECREF(aw); Py_DECREF(value); + value = tmp; + } else + { + value = Py_NewRef(Py_None); + } + + Py_INCREF(aw); + int res = cb->callback((PyObject *) aw, value); + Py_DECREF(aw); + Py_DECREF(value); - if (result < -1) + if (res < -1) + { + // -2 or lower denotes that the error should be deferred, + // regardless of whether a handler is present. + DONE(cb); + AW_DONE(); + return NULL; + } + + if (res < 0) + { + if (!PyErr_Occurred()) { - // -2 or lower denotes that the error should be deferred, - // regardless of whether a handler is present. + PyErr_SetString( + PyExc_SystemError, + "pyawaitable: callback returned -1 without exception set" + ); + DONE(cb); + AW_DONE(); return NULL; } - - if (result < 0) + if ( + genwrapper_fire_err_callback( + (PyObject *) aw, + g->gw_current_await, + cb + ) < 0 + ) { - if (!PyErr_Occurred()) - { - PyErr_SetString( - PyExc_SystemError, - "pyawaitable: callback returned -1 without exception set" - ); - return NULL; - } - if ( - genwrapper_fire_err_callback( - (PyObject *) aw, - g->gw_current_await, - cb - ) < 0 - ) - { - return NULL; - } + DONE(cb); + AW_DONE(); + return NULL; } - - cb->done = true; - Py_DECREF(g->gw_current_await); - g->gw_current_await = NULL; - return genwrapper_next(self); } - return result; + DONE(cb); + return genwrapper_next(self); } PyTypeObject _PyAwaitableGenWrapperType = diff --git a/src/_pyawaitable/mod.c b/src/_pyawaitable/mod.c index 98bdcb3..8f9d99e 100644 --- a/src/_pyawaitable/mod.c +++ b/src/_pyawaitable/mod.c @@ -45,7 +45,9 @@ static PyAwaitableABI _abi_interface = pyawaitable_unpack_impl, pyawaitable_unpack_arb_impl, &_PyAwaitableType, - pyawaitable_await_function_impl + pyawaitable_await_function_impl, + pyawaitable_save_int_impl, + pyawaitable_unpack_int_impl }; static void diff --git a/src/_pyawaitable/values.c b/src/_pyawaitable/values.c index 36540d6..6d13605 100644 --- a/src/_pyawaitable/values.c +++ b/src/_pyawaitable/values.c @@ -98,3 +98,28 @@ pyawaitable_save_arb_impl(PyObject *awaitable, Py_ssize_t nargs, ...) NOTHING ); } + +/* Integer Values */ + +int +pyawaitable_unpack_int_impl(PyObject *awaitable, ...) +{ + UNPACK( + aw->aw_int_values, + long *, + "integer values", + aw->aw_int_values_index + ); +} + +int +pyawaitable_save_int_impl(PyObject *awaitable, Py_ssize_t nargs, ...) +{ + SAVE( + aw->aw_int_values, + aw->aw_int_values_index, + long, + "integer values", + NOTHING + ); +} diff --git a/src/pyawaitable/bindings.py b/src/pyawaitable/bindings.py index 2b86ce3..546e5b0 100644 --- a/src/pyawaitable/bindings.py +++ b/src/pyawaitable/bindings.py @@ -72,6 +72,11 @@ class AwaitableABI(PyABI): ("unpack_arb", ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.py_object)), ("PyAwaitableType", ctypes.py_object), ("await_function", ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.py_object, ctypes.py_object, ctypes.c_char_p, awaitcallback, awaitcallback_err,)), + ( + "save_int", + ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.py_object, ctypes.c_ssize_t), + ), + ("unpack_int", ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.py_object)), ] diff --git a/src/pyawaitable/pyawaitable.h b/src/pyawaitable/pyawaitable.h index 8f86b12..f08dd80 100644 --- a/src/pyawaitable/pyawaitable.h +++ b/src/pyawaitable/pyawaitable.h @@ -3,8 +3,8 @@ #include #define PYAWAITABLE_MAJOR_VERSION 1 -#define PYAWAITABLE_MINOR_VERSION 0 -#define PYAWAITABLE_MICRO_VERSION 1 +#define PYAWAITABLE_MINOR_VERSION 1 +#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 0xF @@ -38,6 +38,8 @@ typedef struct _pyawaitable_abi awaitcallback_err, ... ); + int (*save_int)(PyObject *, Py_ssize_t nargs, ...); + int (*unpack_int)(PyObject *, ...); } PyAwaitableABI; #ifdef PYAWAITABLE_THIS_FILE_INIT @@ -66,6 +68,10 @@ extern PyAwaitableABI *pyawaitable_abi; #define pyawaitable_await_function pyawaitable_abi->await_function +#define pyawaitable_unpack_int pyawaitable_abi->unpack_int + +#define pyawaitable_save_int pyawaitable_abi->save_int + #ifdef PYAWAITABLE_THIS_FILE_INIT static int pyawaitable_init() @@ -107,6 +113,8 @@ pyawaitable_init() #define PyAwaitable_ABI pyawaitable_abi #define PyAwaitable_Type PyAwaitableType #define PyAwaitable_AwaitFunction pyawaitable_await_function +#define PyAwaitable_SaveIntValues pyawaitable_save_int +#define PyAwaitable_UnpackIntValues pyawaitable_unpack_int #endif static int diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8f2d6cf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +import functools +import inspect +import platform +import sys +from typing import Any, Callable + +import pytest + +ITERATIONS: int = 10000 + + +def pytest_addoption(parser: Any) -> None: + parser.addoption("--enable-leak-tracking", action="store_true") + + +def limit_leaks(memstring: str): + def decorator(func: Callable): + if "--enable-leak-tracking" not in sys.argv: + return func + + if platform.system() != "Windows": + if not inspect.iscoroutinefunction(func): + + @functools.wraps(func) + def wrapper(*args, **kwargs): # type: ignore + for _ in range(ITERATIONS): + func(*args, **kwargs) + + else: + + @functools.wraps(func) + async def wrapper(*args, **kwargs): + for _ in range(ITERATIONS): + await func(*args, **kwargs) + + wrapper = pytest.mark.asyncio(wrapper) + + return pytest.mark.limit_leaks(memstring)(wrapper) + else: + return func + + return decorator diff --git a/tests/test_awaitable.py b/tests/test_awaitable.py index 1ec3120..ebbcac8 100644 --- a/tests/test_awaitable.py +++ b/tests/test_awaitable.py @@ -2,11 +2,10 @@ import ctypes import pytest import asyncio -import platform import _pyawaitable_test from collections.abc import Coroutine -from typing import Callable from pyawaitable.bindings import abi, add_await, awaitcallback, awaitcallback_err +from conftest import limit_leaks LEAK_LIMIT: str = "10 KB" @@ -16,17 +15,6 @@ ) -def limit_leaks(memstring: str): - def decorator(func: Callable): - if platform.system() != "Windows": - func = pytest.mark.limit_leaks(memstring)(func) - return func - else: - return func - - return decorator - - @limit_leaks(LEAK_LIMIT) @pytest.mark.asyncio async def test_new(): @@ -38,13 +26,6 @@ async def test_new(): await abi.new() -@limit_leaks(LEAK_LIMIT) -@pytest.mark.asyncio -async def test_object_cleanup(): - for i in range(100000): - await abi.new() - - @limit_leaks(LEAK_LIMIT) @pytest.mark.asyncio async def test_await(): @@ -101,6 +82,7 @@ def cb_err(awaitable_inner: pyawaitable.PyAwaitable, err: Exception) -> int: add_await(awaitable, coro_raise(), cb, cb_err) await awaitable + @limit_leaks(LEAK_LIMIT) @pytest.mark.asyncio async def test_await_cb_err_cb(): @@ -168,7 +150,6 @@ def cb_err(awaitable_inner: pyawaitable.PyAwaitable, err: Exception) -> int: assert called is True - @limit_leaks(LEAK_LIMIT) @pytest.mark.asyncio async def test_await_cb_err_norestore(): @@ -214,7 +195,9 @@ def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: @limit_leaks(LEAK_LIMIT) @pytest.mark.asyncio -@pytest.mark.filterwarnings("ignore::RuntimeWarning") # Second and third iteration of echo() are skipped, resulting in a RuntimeWarning +@pytest.mark.filterwarnings( + "ignore::RuntimeWarning" +) # Second and third iteration of echo() are skipped, resulting in a RuntimeWarning async def test_await_cancel(): data = [] @@ -393,6 +376,7 @@ def cb(awaitable_inner: pyawaitable.PyAwaitable, result: str): await awaitable assert called is True + @limit_leaks(LEAK_LIMIT) @pytest.mark.asyncio async def test_null_save_arb(): @@ -411,7 +395,12 @@ def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: buffer_inner = ctypes.c_char_p() null = ctypes.c_void_p() buffer2_inner = ctypes.c_char_p() - abi.unpack_arb(awaitable_inner, ctypes.byref(buffer_inner), ctypes.byref(null), ctypes.byref(buffer2_inner)) + abi.unpack_arb( + awaitable_inner, + ctypes.byref(buffer_inner), + ctypes.byref(null), + ctypes.byref(buffer2_inner), + ) assert buffer_inner.value == b"test" assert buffer2_inner.value == b"hello" return 0 @@ -420,3 +409,36 @@ def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: await awaitable +@limit_leaks(LEAK_LIMIT) +@pytest.mark.asyncio +async def test_int_values(): + awaitable = abi.new() + + abi.save_int( + awaitable, + 3, + ctypes.c_long(42), + ctypes.c_long(3000), + ctypes.c_long(-10), + ) + + @awaitcallback + def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: + first = ctypes.c_long() + second = ctypes.c_long() + third = ctypes.c_long() + abi.unpack_int( + awaitable_inner, + ctypes.byref(first), + ctypes.byref(second), + ctypes.byref(third), + ) + assert first.value == 42 + assert second.value == 3000 + assert third.value == -10 + return 0 + + async def coro(): ... + + add_await(awaitable, coro(), cb, awaitcallback_err(0)) + await awaitable