diff --git a/CHANGELOG.md b/CHANGELOG.md index da32253..827ad11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed limit on number of stored callbacks or values. - Switched some user-error messages to `RuntimeError` instead of `SystemError`. - Added `PyAwaitable_DeferAwait` for executing code when the awaitable object is called by the event loop. +- Added `PyAwaitable_AwaitFunctionKeywords` for executing awaitable code that accepts keyword arguments. +- Added `PyAwaitable_AwaitFunctionNoArgs` for executing awaitable code that does not accept any arguments. ## [1.3.0] - 2024-10-26 diff --git a/include/pyawaitable/awaitableobject.h b/include/pyawaitable/awaitableobject.h index e22ee83..416430c 100644 --- a/include/pyawaitable/awaitableobject.h +++ b/include/pyawaitable/awaitableobject.h @@ -73,4 +73,21 @@ pyawaitable_await_function_impl( ... ); +int pyawaitable_await_function_keywords_impl( + PyObject *awaitable, + PyObject *func, + PyObject *args, + const char *fmt, + awaitcallback cb, + awaitcallback_err err, + ... +); + +int pyawaitable_await_function_no_args_impl( + PyObject *awaitable, + PyObject *func, + awaitcallback cb, + awaitcallback_err err +); + #endif diff --git a/src/_pyawaitable/awaitable.c b/src/_pyawaitable/awaitable.c index 6fea0e3..1e483e5 100644 --- a/src/_pyawaitable/awaitable.c +++ b/src/_pyawaitable/awaitable.c @@ -249,9 +249,76 @@ pyawaitable_await_function_impl( if (!args) return -1; - PyObject *coro = PyObject_Call(func, args, NULL); + int result = pyawaitable_await_function_keywords_impl(awaitable, func, args, NULL, cb, err); Py_DECREF(args); + return result; +} + +int pyawaitable_await_function_keywords_impl( + PyObject *awaitable, + PyObject *func, + PyObject *args, + const char *fmt, + awaitcallback cb, + awaitcallback_err err, + ... +) +{ + PyObject *kwargs; + if (fmt == NULL) + { + kwargs = NULL; + } + else + { + size_t len = strlen(fmt); + size_t size = len + 3; + char *dic_format = PyMem_Malloc(size); + if (!dic_format) + { + PyErr_NoMemory(); + return -1; + } + + dic_format[0] = '{'; + for (size_t i = 0; i < len; ++i) + { + dic_format[i + 1] = fmt[i]; + } + + dic_format[size - 2] = '}'; + dic_format[size - 1] = '\0'; + + va_list vargs; + va_start(vargs, err); + kwargs = Py_VaBuildValue(dic_format, vargs); + va_end(vargs); + PyMem_Free(dic_format); + } + + PyObject *coro = PyObject_Call(func, args, kwargs); + Py_XDECREF(kwargs); + if (!coro) + return -1; + + if (pyawaitable_await_impl(awaitable, coro, cb, err) < 0) + { + Py_DECREF(coro); + return -1; + } + Py_DECREF(coro); + return 0; +} + +int pyawaitable_await_function_no_args_impl( + PyObject *awaitable, + PyObject *func, + awaitcallback cb, + awaitcallback_err err +) +{ + PyObject *coro = PyObject_CallNoArgs(func); if (!coro) return -1; diff --git a/src/_pyawaitable/mod.c b/src/_pyawaitable/mod.c index f5a1c45..c7b555d 100644 --- a/src/_pyawaitable/mod.c +++ b/src/_pyawaitable/mod.c @@ -56,7 +56,9 @@ static PyAwaitableABI _abi_interface = pyawaitable_get_arb_impl, pyawaitable_get_int_impl, pyawaitable_async_with_impl, - pyawaitable_defer_await_impl + pyawaitable_defer_await_impl, + pyawaitable_await_function_keywords_impl, + pyawaitable_await_function_no_args_impl }; PyMODINIT_FUNC diff --git a/src/pyawaitable/bindings.py b/src/pyawaitable/bindings.py index 0169b90..8019edb 100644 --- a/src/pyawaitable/bindings.py +++ b/src/pyawaitable/bindings.py @@ -173,6 +173,28 @@ class AwaitableABI(PyABI): defer_callback ), ), + ( + "await_function_keywords", + ctypes.PYFUNCTYPE( + ctypes.c_int, + ctypes.py_object, + ctypes.py_object, + ctypes.py_object, + ctypes.c_char_p, + awaitcallback, + awaitcallback_err, + ), + ), + ( + "await_function_no_args", + ctypes.PYFUNCTYPE( + ctypes.c_int, + ctypes.py_object, + ctypes.py_object, + awaitcallback, + awaitcallback_err + ), + ), ] diff --git a/src/pyawaitable/pyawaitable.h b/src/pyawaitable/pyawaitable.h index 5c62088..a222462 100644 --- a/src/pyawaitable/pyawaitable.h +++ b/src/pyawaitable/pyawaitable.h @@ -56,6 +56,21 @@ typedef struct _pyawaitable_abi int (*defer_await)( PyObject *aw, defer_callback cb); + int (*await_function_keywords)( + PyObject *, + PyObject *, + PyObject *, + const char *fmt, + awaitcallback, + awaitcallback_err, + ... + ); + int (*await_function_no_args)( + PyObject *, + PyObject *, + awaitcallback cb, + awaitcallback_err err + ); } PyAwaitableABI; #ifdef PYAWAITABLE_THIS_FILE_INIT @@ -70,6 +85,8 @@ extern PyAwaitableABI *pyawaitable_abi; #define pyawaitable_await pyawaitable_abi->await #define pyawaitable_await_function pyawaitable_abi->await_function +#define pyawaitable_await_function_keywords pyawaitable_abi->await_function_keywords +#define pyawaitable_await_function_no_args pyawaitable_abi->await_function_no_args #define pyawaitable_async_with pyawaitable_abi->async_with #define pyawaitable_defer_await pyawaitable_abi->defer_await @@ -131,6 +148,8 @@ pyawaitable_init() #define PyAwaitable_AddAwait pyawaitable_await #define PyAwaitable_AwaitFunction pyawaitable_await_function +#define PyAwaitable_AwaitFunctionKeywords pyawaitable_await_function_keywords +#define PyAwaitable_AwaitFunctionNoArgs pyawaitable_await_function_no_args #define PyAwaitable_AsyncWith pyawaitable_async_with #define PyAwaitable_DeferAwait pyawaitable_defer_await diff --git a/tests/test_awaitable.py b/tests/test_awaitable.py index c6a27ef..b1c68c9 100644 --- a/tests/test_awaitable.py +++ b/tests/test_awaitable.py @@ -207,6 +207,52 @@ def cb(awaitable_inner: pyawaitable.PyAwaitable, result: str): assert called is True +@limit_leaks +@pytest.mark.asyncio +async def test_await_function_keywords(): + awaitable = abi.new() + called: bool = False + + async def coro(value: int, suffix: str ="test") -> str: + await asyncio.sleep(0) + return str(value * 2) + suffix + + @awaitcallback + def cb(awaitable_inner: pyawaitable.PyAwaitable, result: str): + nonlocal called + called = True + assert result == "70hello" + return 0 + + abi.await_function_keywords( + awaitable, coro, (35,), b"s:s", cb, awaitcallback_err(0), b"suffix", b"hello" + ) + await awaitable + assert called is True + + +@limit_leaks +@pytest.mark.asyncio +async def test_await_function_no_args(): + awaitable = abi.new() + called: bool = False + + async def coro() -> str: + await asyncio.sleep(0) + return "70Test" + + @awaitcallback + def cb(awaitable_inner: pyawaitable.PyAwaitable, result: str): + nonlocal called + called = True + assert result == "70Test" + return 0 + + abi.await_function_no_args(awaitable, coro, cb, awaitcallback_err(0)) + await awaitable + assert called is True + + @limit_leaks @pytest.mark.asyncio async def test_c_built_extension():