├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── crash.yml │ ├── feature.yml │ └── improvement.yml └── workflows │ ├── build.yml │ ├── tests.yml │ └── triage.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── adding_awaits.md ├── c_async.md ├── index.md ├── installation.md └── value_storage.md ├── hatch.toml ├── hatch_build.py ├── include └── pyawaitable │ ├── array.h │ ├── awaitableobject.h │ ├── backport.h │ ├── coro.h │ ├── dist.h │ ├── genwrapper.h │ ├── init.h │ ├── optimize.h │ ├── values.h │ └── with.h ├── mkdocs.yml ├── netlify.toml ├── pyproject.toml ├── requirements.txt ├── runtime.txt ├── src ├── _pyawaitable │ ├── array.c │ ├── awaitable.c │ ├── coro.c │ ├── genwrapper.c │ ├── init.c │ ├── values.c │ └── with.c └── pyawaitable │ ├── __init__.py │ └── __main__.py ├── tests ├── builds │ ├── ensure_build_worked.py │ ├── meson │ │ ├── meson.build │ │ ├── module.c │ │ └── pyproject.toml │ └── scikit-build-core │ │ ├── CMakeLists.txt │ │ ├── module.c │ │ └── pyproject.toml ├── module.c ├── pyawaitable_test.h ├── pyproject.toml ├── setup.py ├── test_awaitable.c ├── test_callbacks.c ├── test_main.py ├── test_util.h ├── test_values.c └── util.c └── uncrustify.cfg /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ZeroIntensity 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Submit a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: "Problem:" 8 | description: > 9 | Give a clear description on what's going wrong and how to reproduce it, if possible. 10 | 11 | This should only be for *bugs*, not crashes. For that, use the crash template. 12 | 13 | value: | 14 | ```c 15 | // Add your code here, if needed 16 | ``` 17 | validations: 18 | required: true 19 | - type: input 20 | attributes: 21 | label: "Version:" 22 | value: | 23 | What version(s) of PyAwaitable are you using? 24 | validations: 25 | required: true 26 | - type: dropdown 27 | attributes: 28 | label: "Operating system(s) tested on:" 29 | multiple: true 30 | options: 31 | - Linux 32 | - macOS 33 | - Windows 34 | - Other 35 | validations: 36 | required: false 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/crash.yml: -------------------------------------------------------------------------------- 1 | name: Crash report 2 | description: Submit a crash report 3 | labels: ["crash"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: "Crash Information:" 8 | description: > 9 | Give a clear description of what happened and how to reproduce it, if possible. If you aren't sure, attempt to run the program through a debugger, such as [Valgrind](https://valgrind.org/) or enabling [faulthandler](https://docs.python.org/3/library/faulthandler.html). 10 | 11 | This should only be for *crashes* not bugs. For that, use the bug template. 12 | 13 | value: | 14 | ```c 15 | // Add your code here, if needed 16 | ``` 17 | validations: 18 | required: true 19 | - type: input 20 | attributes: 21 | label: "Version:" 22 | value: | 23 | What version(s) of PyAwaitable are you using? 24 | validations: 25 | required: true 26 | - type: dropdown 27 | attributes: 28 | label: "Operating system(s) tested on:" 29 | multiple: true 30 | options: 31 | - Linux 32 | - macOS 33 | - Windows 34 | - Other 35 | validations: 36 | required: false 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature 2 | description: Suggest a new feature. 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # Feature Proposal 9 | 10 | This is where you should propose a *new* feature to PyAwaitable. New features should not make breaking changes. This should be for something totally new, such as adding a new function to the ABI. General improvements to existing features should be made using an improvement request. 11 | - type: textarea 12 | attributes: 13 | label: "Proposal:" 14 | description: > 15 | Outline your idea and why it would be a good idea for PyAwaitable. Make sure to include an example API for what this could look like if implemented. 16 | value: | 17 | ```c 18 | // Example API 19 | ``` 20 | validations: 21 | required: true 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improvement.yml: -------------------------------------------------------------------------------- 1 | name: Improvement 2 | description: Suggest an improvement to an existing feature. 3 | labels: ["improvement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # Improvement 9 | 10 | An improvement proposal should be related to an *existing* feature. For example, adding some functionality to the *existing* coroutine implementation. 11 | - type: textarea 12 | attributes: 13 | label: "Description:" 14 | description: > 15 | Outline what needs to be improved and why? Be sure to include an example API if necessary. 16 | value: | 17 | ```c 18 | // Example API 19 | ``` 20 | validations: 21 | required: true 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | paths: 10 | - "src/**" 11 | pull_request: 12 | branches: 13 | - master 14 | 15 | concurrency: 16 | group: build-${{ github.head_ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | pure-python-wheel-and-sdist: 21 | name: Build a pure Python wheel and source distribution 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Install build dependencies 30 | run: python -m pip install --upgrade build 31 | 32 | - name: Build 33 | run: python -m build 34 | 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: artifacts 38 | path: dist/* 39 | if-no-files-found: error 40 | 41 | publish: 42 | name: Publish release 43 | needs: 44 | - pure-python-wheel-and-sdist 45 | runs-on: ubuntu-latest 46 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 47 | 48 | steps: 49 | - uses: actions/download-artifact@v4 50 | with: 51 | name: artifacts 52 | path: dist 53 | 54 | - name: Push build artifacts to PyPI 55 | uses: pypa/gh-action-pypi-publish@v1.12.4 56 | with: 57 | skip_existing: true 58 | user: __token__ 59 | password: ${{ secrets.PYPI_API_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | concurrency: 12 | group: test-${{ github.head_ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | PYTHONUNBUFFERED: "1" 17 | FORCE_COLOR: "1" 18 | PYTHONIOENCODING: "utf8" 19 | 20 | jobs: 21 | changes: 22 | name: Check for changed files 23 | runs-on: ubuntu-latest 24 | outputs: 25 | source: ${{ steps.filter.outputs.source }} 26 | tests: ${{ steps.filter.outputs.tests }} 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: dorny/paths-filter@v3 30 | id: filter 31 | with: 32 | filters: | 33 | source: 34 | - 'src/**' 35 | tests: 36 | - 'tests/**' 37 | 38 | run-tests: 39 | needs: changes 40 | if: ${{ needs.changes.outputs.source == 'true' || needs.changes.outputs.tests == 'true' }} 41 | name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} 42 | runs-on: ${{ matrix.os }} 43 | strategy: 44 | fail-fast: true 45 | matrix: 46 | os: [ubuntu-latest, windows-latest, macos-latest] 47 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 48 | steps: 49 | - uses: actions/checkout@v3 50 | 51 | - name: Set up Python ${{ matrix.python-version }} 52 | uses: actions/setup-python@v4 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | 56 | - name: Install Hatch 57 | uses: pypa/hatch@install 58 | 59 | - name: Run tests 60 | run: hatch test 61 | 62 | - name: Run build test for scikit-build-core 63 | run: hatch run test-build:scikit-build-core 64 | 65 | - name: Run build test for meson-python 66 | run: hatch run test-build:meson 67 | 68 | tests-pass: 69 | runs-on: ubuntu-latest 70 | name: All tests passed 71 | if: always() 72 | 73 | needs: 74 | - run-tests 75 | 76 | steps: 77 | - name: Check whether all tests passed 78 | uses: re-actors/alls-green@release/v1 79 | with: 80 | jobs: ${{ toJSON(needs) }} 81 | allowed-skips: ${{ toJSON(needs) }} 82 | -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | name: Triage 2 | on: 3 | pull_request: 4 | types: 5 | - "opened" 6 | - "reopened" 7 | - "synchronize" 8 | - "labeled" 9 | - "unlabeled" 10 | 11 | jobs: 12 | changelog_check: 13 | runs-on: ubuntu-latest 14 | name: Check for changelog updates 15 | steps: 16 | - name: "Check if the source directory was changed" 17 | uses: dorny/paths-filter@v3 18 | id: changes 19 | with: 20 | filters: | 21 | src: 22 | - 'src/**' 23 | 24 | - name: "Check for changelog updates" 25 | if: steps.changes.outputs.src == 'true' 26 | uses: brettcannon/check-for-changed-files@v1 27 | with: 28 | file-pattern: | 29 | CHANGELOG.md 30 | skip-label: "skip changelog" 31 | failure-message: "Missing a CHANGELOG.md update; please add one or apply the ${skip-label} label to the pull request" 32 | 33 | tests_check: 34 | runs-on: ubuntu-latest 35 | name: Check for updated tests 36 | steps: 37 | - name: "Check if the source directory was changed" 38 | uses: dorny/paths-filter@v3 39 | id: changes 40 | with: 41 | filters: | 42 | src: 43 | - 'src/**' 44 | 45 | - name: "Check for test updates" 46 | if: steps.changes.outputs.src == 'true' 47 | uses: brettcannon/check-for-changed-files@v1 48 | with: 49 | file-pattern: | 50 | tests/* 51 | skip-label: "skip tests" 52 | failure-message: "Missing unit tests; please add some or apply the ${skip-label} label to the pull request" 53 | 54 | all_green: 55 | runs-on: ubuntu-latest 56 | name: PR has no missing information 57 | if: always() 58 | 59 | needs: 60 | - changelog_check 61 | - tests_check 62 | 63 | steps: 64 | - name: Check whether jobs passed 65 | uses: re-actors/alls-green@release/v1 66 | with: 67 | jobs: ${{ toJSON(needs) }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | .venv/ 4 | 5 | # Builds 6 | *.egg-info 7 | test/ 8 | dist/ 9 | *.so 10 | src/pyawaitable/pyawaitable.h 11 | pcbuild/ 12 | *.o 13 | 14 | # LSP 15 | compile_flags.txt 16 | build/ 17 | .vscode/ 18 | .vs/ 19 | *.sln 20 | *.user 21 | *.vcxproj* 22 | .clang-format 23 | 24 | # Misc 25 | test.py 26 | a.py 27 | vgcore* 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## [2.0.0] - 2025-03-30 6 | 7 | - Moved away from function pointer tables for loading PyAwaitable--everything is now vendored upon installation. 8 | - Improved performance with compiler optimizations. 9 | - `PyAwaitable_` prefixes are now required, and the old `pyawaitable_*` functions have been removed. 10 | - The warning emitted when a PyAwaitable object is not awaited is now a `ResourceWarning` (was a `RuntimeWarning`). 11 | - `PyAwaitable_AddAwait` now raises a `ValueError` if the passed object is `NULL` or self, and also now raises a `TypeError` if the passed object is not a coroutine. 12 | - Added a simple CLI, primarily for getting the include directory from `meson-python` (`pyawaitable --include`). 13 | - PyAwaitable objects now support garbage collection. 14 | - **Breaking Change:** `PyAwaitable_Init` no longer takes a module object. 15 | - **Breaking Change:** Renamed `awaitcallback` to `PyAwaitable_Callback` 16 | - **Breaking Change:** Renamed `awaitcallback_err` to `PyAwaitable_Error` 17 | - **Breaking Change:** Renamed `defercallback` to `PyAwaitable_Defer` 18 | - **Breaking Change:** Removed the integer value APIs (`SaveIntValues`, `LoadIntValues`, `SetIntValue`, `GetIntValue`). They proved to be maintenance heavy, unintuitive, and most of all replaceable with the arbitrary values API (via `malloc`ing an integer and storing it). 19 | 20 | ## [1.4.0] - 2025-02-09 21 | 22 | - Significantly reduced awaitable object size by dynamically allocating it. 23 | - Reduced memory footprint by removing preallocated awaitable objects. 24 | - Objects returned by a PyAwaitable object's `__await__` are now garbage collected (_i.e._, they don't leak with rare circular references). 25 | - Removed limit on number of stored callbacks or values. 26 | - Switched some user-error messages to `RuntimeError` instead of `SystemError`. 27 | - Added `PyAwaitable_DeferAwait` for executing code without a coroutine when the awaitable object is called by the event loop. 28 | 29 | ## [1.3.0] - 2024-10-26 30 | 31 | - Added support for `async with` via `pyawaitable_async_with`. 32 | 33 | ## [1.2.0] - 2024-08-06 34 | 35 | - Added getting and setting of value storage. 36 | 37 | ## [1.1.0] - 2024-08-03 38 | 39 | - Changed error message when attempting to await a non-awaitable object (_i.e._, it has no `__await__`). 40 | - Fixed coroutine iterator reference leak. 41 | - Fixed reference leak in error callbacks. 42 | - Fixed early exit of `pyawaitable_unpack_arb` if a `NULL` value was saved. 43 | - Added integer value saving and unpacking (`pyawaitable_save_int` and `pyawaitable_unpack_int`). 44 | - Callbacks are now preallocated for better performance. 45 | - Fixed reference leak in the coroutine `send()` method. 46 | 47 | ## [1.0.0] - 2024-06-24 48 | 49 | - Initial release. 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 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 | ## Running Tests 48 | 49 | PyAwaitable uses [Hatch](https://hatch.pypa.io), so that will handle everything for you: 50 | 51 | ``` 52 | $ hatch test 53 | ``` 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Peter Bierma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyAwaitable 2 | 3 | ## Call asynchronous code from an extension module 4 | 5 | [![Build](https://github.com/ZeroIntensity/pyawaitable/actions/workflows/build.yml/badge.svg)](https://github.com/ZeroIntensity/pyawaitable/actions/workflows/build.yml) 6 | ![Tests](https://github.com/ZeroIntensity/pyawaitable/actions/workflows/tests.yml/badge.svg) 7 | 8 | - [Docs](https://pyawaitable.zintensity.dev) 9 | - [Source](https://github.com/ZeroIntensity/pyawaitable) 10 | - [PyPI](https://pypi.org/project/pyawaitable) 11 | 12 | ## What is it? 13 | 14 | PyAwaitable is the _only_ library to support defining and calling asynchronous Python functions from pure C code. 15 | 16 | It was originally designed to be directly part of CPython; you can read the [scrapped PEP](https://gist.github.com/ZeroIntensity/8d32e94b243529c7e1c27349e972d926) about it. But, since this library only uses the public ABI, it's better fit outside of CPython, as a library. 17 | 18 | ## Installation 19 | 20 | Add it to your project's build process: 21 | 22 | ```toml 23 | # pyproject.toml example with setuptools 24 | [build-system] 25 | requires = ["setuptools", "pyawaitable"] 26 | build-backend = "setuptools.build_meta" 27 | ``` 28 | 29 | Include it in your extension: 30 | 31 | ```py 32 | from setuptools import setup, Extension 33 | import pyawaitable 34 | 35 | if __name__ == "__main__": 36 | setup( 37 | ..., 38 | ext_modules=[Extension(..., include_dirs=[pyawaitable.include()])] 39 | ) 40 | ``` 41 | 42 | ## Example 43 | 44 | ```c 45 | /* 46 | Equivalent to the following Python function: 47 | 48 | async def async_function(coro: collections.abc.Awaitable) -> None: 49 | await coro 50 | 51 | */ 52 | static PyObject * 53 | async_function(PyObject *self, PyObject *coro) 54 | { 55 | // Create our transport between the C world and the asynchronous world. 56 | PyObject *awaitable = PyAwaitable_New(); 57 | if (awaitable == NULL) { 58 | return NULL; 59 | } 60 | 61 | // Mark our Python coroutine, *coro*, for being executed by the event loop. 62 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 63 | Py_DECREF(awaitable); 64 | return NULL; 65 | } 66 | 67 | // Return our transport, allowing *coro* to be eventually executed. 68 | return awaitable; 69 | } 70 | ``` 71 | 72 | ## Copyright 73 | 74 | `pyawaitable` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. 75 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | 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. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | 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 -------------------------------------------------------------------------------- /docs/adding_awaits.md: -------------------------------------------------------------------------------- 1 | # Executing Asynchronous Calls from C 2 | 3 | Let's say we wanted to replicate the following in C: 4 | 5 | ```py 6 | async def trampoline(coro: collections.abc.Coroutine) -> int: 7 | return await coro 8 | ``` 9 | 10 | This is simply a function that we pass a coroutine to, and it will await it for us. It's not particularly useful, but it's just for learning purposes. 11 | 12 | We already know that `trampoline()` will evaluate to a magic coroutine object that supports `await`, via the `__await__` dunder, and needs to `yield from` its coroutines. So, if we wanted to break `trampoline` down into a synchronous Python function, it would look something like this: 13 | 14 | ```py 15 | class _trampoline_coroutine: 16 | def __init__(self, coro: collections.abc.Coroutine) -> None: 17 | self.coro = coro 18 | 19 | def __await__(self) -> collections.abc.Generator: 20 | yield 21 | yield from self.coro.__await__() 22 | 23 | def trampoline(coro: collections.abc.Coroutine) -> collections.abc.Coroutine: 24 | return _trampoline_coroutine(coro) 25 | ``` 26 | 27 | But, this is using `yield from`; there's no `yield from` in C, so how do we actually await things, or more importantly, use their return value? This is where things get tricky. 28 | 29 | ## Adding Awaits to a PyAwaitable Object 30 | 31 | There's one big function for "adding" coroutines to a PyAwaitable object: `PyAwaitable_AddAwait`. By "add," we mean that the asynchronous call won't happen right then and there. Instead, the PyAwaitable will store it, and then when something comes to call the `__await__` on the PyAwaitable object, it will mimick a `yield from` on that coroutine. 32 | 33 | `PyAwaitable_AddAwait` takes four arguments: 34 | 35 | - The PyAwaitable object. 36 | - The _coroutine_ to store. (Not an `async def` function, but the result of calling one without `await`.) 37 | - A callback. 38 | - An error callback. 39 | 40 | Let's focus on the first two for now, and just pass `NULL` for the other two in the meantime. We can implement `trampoline` from our earlier example pretty easily: 41 | 42 | ```c 43 | static PyObject * 44 | trampoline(PyObject *self, PyObject *coro) // METH_O 45 | { 46 | PyObject *awaitable = PyAwaitable_New(); 47 | if (awaitable == NULL) { 48 | return NULL; 49 | } 50 | 51 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 52 | Py_DECREF(awaitable); 53 | return NULL; 54 | } 55 | 56 | return awaitable; 57 | } 58 | ``` 59 | 60 | To your eyes, the `yield from` and all of that mess is completely hidden; you give PyAwaitable your coroutine, and it handles the rest! `trampoline` now acts like our pure-Python function from earlier: 61 | 62 | ```py 63 | >>> from _yourmod import trampoline 64 | >>> import asyncio 65 | >>> await trampoline(asyncio.sleep(2)) # Sleeps for 2 seconds 66 | ``` 67 | 68 | Yay! We called an asynchronous function from C! 69 | 70 | ## Getting the Return Value in a Callback 71 | 72 | In many cases, it's desirable to use the return value of a coroutine. For example, let's say we wanted to get the result of the following asynchronous function: 73 | 74 | ```py 75 | async def silly() -> int: 76 | await asyncio.sleep(2) # Simulate doing some I/O work 77 | return 42 78 | ``` 79 | 80 | The details of how coroutines return values aren't relevant, but we do know that a coroutine isn't actually "awaited" until _after_ we've already returned our PyAwaitable object from C. That means we have to use a callback to get the return value of the coroutine. 81 | 82 | Specifically, we can pass a function pointer to the third parameter of `PyAwaitable_AddAwait`. A callback function takes two `PyObject *` parameters: 83 | 84 | - A _borrowed_ reference to the PyAwaitable object that called it. 85 | - A _borrowed_ reference to the return value of the coroutine. 86 | 87 | A callback must return `0` to indicate success, or `-1` with an exception set to indicate failure. 88 | 89 | Now, we can use the result of `silly` in C: 90 | 91 | ```c 92 | static int 93 | callback(PyObject *awaitable, PyObject *value) 94 | { 95 | if (PyAwaitable_SetResult(awaitable, value) < 0) { 96 | return -1; 97 | } 98 | 99 | return 0; 100 | } 101 | 102 | static PyObject * 103 | call_silly(PyObject *self, PyObject *silly) 104 | { 105 | PyObject *awaitable = PyAwaitable_New(); 106 | if (awaitable == NULL) { 107 | return NULL; 108 | } 109 | 110 | // Get the coroutine by calling silly() 111 | PyObject *coro = PyObject_CallNoArgs(silly); 112 | if (coro == NULL) { 113 | Py_DECREF(awaitable); 114 | return NULL; 115 | } 116 | 117 | if (PyAwaitable_AddAwait(awaitable, coro, callback, NULL) < 0) { 118 | Py_DECREF(awaitable); 119 | Py_DECREF(coro); 120 | return NULL; 121 | } 122 | 123 | Py_DECREF(coro); 124 | return awaitable; 125 | } 126 | ``` 127 | 128 | This can be used from Python as such: 129 | 130 | ```py 131 | >>> from _yourmod import call_silly 132 | >>> await call_silly(silly) # Sleeps for 2 seconds 133 | silly() returned: 42 134 | ``` 135 | 136 | ## Handling Errors with Callbacks 137 | 138 | Coroutines can raise exceptions, during execution. For example, imagine we wanted to use a function that makes a network request: 139 | 140 | ```py 141 | import asyncio 142 | 143 | 144 | async def make_request() -> str: 145 | async with asyncio.timeout(5): 146 | await asyncio.sleep(10) # Simulate some I/O 147 | return "..." 148 | ``` 149 | 150 | The above will raise `TimeoutError`, but not on simply calling `make_request()`; it will only raise once it's actually started executing in an `await`, and as we already know that coroutines don't execute at the `PyAwaitable_AddAwait` callsite, we can't simply check for errors there. So, similar to return value callbacks, PyAwaitable provides error callbacks, which--you guessed it--is the fourth argument to `PyAwaitable_AddAwait`. 151 | 152 | An error callback has the same signature as a return value callback, but instead of taking a reference to a return value, it takes a borrowed reference to an exception object that was caught and raised by either the coroutine or the coroutine's callback. 153 | 154 | !!! note 155 | 156 | Error callbacks are *not* called with an exception "set" (*i.e.*, `PyErr_Occurred()` returns `NULL`), so it's safe to call most of Python's C API without worrying about those kinds of failures. 157 | 158 | An error callback's return value can do a number of different things to the state of the PyAwaitable object's exception. Namely: 159 | 160 | - Returning `0` will consider the error successfully caught, so PyAwaitable object will clear the exception and continue executing the rest of its coroutine. 161 | - Returning `-1` indicates that the error should be repropagated. The PyAwaitable object will officially "set" the Python exception (via `PyErr_SetRaisedException`), raise the error to the event loop and stop itself from executing any future coroutines. 162 | - Returning `-2` indicates that a new error occurred while handling the other one; the original exception is _not_ restored, and an exception set by the error callback is used instead and propagated to the event loop. 163 | 164 | !!! note 165 | 166 | Return value callbacks are not called if an exception occurred while executing the coroutine. 167 | 168 | To try and give a real-world example of all three of these, let's implement the following function in C: 169 | 170 | ```py 171 | async def is_api_reachable(make_request: Callable[[], collections.abc.Coroutine]) -> bool: 172 | try: 173 | await make_request() 174 | return True 175 | except TimeoutError: 176 | return False 177 | ``` 178 | 179 | !!! note 180 | 181 | `asyncio.TimeoutError` is an alias of the `TimeoutError` builtin. 182 | 183 | We have to do several things here: 184 | 185 | - Call `make_request` to get the coroutine object to `await`. 186 | - Add an error callback for that coroutine. 187 | - In a return value callback, set the return value to `True`, because that means the operation didn't time out. 188 | - In an error callback, check if the exception is an instance of `TimeoutError`, and set the return value to `False` if it is. 189 | - If its something other than `TimeoutError`, let it propagate. 190 | 191 | In C, all that would be implemented like this: 192 | 193 | ```c 194 | static int 195 | return_true(PyObject *awaitable, PyObject *unused) 196 | { 197 | return PyAwaitable_SetResult(awaitable, Py_True); 198 | } 199 | 200 | static int 201 | return_false(PyObject *awaitable, PyObject *exc) 202 | { 203 | if (PyErr_GivenExceptionMatches(exc, PyExc_TimeoutError)) { 204 | if (PyAwaitable_SetResult(exc, Py_False) < 0) { 205 | // New exception occurred; give it to the event loop. 206 | return -2; 207 | } 208 | 209 | return 0; 210 | } else { 211 | // This isn't a TimeoutError! 212 | return -1; 213 | } 214 | } 215 | 216 | static PyObject * 217 | is_api_reachable(PyObject *self, PyObject *make_request) 218 | { 219 | PyObject *awaitable = PyAwaitable_New(); 220 | if (awaitable == NULL) { 221 | return NULL; 222 | } 223 | 224 | // Remember, this isn't the same as executing the coroutine, so 225 | // the timeout doesn't show up here. But, we still need to handle 226 | // an exception case, because something might have gone wrong 227 | // in getting the coroutine object, e.g., the object isn't callable 228 | // or we're out of memory. 229 | PyObject *coro = PyObject_CallNoArgs(make_request); 230 | if (coro == NULL) { 231 | Py_DECREF(awaitable); 232 | return NULL; 233 | } 234 | 235 | if (PyAwaitable_AddAwait(awaitable, coro, return_true, return_false)) { 236 | Py_DECREF(awaitable); 237 | Py_DECREF(coro); 238 | return NULL; 239 | } 240 | 241 | Py_DECREF(coro); 242 | return awaitable; 243 | } 244 | ``` 245 | 246 | ### Propagation of Errors in Return Value Callbacks 247 | 248 | By default, returning `-1` from a return value callback will implicitly call the error callback if one is set. But, this isn't always desirable; sometimes, you want to let errors in callbacks bubble up instead of getting handled by some default error handling mechanism you've installed. 249 | 250 | You can force the PyAwaitable object to propagate the exception by returning `-2` from a return value callback. If `-2` is returned, the exception set by the callback will always be raised back to whoever `await`ed the PyAwaitable object. 251 | 252 | For example, if we installed some global exception logger inside of the error callback, but don't want that to grab things like a `MemoryError` inside of the return callback, we would return `-2`: 253 | 254 | ```c 255 | static int 256 | error_handler(PyObject *awaitable, PyObject *error) 257 | { 258 | // Simply print the error and continue execution 259 | PyErr_SetRaisedException(Py_NewRef(error)); 260 | PyErr_Print(); 261 | return 0; 262 | } 263 | 264 | static int 265 | handle_value(PyObject *awaitable, PyObject *something) 266 | { 267 | PyObject *message = PyUnicode_FromString("LOG: Got value"); 268 | if (message == NULL) { 269 | // Skip the error callback 270 | return -2; 271 | } 272 | 273 | if (magically_log_value(message, something) < 0) { 274 | // Skip the error callback 275 | return -2; 276 | } 277 | 278 | return 0; 279 | } 280 | ``` 281 | -------------------------------------------------------------------------------- /docs/c_async.md: -------------------------------------------------------------------------------- 1 | # Making a C Function Asynchronous 2 | 3 | Let's make a C function that replicates the following Python code: 4 | 5 | ```py 6 | async def hello() -> None: 7 | print("Hello, PyAwaitable") 8 | ``` 9 | 10 | If you've tried to implement an asynchronous C function in the past, this is likely where you got stuck. How do we make a C function `async`? 11 | 12 | ## Breaking Down Awaitable Functions 13 | 14 | In Python, you have to _call_ an `async def` function to use it with `await`. In our example above, the following would be invalid: 15 | 16 | ```py 17 | >>> await hello 18 | ``` 19 | 20 | Of course, you need to do `await hello()` instead. `hello()` is returning a _coroutine_, and coroutine objects are usable with the `await` keyword. So, `hello` as a synchronous function would really be like: 21 | 22 | ```py 23 | class _hello_coroutine: 24 | def __await__(self) -> collections.abc.Generator: 25 | print("Hello, PyAwaitable") 26 | yield 27 | 28 | def hello() -> collections.abc.Coroutine: 29 | return _hello_coroutine() 30 | ``` 31 | 32 | If there were to be `await` expressions inside `hello`, the returned coroutine object would handle those by yielding inside of the `__await__` dunder method. We can do the same kind of thing in C. 33 | 34 | ## Creating PyAwaitable Objects 35 | 36 | You can create a new PyAwaitable object with `PyAwaitable_New`. This returns a _strong reference_ to a PyAwaitable object, and `NULL` with an exception set on failure. 37 | 38 | Think of a PyAwaitable object sort of like the `_hello_coroutine` example from above, but it's _generic_ instead of being special for `hello`. So, like our Python example, we need to return the coroutine to allow it to be used in `await` expressions: 39 | 40 | ```c 41 | static PyObject * 42 | hello(PyObject *self, PyObject *nothing) // METH_NOARGS 43 | { 44 | PyObject *awaitable = PyAwaitable_New(); 45 | if (awaitable == NULL) { 46 | return NULL; 47 | } 48 | 49 | puts("Hello, PyAwaitable"); 50 | return awaitable; 51 | } 52 | ``` 53 | 54 | !!! note "There's a difference between native coroutines and implemented coroutines" 55 | 56 | "Coroutine" is a bit of an ambigious term in Python. There are two types of coroutines: native ones ([`types.CoroutineType`](https://docs.python.org/3/library/types.html#types.CoroutineType)), and objects that implement the *coroutine protocol* ([`collections.abc.Coroutine`](https://docs.python.org/3/library/types.html#types.CoroutineType)). Only the interpreter itself can create native coroutines, so a PyAwaitable object is an object that implements the coroutine protocol. 57 | 58 | Yay! We can now use `hello` in `await` expressions: 59 | 60 | ```py 61 | >>> from _yourmod import hello 62 | >>> await hello() 63 | Hello, PyAwaitable 64 | ``` 65 | 66 | ## Changing the Return Value 67 | 68 | Note that in all code-paths, we should return the PyAwaitable object, or `NULL` with an exception set to indicate a failure. But that means you can't simply `return` your own value; how can the `await` expression evaluate to something useful? 69 | 70 | By default, the "return value" (_i.e._, what `await` will evaluate to) is `None`. That can be changed with `PyAwaitable_SetResult`, which takes a reference to the object you want to return. 71 | 72 | For example, if you wanted to return the Python integer `42` from `hello`, you would simply pass that to `PyAwaitable_SetResult`: 73 | 74 | ```c 75 | static PyObject * 76 | hello(PyObject *self, PyObject *nothing) // METH_NOARGS 77 | { 78 | PyObject *awaitable = PyAwaitable_New(); 79 | if (awiatable == NULL) { 80 | return NULL; 81 | } 82 | 83 | PyObject *my_number = PyLong_FromLong(42); 84 | if (my_number == NULL) { 85 | Py_DECREF(awaitable); 86 | return NULL; 87 | } 88 | 89 | if (PyAwaitable_SetResult(awaitable, my_number) < 0) { 90 | Py_DECREF(awaitable); 91 | Py_DECREF(my_number); 92 | return NULL; 93 | } 94 | 95 | Py_DECREF(my_number); 96 | 97 | puts("Hello, PyAwaitable"); 98 | return awaitable; 99 | } 100 | ``` 101 | 102 | Now, the `await` expression evalutes to `42`: 103 | 104 | ```py 105 | >>> from _yourmod import hello 106 | >>> await hello() 107 | Hello, PyAwaitable 108 | 42 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PyAwaitable 2 | 3 | !!! note 4 | 5 | This project originates from a scrapped PEP. For the original text, see [here](https://gist.github.com/ZeroIntensity/8d32e94b243529c7e1c27349e972d926). 6 | 7 | ## Introduction 8 | 9 | CPython currently has no existing C interface for writing asynchronous functions or doing any sort of `await` operations, other than defining extension types and manually implementing methods like `__await__` from scratch. This lack of an API can be seen in some Python-to-C transpilers (such as `mypyc`) having limited support for asynchronous code. 10 | 11 | In the C API, developers are forced to do one of three things when it comes to asynchronous code: 12 | 13 | - Manually implementing coroutines using extension types. 14 | - Use an external tool to compile their asynchronous code to C. 15 | - Defer their asynchronous logic to a synchronous Python function, and then call that natively. 16 | 17 | Since there are other event loop implementations, PyAwaitable aims to be a _generic_ interface for working with asynchronous operations from C (as in, we'll only be implementing features like `async def` and `await`, but not things like `asyncio.create_task`.) 18 | 19 | This documentation assumes that you're familiar with the C API already, and understand some essential concepts like reference counting (as well as borrowed and strong references). If you don't know what any of that means, it's highly advised that you read through the [Python docs](https://docs.python.org/3/extending/extending.html) before trying to use PyAwaitable. 20 | 21 | ## Quickstart 22 | 23 | Add PyAwaitable as a build dependency: 24 | 25 | ```toml 26 | # pyproject.toml 27 | [build-system] 28 | requires = ["your_preferred_build_system", "pyawaitable>=2.0.0"] 29 | build-backend = "your_preferred_build_system.build" 30 | ``` 31 | 32 | Use it in your extension: 33 | 34 | ```c 35 | /* 36 | Equivalent to the following Python function: 37 | 38 | async def async_function(coro: collections.abc.Awaitable) -> None: 39 | await coro 40 | 41 | */ 42 | static PyObject * 43 | async_function(PyObject *self, PyObject *coro) 44 | { 45 | PyObject *awaitable = PyAwaitable_New(); 46 | if (awaitable == NULL) { 47 | return NULL; 48 | } 49 | 50 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 51 | Py_DECREF(awaitable); 52 | return NULL; 53 | } 54 | 55 | return awaitable; 56 | } 57 | ``` 58 | 59 | !!! note 60 | 61 | You need to call `PyAwaitable_Init` upon initializing your extension! This can be done in the `PyInit_` function, or a module-exec function if using [multi-phase initialization](https://docs.python.org/3/c-api/module.html#initializing-c-modules). 62 | 63 | ## Acknowledgements 64 | 65 | Special thanks to: 66 | 67 | - [Petr Viktorin](https://github.com/encukou), for his feedback on the initial API design and PEP. 68 | - [Sean Hunt](https://github.com/AraHaan), for beta testing. 69 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Build Dependency 4 | 5 | PyAwaitable needs to be installed as a build dependency, not a runtime dependency. For the `project.dependencies` portion of your `pyproject.toml`, you can completely omit PyAwaitable as a dependency! 6 | 7 | For example, in a `setuptools` based project, your `build-system` section would look like: 8 | 9 | ```toml 10 | # pyproject.toml 11 | [build-system] 12 | requires = ["setuptools~=78.1", "pyawaitable>=2.0.0"] 13 | build-backend = "setuptools.build_meta" 14 | ``` 15 | 16 | ## Including the `pyawaitable.h` File 17 | 18 | PyAwaitable provides a number of APIs to access the include path for a number of different build systems. Namely: 19 | 20 | - `pyawaitable.include()` is available in Python code (typically for `setup.py`-based extensions). 21 | - `PYAWAITABLE_INCLUDE` is accessible as an environment variable, but only if Python has been started without `-S` (this is useful for `scikit-build-core` projects). 22 | - `pyawaitable --include` returns the path of the include directory (useful for everything else, such as `meson-python`). 23 | 24 | !!! note 25 | 26 | PyAwaitable uses a nifty trick for building itself into your project. Python's packaging ecosystem isn't exactly great at distributing C libraries, so the `pyawaitable.h` actually contains the entire PyAwaitable source code (but with mangled names to prevent collisions with your own project). 27 | 28 | This has some pros and cons: 29 | 30 | - PyAwaitable doesn't need to be installed at runtime, as it's embedded directly into your extension. This means it's *extremely* portable; completely different PyAwaitable versions can peacefully coexist in the same process. 31 | - Enabling debug flags in your extension also means enabling debug flags in PyAwaitable, thus enabling assertions and whatnot. This is useful for debugging. 32 | - However, PyAwaitable can't use the limited API, so it prevents your extension from using the limited API (see the note below). 33 | 34 | ## Initializing PyAwaitable in Your Extension 35 | 36 | PyAwaitable has to do a one-time initialization to get its types and other state initialized in the Python process. This is done with `PyAwaitable_Init`, which can be called basically anywhere, as long as its called before any other PyAwaitable functions are used. 37 | 38 | Typically, you'll want to call this in your extension's `PyInit_` function, or in the module-exec function in multi-phase extensions. For example: 39 | 40 | ```c 41 | // Single-phase 42 | PyMODINIT_FUNC 43 | PyInit_mymodule() 44 | { 45 | if (PyAwaitable_Init() < 0) { 46 | return NULL; 47 | } 48 | 49 | return PyModule_Create(/* ... */); 50 | } 51 | ``` 52 | 53 | ```c 54 | // Multi-phase 55 | static int 56 | module_exec(PyObject *mod) 57 | { 58 | return PyAwaitable_Init(); 59 | } 60 | ``` 61 | 62 | !!! warning "No Limited API Support" 63 | 64 | Unfortunately, PyAwaitable cannot be used with the [limited C API](https://docs.python.org/3/c-api/stable.html#limited-c-api). This is due to PyAwaitable needing [am_send](https://docs.python.org/3/c-api/typeobj.html#c.PyAsyncMethods.am_send) to implement the coroutine protocol on 3.10+, but the corresponding heap-type slot `Py_am_send` was not added until 3.11. Therefore, PyAwaitable cannot support the limited API without dropping support for <3.11. 65 | 66 | ## Examples 67 | 68 | ### `setuptools` 69 | 70 | ```py 71 | # setup.py 72 | from setuptools import setup, Extension 73 | import pyawaitable 74 | 75 | if __name__ == "__main__": 76 | setup( 77 | ext_modules=[ 78 | Extension("_module", ["src/module.c"], include_dirs=[pyawaitable.include()]) 79 | ] 80 | ) 81 | ``` 82 | 83 | ### `scikit-build-core` 84 | 85 | ```t 86 | # CMakeLists.txt 87 | cmake_minimum_required(VERSION 3.15...3.30) 88 | project(${SKBUILD_PROJECT_NAME} LANGUAGES C) 89 | 90 | find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) 91 | 92 | Python_add_library(_module MODULE src/module.c WITH_SOABI) 93 | target_include_directories(_module PRIVATE $ENV{PYAWAITABLE_INCLUDE}) 94 | install(TARGETS _module DESTINATION .) 95 | ``` 96 | 97 | ### `meson-python` 98 | 99 | ```py 100 | # meson.build 101 | project('_module', 'c') 102 | 103 | py = import('python').find_installation(pure: false) 104 | pyawaitable_include = run_command('pyawaitable --include', check: true).stdout().strip() 105 | 106 | py.extension_module( 107 | '_module', 108 | 'src/module.c', 109 | install: true, 110 | include_directories: [pyawaitable_include], 111 | ) 112 | ``` 113 | 114 | ## Simple Extension Example 115 | 116 | ```c 117 | #include 118 | #include 119 | 120 | static int 121 | module_exec(PyObject *mod) 122 | { 123 | return PyAwaitable_Init(); 124 | } 125 | 126 | /* 127 | Equivalent to the following Python function: 128 | 129 | async def async_function(coro: collections.abc.Awaitable) -> None: 130 | await coro 131 | 132 | */ 133 | static PyObject * 134 | async_function(PyObject *self, PyObject *coro) 135 | { 136 | PyObject *awaitable = PyAwaitable_New(); 137 | if (awaitable == NULL) { 138 | return NULL; 139 | } 140 | 141 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 142 | Py_DECREF(awaitable); 143 | return NULL; 144 | } 145 | 146 | return awaitable; 147 | } 148 | 149 | static PyModuleDef_Slot module_slots[] = { 150 | {Py_mod_exec, module_exec}, 151 | {0, NULL} 152 | }; 153 | 154 | static PyMethodDef module_methods[] = { 155 | {"async_function", async_function, METH_O, NULL}, 156 | {NULL, NULL, 0, NULL}, 157 | }; 158 | 159 | static PyModuleDef module = { 160 | .m_base = PyModuleDef_HEAD_INIT, 161 | .m_size = 0, 162 | .m_slots = module_slots, 163 | .m_methods = module_methods 164 | }; 165 | 166 | PyMODINIT_FUNC 167 | PyInit__module() 168 | { 169 | return PyModuleDef_Init(&module); 170 | } 171 | ``` 172 | -------------------------------------------------------------------------------- /docs/value_storage.md: -------------------------------------------------------------------------------- 1 | # Managing State Between Callbacks 2 | 3 | So far, all of our examples haven't needed any transfer of state between the `PyAwaitable_AddAwait` callsite and the time the coroutine is executed (_i.e._, in a callback). You might have noticed that the callbacks don't take a `void *arg` parameter to make up for C's lack of closures, so how do we manage state? 4 | 5 | First, let's get an example of what we might want state for. Our goal is to implement a function that takes a paremeter and does something wiith it against the result of a coroutine. For example: 6 | 7 | ```py 8 | async def multiply_async( 9 | number: int, 10 | get_number_io: Callable[[], collections.abc.Awaitable[int]] 11 | ) -> str: 12 | value = await get_number_io() 13 | return value * number 14 | ``` 15 | 16 | ## Introducing Value Storage 17 | 18 | Instead of `void *arg` parameters, PyAwaitable provides APIs for storing state directly on the PyAwaitable object. There are two types of value storage: 19 | 20 | - Object value storage; `PyObject *` pointers that PyAwaitable correctly stores references to. 21 | - Arbitrary value storage; `void *` pointers that PyAwaitable never dereferences--it's your job to manage it. 22 | 23 | Value storage is generally a lot more convenient than something like a `void *arg`, because you don't have to define any `struct` or make an extra allocations. It's especially convenient in the `PyObject *` case, because you don't have to worry about dealing with their reference counts or traversing reference cycles. And, even if a single state `struct` is more convenient for your case, it's easy to implement it with the arbitrary values API. 24 | 25 | There are four parts to the value APIs, each with a variant for object and arbitrary values: 26 | 27 | - Saving values; `PyAwaitable_SaveValues`/`PyAwaitable_SaveArbValues`. 28 | - Unpacking values; `PyAwaitable_UnpackValues`/`PyAwaitable_UnpackArbValues`. 29 | - Getting values; `PyAwaitable_GetValue`/`PyAwaitable_GetArbValue`. 30 | - Setting values; `PyAwaitable_SetValue`/`PyAwaitable_SetArbValue`. 31 | 32 | ## Object Value Storage 33 | 34 | In most cases, you want to store Python objects on your PyAwaitable object. This can be anything you want, such as arguments passed into your function. The two main APIs you want when using value storage are `PyAwaitable_SaveValues` and `PyAwaitable_UnpackValues`. 35 | 36 | These are variadic C functions; for `Save`, pass the PyAwaitable object and the number of objects you want to store, and then pass `PyObject *` pointers matching that number. These references will _not_ be stolen by PyAwaitable. 37 | 38 | `Unpack`, on the other hand, does not require you to pass the number of objects that you want--it remembers how many you stored in `Save`. In `Unpack`, you just pass the PyAwaitable object and pointers to local `PyObject *` variables, which will then be unpacked by the PyAwaitable object (these may be `NULL`, in which case the value is skipped). 39 | 40 | !!! note 41 | 42 | Both `PyAwaitable_SaveValues` and `PyAwaitable_UnpackValues` can fail. They return `-1` with an exception set on failure, and `0` on success. 43 | 44 | For example, if you called `PyAwaitable_SaveValues(awaitable, 3, /* ... */)`, you must pass three non-`NULL` `PyObject *` references, and then pass three pointers-to-pointers to `PyAwaitable_UnpackValues` (but these may be `NULL`). 45 | 46 | So, with all that in mind, we can implement `multiply_async` above as such: 47 | 48 | ```c 49 | static int 50 | multiply_callback(PyObject *awaitable, PyObject *value) 51 | { 52 | PyObject *number; 53 | if (PyAwaitable_UnpackValues(awaitable, &number) < 0) { 54 | return -1; 55 | } 56 | 57 | PyObject *result = PyNumber_Multiply(number, value); 58 | if (result == NULL) { 59 | return -1; 60 | } 61 | 62 | if (PyAwaitable_SetResult(awaitable, result) < 0) { 63 | Py_DECREF(result); 64 | return -1; 65 | } 66 | 67 | Py_DECREF(result); 68 | return 0; 69 | } 70 | 71 | static PyObject * 72 | multiply_async(PyObject *self, PyObject *args) // METH_VARARGS 73 | { 74 | PyObject *number; 75 | PyObject *get_number_io; 76 | 77 | if (!PyArg_ParseTuple(args, "OO", &number, &get_number_io)) { 78 | return NULL; 79 | } 80 | 81 | PyObject *awaitable = PyAwaitable_New(); 82 | if (awaitable == NULL) { 83 | return NULL; 84 | } 85 | 86 | if (PyAwaitable_SaveValues(awaitable, 1, number) < 0) { 87 | Py_DECREF(awaitable); 88 | return NULL; 89 | } 90 | 91 | PyObject *coro = PyObject_CallNoArgs(get_number_io); 92 | if (coro == NULL) { 93 | Py_DECREF(awaitable); 94 | return NULL; 95 | } 96 | 97 | if (PyAwaitable_AddAwait(awaitable, coro, multiply_callback, NULL) < 0) { 98 | Py_DECREF(awaitable); 99 | Py_DECREF(coro); 100 | return NULL; 101 | } 102 | 103 | Py_DECREF(coro); 104 | return awaitable; 105 | } 106 | ``` 107 | 108 | ## Getting and Setting Values 109 | 110 | In rare cases, it might be desirable to get or set a specific value at an index. `PyAwaitable_SetValue` is useful if you intend to completely overwrite an object at a value index, but `PyAwaitable_GetValue` should basically never be preferred over `PyAwaitable_UnpackValues`; it's, more or less, there for completion. 111 | 112 | ## Arbitrary Value Storage 113 | 114 | Arbitrary value storage works exactly the same as object value storage, with the exception of taking `void *` pointers instead of `PyObject *` pointers. PyAwaitable will never attempt to read or write the pointers that you pass, so managing their lifetime is up to you. In most cases, if your PyAwaitable object is supposed to own the state of the arbitrary value, you deallocate it in the last callback. 115 | -------------------------------------------------------------------------------- /hatch.toml: -------------------------------------------------------------------------------- 1 | [version] 2 | path = "src/pyawaitable/__init__.py" 3 | 4 | [build.targets.wheel] 5 | packages = ["src/pyawaitable"] 6 | 7 | [build.targets.sdist] 8 | only-include = ["src/pyawaitable", "include/", "src/_pyawaitable"] 9 | 10 | [build.targets.wheel.hooks.autorun] 11 | dependencies = ["hatch-autorun"] 12 | code = """ 13 | import pyawaitable 14 | import os 15 | 16 | os.environ['PYAWAITABLE_INCLUDE'] = pyawaitable.include(suppress_error=True) 17 | """ 18 | 19 | [build.hooks.custom] 20 | enable-by-default = true 21 | dependencies = ["typing_extensions"] 22 | 23 | [envs.hatch-test] 24 | dependencies = ["pyawaitable_test @ {root:uri}/tests", "pytest"] 25 | default-args = ["--verbose"] 26 | 27 | [[envs.hatch-test.matrix]] 28 | python = ["3.13", "3.12", "3.11", "3.10", "3.9"] 29 | 30 | [envs.test-build] 31 | dependencies = [ 32 | "pyawaitable_test_meson @ {root:uri}/tests/builds/meson", 33 | "pyawaitable_test_sbc @ {root:uri}/tests/builds/scikit-build-core", 34 | ] 35 | 36 | [envs.test-build.scripts] 37 | meson = "python3 tests/builds/ensure_build_worked.py _meson_module" 38 | scikit-build-core = "python3 tests/builds/ensure_build_worked.py _sbc_module" 39 | -------------------------------------------------------------------------------- /hatch_build.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyAwaitable Vendoring Script 3 | """ 4 | 5 | import os 6 | from pathlib import Path 7 | from typing import Callable, TextIO, TypeVar 8 | 9 | try: 10 | from typing import ParamSpec 11 | except ImportError: 12 | # Let's hope it's installed! 13 | from typing_extensions import ParamSpec 14 | import re 15 | from contextlib import contextmanager 16 | import functools 17 | import textwrap 18 | 19 | try: 20 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface 21 | except ImportError: 22 | 23 | class BuildHookInterface: 24 | pass 25 | 26 | 27 | DIST_PATH: str = "src/pyawaitable/pyawaitable.h" 28 | HEADER_FILES: list[str] = [ 29 | "optimize.h", 30 | "dist.h", 31 | "array.h", 32 | "backport.h", 33 | "coro.h", 34 | "awaitableobject.h", 35 | "genwrapper.h", 36 | "values.h", 37 | "with.h", 38 | "init.h", 39 | ] 40 | SOURCE_FILES: list[Path] = [ 41 | Path("./src/_pyawaitable/array.c"), 42 | Path("./src/_pyawaitable/coro.c"), 43 | Path("./src/_pyawaitable/awaitable.c"), 44 | Path("./src/_pyawaitable/genwrapper.c"), 45 | Path("./src/_pyawaitable/values.c"), 46 | Path("./src/_pyawaitable/with.c"), 47 | Path("./src/_pyawaitable/init.c"), 48 | ] 49 | 50 | INCLUDE_REGEX = re.compile(r"#include <(.+)>") 51 | FUNCTION_REGEX = re.compile(r"(.+)\(.*\).*") 52 | INTERNAL_FUNCTION_REGEX = re.compile( 53 | r"_PyAwaitable_INTERNAL\(.+\)\n(.+)\(.*\).*" 54 | ) 55 | INTERNAL_DATA_REGEX = re.compile(r"_PyAwaitable_INTERNAL_DATA\(.+\) (.+)") 56 | EXPLICIT_REGEX = re.compile(r".*_PyAwaitable_MANGLE\((.+)\).*") 57 | NO_EXPLICIT_REGEX = re.compile(r".*_PyAwaitable_NO_MANGLE\((.+)\).*") 58 | DEFINE_REGEX = re.compile(r" *# *define *(\w+)(\(.*\))?.*") 59 | 60 | HEADER_GUARD = """ 61 | #ifndef PYAWAITABLE_VENDOR_H 62 | #define PYAWAITABLE_VENDOR_H 63 | #define _PYAWAITABLE_VENDOR 64 | 65 | #ifdef Py_LIMITED_API 66 | #error "Sorry, the limited API cannot be used with PyAwaitable." 67 | #endif 68 | """ 69 | HEADER = ( 70 | lambda version: f"""\ 71 | /* 72 | * PyAwaitable - Autogenerated distribution copy of version {version} 73 | * 74 | * Docs: https://pyawaitable.zintensity.dev 75 | * Source: https://github.com/ZeroIntensity/pyawaitable 76 | */ 77 | """ 78 | ) 79 | MANGLED = "__PyAwaitable_Mangled_" 80 | 81 | _LOG_NEST = 0 82 | 83 | 84 | @contextmanager 85 | def logging_context(): 86 | global _LOG_NEST 87 | assert _LOG_NEST >= 0 88 | try: 89 | _LOG_NEST += 1 90 | yield 91 | finally: 92 | _LOG_NEST -= 1 93 | 94 | 95 | T = TypeVar("T") 96 | P = ParamSpec("P") 97 | 98 | 99 | def new_context(message: str) -> Callable[[Callable[P, T]], Callable[P, T]]: 100 | def decorator_factory(func: Callable[P, T]) -> Callable[P, T]: 101 | @functools.wraps(func) 102 | def decorator(*args: P.args, **kwargs: P.kwargs): 103 | log(message) 104 | with logging_context(): 105 | return func(*args, **kwargs) 106 | 107 | return decorator 108 | 109 | return decorator_factory 110 | 111 | 112 | def log(*text: str) -> None: 113 | indent = " " * _LOG_NEST 114 | print(indent + " ".join(text)) 115 | 116 | 117 | def write(fp: TextIO, value: str) -> None: 118 | fp.write(value + "\n") 119 | log(f"Wrote {len(value.encode('utf-8'))} bytes to {fp.name}") 120 | 121 | 122 | @new_context("Finding includes...") 123 | def find_includes(lines: list[str], includes: set[str]) -> None: 124 | for line in lines.copy(): 125 | match = INCLUDE_REGEX.match(line) 126 | if match: 127 | lines.remove(line) 128 | include = match.group(1) 129 | if include.startswith("pyawaitable/"): 130 | # Won't exist in the vendor 131 | continue 132 | 133 | includes.add(include) 134 | 135 | 136 | @new_context("Finding source file macros...") 137 | def find_defines(lines: list[str], defines: set[str]) -> None: 138 | for line in lines: 139 | match = DEFINE_REGEX.match(line) 140 | if not match: 141 | continue 142 | 143 | defines.add(match.group(1)) 144 | 145 | 146 | def filter_name(name: str) -> bool: 147 | return name.startswith("__") 148 | 149 | 150 | @new_context("Processing explicitly marked name...") 151 | def mangle_explicit(changed_names: dict[str, str], line: str) -> None: 152 | explicit = EXPLICIT_REGEX.match(line) 153 | if explicit is None: 154 | raise RuntimeError( 155 | f"{line} does not follow _PyAwaitable_MANGLE correctly" 156 | ) 157 | 158 | name = explicit.group(1) 159 | if filter_name(name): 160 | return 161 | changed_names[name] = MANGLED + name 162 | log(f"Marked {name} for mangling") 163 | 164 | 165 | @new_context("Processing _PyAwaitable_INTERNAL function...") 166 | def mangle_internal( 167 | changed_names: dict[str, str], lines: list[str], index: int 168 | ) -> None: 169 | try: 170 | func_def = INTERNAL_FUNCTION_REGEX.match( 171 | lines[index] + "\n" + lines[index + 1] 172 | ) 173 | except IndexError: 174 | return 175 | 176 | if func_def is None: 177 | return 178 | 179 | name = func_def.group(1) 180 | if filter_name(name): 181 | return 182 | changed_names[name] = "__PyAwaitable_Internal_" + name 183 | log(f"Marked {name} for mangling") 184 | 185 | 186 | @new_context("Processing internal data...") 187 | def mangle_internal_data(changed_names: dict[str, str], line: str) -> None: 188 | internal_data = INTERNAL_DATA_REGEX.match(line) 189 | if internal_data is None: 190 | raise RuntimeError( 191 | f"{line} does not follow _PyAwaitable_INTERNAL_DATA correctly" 192 | ) 193 | 194 | name = internal_data.group(1) 195 | changed_names[name] = "__PyAwaitable_InternalData_" + name 196 | log(f"Marked {name} for mangling") 197 | 198 | 199 | @new_context("Processing static function...") 200 | def mangle_static( 201 | changed_names: dict[str, str], lines: list[str], index: int 202 | ) -> None: 203 | try: 204 | line = lines[index + 1] 205 | except IndexError: 206 | return 207 | 208 | if NO_EXPLICIT_REGEX.match(line) is not None: 209 | return 210 | 211 | func_def = FUNCTION_REGEX.match(line) 212 | 213 | if func_def is None: 214 | return 215 | 216 | name = func_def.group(1) 217 | if filter_name(name): 218 | return 219 | changed_names[name] = "__PyAwaitable_Static_" + name 220 | log(f"Marked {name} for mangling") 221 | 222 | 223 | @new_context("Calculating mangled names...") 224 | def mangle_names(changed_names: dict[str, str], lines: list[str]) -> None: 225 | for index, line in enumerate(lines): 226 | if line.startswith("#define"): 227 | continue 228 | 229 | if "_PyAwaitable_MANGLE" in line: 230 | mangle_explicit(changed_names, line) 231 | elif "_PyAwaitable_INTERNAL" in line: 232 | mangle_internal(changed_names, lines, index) 233 | elif "_PyAwaitable_INTERNAL_DATA" in line: 234 | mangle_internal_data(changed_names, line) 235 | elif line.startswith("static"): 236 | mangle_static(changed_names, lines, index) 237 | 238 | 239 | def orderize_mangled(changed_names: dict[str, str]) -> dict[str, str]: 240 | result: dict[str, str] = {} 241 | orders: list[tuple[str, int]] = [] 242 | 243 | for name in changed_names.keys(): 244 | # Count how many times other keys go into name 245 | amount = 0 246 | for second_name in changed_names.keys(): 247 | if second_name in name: 248 | amount += 1 249 | 250 | orders.append((name, amount)) 251 | 252 | orders.sort(key=lambda item: item[1]) 253 | for index, data in enumerate(orders.copy()): 254 | name, amount = data 255 | if changed_names[name].startswith(MANGLED): 256 | # Always do explicit mangles last 257 | orders.insert(0, orders.pop(index)) 258 | 259 | for name, amount in reversed(orders): 260 | result[name] = changed_names[name] 261 | 262 | return result 263 | 264 | 265 | DOUBLE_MANGLE = "__PyAwaitable_Mangled___PyAwaitable_Mangled_" 266 | 267 | 268 | def clean_mangled(text: str) -> str: 269 | return text.replace(DOUBLE_MANGLE, MANGLED) 270 | 271 | 272 | def process_files(fp: TextIO) -> None: 273 | includes = set[str]() 274 | to_write: list[str] = [] 275 | log("Processing header files...") 276 | changed_names: dict[str, str] = {} 277 | source_macros: set[str] = set() 278 | 279 | with logging_context(): 280 | for header_file in HEADER_FILES: 281 | header_file = "include/pyawaitable" / Path(header_file) 282 | log(f"Processing {header_file}") 283 | lines: list[str] = header_file.read_text(encoding="utf-8").split( 284 | "\n" 285 | ) 286 | find_includes(lines, includes) 287 | mangle_names(changed_names, lines) 288 | to_write.append("\n".join(lines)) 289 | 290 | log("Processing source files...") 291 | with logging_context(): 292 | for source_file in SOURCE_FILES: 293 | lines: list[str] = source_file.read_text(encoding="utf-8").split( 294 | "\n" 295 | ) 296 | log(f"Processing {source_file}") 297 | find_includes(lines, includes) 298 | mangle_names(changed_names, lines) 299 | find_defines(lines, source_macros) 300 | to_write.append("\n".join(lines)) 301 | 302 | log("Writing macros...") 303 | with logging_context(): 304 | for include in includes: 305 | assert not include.startswith( 306 | "pyawaitable/" 307 | ), "found pyawaitable headers somehow" 308 | write(fp, f"#include <{include}>") 309 | 310 | log("Writing mangled names...") 311 | with logging_context(): 312 | for name, new_name in orderize_mangled(changed_names).items(): 313 | for index, line in enumerate(to_write): 314 | to_write[index] = clean_mangled(line.replace(name, new_name)) 315 | 316 | for line in to_write: 317 | write(fp, line) 318 | 319 | log("Writing macro cleanup...") 320 | with logging_context(): 321 | for define in source_macros: 322 | write(fp, f"#undef {define}") 323 | 324 | 325 | FINAL = "0xF" 326 | BETA = "0xB" 327 | ALPHA = "0xA" 328 | RELEASE_LEVEL = re.compile(r"([A-z]+)([0-9])*") 329 | RELEASE_LEVELS = { 330 | "dev": ALPHA, 331 | "alpha": ALPHA, 332 | "a": ALPHA, 333 | "beta": BETA, 334 | "b": BETA, 335 | } 336 | 337 | 338 | def deduce_release_level(part: str) -> tuple[str, str]: 339 | part = part.replace(".", "-") 340 | dev = part.split("-", maxsplit=1) 341 | if len(dev) == 1: 342 | # No release level attached, assume final release 343 | return FINAL, "0" 344 | 345 | release = dev[1] 346 | match = RELEASE_LEVEL.match(release) 347 | if not match: 348 | raise RuntimeError(f"version did not match expression: {release}") 349 | 350 | name = match.group(1).lower() 351 | level = RELEASE_LEVELS.get(name) 352 | if not level: 353 | raise RuntimeError(f"{name} is not a valid release level") 354 | number = match.group(2) 355 | 356 | # Sanity check 357 | if number and not number.isdigit(): 358 | raise RuntimeError(f"{number} is not a valid number") 359 | 360 | if number == "": 361 | number = "0" 362 | 363 | amount = 2 if level == BETA else 3 364 | number = ("0" * amount) + number 365 | 366 | return level, number 367 | 368 | 369 | def clean_micro_version(micro: str) -> str: 370 | return micro.replace(".", "-").split("-", maxsplit=1)[0] 371 | 372 | 373 | def main(version: str) -> None: 374 | dist = Path(DIST_PATH) 375 | if dist.exists(): 376 | log(f"{dist} already exists, removing it...") 377 | os.remove(dist) 378 | log("Creating vendored copy of pyawaitable...") 379 | 380 | major, minor, micro = version.split(".", maxsplit=2) 381 | release_level, release_number = deduce_release_level(micro) 382 | micro = clean_micro_version(micro) 383 | 384 | version_text = textwrap.dedent( 385 | f""" 386 | #define PyAwaitable_MAJOR_VERSION {major} 387 | #define PyAwaitable_MINOR_VERSION {minor} 388 | #define PyAwaitable_MICRO_VERSION {micro} 389 | #define PyAwaitable_PATCH_VERSION PyAwaitable_MICRO_VERSION 390 | #define PyAwaitable_RELEASE_LEVEL {release_level} 391 | #define PyAwaitable_MAGIC_NUMBER {major}{minor}{micro}{release_number} 392 | """ 393 | ) 394 | 395 | with open(dist, "w", encoding="utf-8") as f: 396 | with logging_context(): 397 | write(f, HEADER(version) + HEADER_GUARD + version_text) 398 | process_files(f) 399 | write( 400 | f, 401 | "#endif /* PYAWAITABLE_VENDOR_H */", 402 | ) 403 | 404 | log(f"Created PyAwaitable distribution at {dist}") 405 | 406 | 407 | class CustomBuildHook(BuildHookInterface): 408 | PLUGIN_NAME = "PyAwaitable Build" 409 | 410 | def clean(self, _: list[str]) -> None: 411 | dist = Path(DIST_PATH) 412 | if dist.exists(): 413 | os.remove(dist) 414 | 415 | def initialize(self, _: str, build_data: dict) -> None: 416 | self.clean([]) 417 | main(self.metadata.version) 418 | build_data["force_include"][DIST_PATH] = DIST_PATH 419 | 420 | 421 | if __name__ == "__main__": 422 | from src.pyawaitable import __version__ 423 | 424 | main(__version__) 425 | -------------------------------------------------------------------------------- /include/pyawaitable/array.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_ARRAY_H 2 | #define PYAWAITABLE_ARRAY_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #define _pyawaitable_array_DEFAULT_SIZE 16 11 | 12 | /* 13 | * Deallocator for items on a pyawaitable_array structure. A NULL pointer 14 | * will never be given to the deallocator. 15 | */ 16 | typedef void (*_PyAwaitable_MANGLE(pyawaitable_array_deallocator))(void *); 17 | 18 | /* 19 | * Internal only dynamic array for PyAwaitable. 20 | */ 21 | typedef struct { 22 | /* 23 | * The actual items in the dynamic array. 24 | * Don't access this field publicly to get 25 | * items--use pyawaitable_array_GET_ITEM() instead. 26 | */ 27 | void **items; 28 | /* 29 | * The length of the actual items array allocation. 30 | */ 31 | Py_ssize_t capacity; 32 | /* 33 | * The number of items in the array. 34 | * Don't use this field publicly--use pyawaitable_array_LENGTH() 35 | */ 36 | Py_ssize_t length; 37 | /* 38 | * The deallocator, set by one of the initializer functions. 39 | * This may be NULL. 40 | */ 41 | pyawaitable_array_deallocator deallocator; 42 | } _PyAwaitable_MANGLE(pyawaitable_array); 43 | 44 | 45 | /* Zero out the array */ 46 | static inline void 47 | pyawaitable_array_ZERO(pyawaitable_array *array) 48 | { 49 | assert(array != NULL); 50 | array->deallocator = NULL; 51 | array->items = NULL; 52 | array->length = 0; 53 | array->capacity = 0; 54 | } 55 | 56 | static inline void 57 | pyawaitable_array_ASSERT_VALID(pyawaitable_array *array) 58 | { 59 | assert(array != NULL); 60 | assert(array->items != NULL); 61 | } 62 | 63 | static inline void 64 | pyawaitable_array_ASSERT_INDEX(pyawaitable_array *array, Py_ssize_t index) 65 | { 66 | // Ensure the index is valid 67 | assert(index < array->length); 68 | assert(index >= 0); 69 | } 70 | 71 | /* 72 | * Initialize a dynamic array with an initial size and deallocator. 73 | * 74 | * If the deallocator is NULL, then nothing happens to items upon 75 | * removal and upon array clearing. 76 | * 77 | * Returns -1 upon failure, 0 otherwise. 78 | */ 79 | _PyAwaitable_INTERNAL(int) 80 | pyawaitable_array_init_with_size( 81 | pyawaitable_array * array, 82 | pyawaitable_array_deallocator deallocator, 83 | Py_ssize_t initial 84 | ); 85 | 86 | /* 87 | * Append to the array. 88 | * 89 | * Returns -1 upon failure, 0 otherwise. 90 | * If this fails, the deallocator is not ran on the item. 91 | */ 92 | _PyAwaitable_INTERNAL(int) 93 | pyawaitable_array_append(pyawaitable_array * array, void *item); 94 | 95 | /* 96 | * Insert an item at the target index. The index 97 | * must currently be a valid index in the array. 98 | * 99 | * Returns -1 upon failure, 0 otherwise. 100 | * If this fails, the deallocator is not ran on the item. 101 | */ 102 | _PyAwaitable_INTERNAL(int) 103 | pyawaitable_array_insert( 104 | pyawaitable_array * array, 105 | Py_ssize_t index, 106 | void *item 107 | ); 108 | 109 | /* Remove all items from the array. */ 110 | _PyAwaitable_INTERNAL(void) 111 | pyawaitable_array_clear_items(pyawaitable_array * array); 112 | 113 | /* 114 | * Clear all the fields on the array. 115 | * 116 | * Note that this does *not* free the actual dynamic array 117 | * structure--use pyawaitable_array_free() for that. 118 | * 119 | * It's safe to call pyawaitable_array_init() or init_with_size() again 120 | * on the array after calling this. 121 | */ 122 | _PyAwaitable_INTERNAL(void) 123 | pyawaitable_array_clear(pyawaitable_array * array); 124 | 125 | /* 126 | * Set a value at index in the array. 127 | * 128 | * If an item already exists at the target index, the deallocator 129 | * is called on it, if the array has one set. 130 | * 131 | * This cannot fail. 132 | */ 133 | _PyAwaitable_INTERNAL(void) 134 | pyawaitable_array_set(pyawaitable_array * array, Py_ssize_t index, void *item); 135 | 136 | /* 137 | * Remove the item at the index, and call the deallocator on it (if the array 138 | * has one set). 139 | * 140 | * This cannot fail. 141 | */ 142 | _PyAwaitable_INTERNAL(void) 143 | pyawaitable_array_remove(pyawaitable_array * array, Py_ssize_t index); 144 | 145 | /* 146 | * Remove the item at the index *without* deallocating it, and 147 | * return the item. 148 | * 149 | * This cannot fail. 150 | */ 151 | _PyAwaitable_INTERNAL(void *) 152 | pyawaitable_array_pop(pyawaitable_array * array, Py_ssize_t index); 153 | 154 | /* 155 | * Clear all the fields on a dynamic array, and then 156 | * free the dynamic array structure itself. 157 | * 158 | * The array must have been created by pyawaitable_array_new() 159 | */ 160 | static inline void 161 | pyawaitable_array_free(pyawaitable_array *array) 162 | { 163 | pyawaitable_array_ASSERT_VALID(array); 164 | pyawaitable_array_clear(array); 165 | PyMem_RawFree(array); 166 | } 167 | 168 | /* 169 | * Equivalent to pyawaitable_array_init_with_size() with a default size of 16. 170 | * 171 | * Returns -1 upon failure, 0 otherwise. 172 | */ 173 | static inline int 174 | pyawaitable_array_init( 175 | pyawaitable_array *array, 176 | pyawaitable_array_deallocator deallocator 177 | ) 178 | { 179 | return pyawaitable_array_init_with_size( 180 | array, 181 | deallocator, 182 | _pyawaitable_array_DEFAULT_SIZE 183 | ); 184 | } 185 | 186 | /* 187 | * Allocate and create a new dynamic array on the heap. 188 | * 189 | * The returned pointer should be freed with pyawaitable_array_free() 190 | * If this function fails, it returns NULL. 191 | */ 192 | static inline pyawaitable_array * 193 | pyawaitable_array_new_with_size( 194 | pyawaitable_array_deallocator deallocator, 195 | Py_ssize_t initial 196 | ) 197 | { 198 | pyawaitable_array *array = PyMem_Malloc(sizeof(pyawaitable_array)); 199 | if (PyAwaitable_UNLIKELY(array == NULL)) { 200 | return NULL; 201 | } 202 | 203 | if (pyawaitable_array_init_with_size(array, deallocator, initial) < 0) { 204 | PyMem_Free(array); 205 | return NULL; 206 | } 207 | 208 | pyawaitable_array_ASSERT_VALID(array); // Sanity check 209 | return array; 210 | } 211 | 212 | /* 213 | * Equivalent to pyawaitable_array_new_with_size() with a size of 16. 214 | * 215 | * The returned array must be freed with pyawaitable_array_free(). 216 | * Returns NULL on failure. 217 | */ 218 | static inline pyawaitable_array * 219 | pyawaitable_array_new(pyawaitable_array_deallocator deallocator) 220 | { 221 | return pyawaitable_array_new_with_size( 222 | deallocator, 223 | _pyawaitable_array_DEFAULT_SIZE 224 | ); 225 | } 226 | 227 | /* 228 | * Get an item from the array. This cannot fail. 229 | * 230 | * If the index is not valid, this is undefined behavior. 231 | */ 232 | static inline void * 233 | pyawaitable_array_GET_ITEM(pyawaitable_array *array, Py_ssize_t index) 234 | { 235 | pyawaitable_array_ASSERT_VALID(array); 236 | pyawaitable_array_ASSERT_INDEX(array, index); 237 | return array->items[index]; 238 | } 239 | 240 | /* 241 | * Get the length of the array. This cannot fail. 242 | */ 243 | static inline Py_ssize_t PyAwaitable_PURE 244 | pyawaitable_array_LENGTH(pyawaitable_array *array) 245 | { 246 | pyawaitable_array_ASSERT_VALID(array); 247 | return array->length; 248 | } 249 | 250 | /* 251 | * Pop the item at the end the array. 252 | * This function cannot fail. 253 | */ 254 | static inline void * 255 | pyawaitable_array_pop_top(pyawaitable_array *array) 256 | { 257 | return pyawaitable_array_pop(array, pyawaitable_array_LENGTH(array) - 1); 258 | } 259 | 260 | #endif 261 | -------------------------------------------------------------------------------- /include/pyawaitable/awaitableobject.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_AWAITABLE_H 2 | #define PYAWAITABLE_AWAITABLE_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | typedef int (*PyAwaitable_Callback)(PyObject *, PyObject *); 11 | typedef int (*PyAwaitable_Error)(PyObject *, PyObject *); 12 | typedef int (*PyAwaitable_Defer)(PyObject *); 13 | 14 | typedef struct _pyawaitable_callback { 15 | PyObject *coro; 16 | PyAwaitable_Callback callback; 17 | PyAwaitable_Error err_callback; 18 | bool done; 19 | } _PyAwaitable_MANGLE(pyawaitable_callback); 20 | 21 | struct _PyAwaitableObject { 22 | PyObject_HEAD 23 | 24 | pyawaitable_array aw_callbacks; 25 | pyawaitable_array aw_object_values; 26 | pyawaitable_array aw_arbitrary_values; 27 | 28 | /* Index of current callback */ 29 | Py_ssize_t aw_state; 30 | /* Is the awaitable done? */ 31 | bool aw_done; 32 | /* Was the awaitable awaited? */ 33 | bool aw_awaited; 34 | /* Strong reference to the result of the coroutine. */ 35 | PyObject *aw_result; 36 | /* Strong reference to the genwrapper. */ 37 | PyObject *aw_gen; 38 | /* Set to 1 if the object was cancelled, for introspection against callbacks */ 39 | int aw_recently_cancelled; 40 | }; 41 | 42 | typedef struct _PyAwaitableObject PyAwaitableObject; 43 | _PyAwaitable_INTERNAL_DATA(PyTypeObject) PyAwaitable_Type; 44 | 45 | _PyAwaitable_API(int) 46 | PyAwaitable_SetResult(PyObject * awaitable, PyObject * result); 47 | 48 | _PyAwaitable_API(int) 49 | PyAwaitable_AddAwait( 50 | PyObject * aw, 51 | PyObject * coro, 52 | PyAwaitable_Callback cb, 53 | PyAwaitable_Error err 54 | ); 55 | 56 | _PyAwaitable_API(int) 57 | PyAwaitable_DeferAwait(PyObject * aw, PyAwaitable_Defer cb); 58 | 59 | _PyAwaitable_API(void) 60 | PyAwaitable_Cancel(PyObject * aw); 61 | 62 | _PyAwaitable_INTERNAL(PyObject *) 63 | awaitable_next(PyObject * self); 64 | 65 | _PyAwaitable_API(PyObject *) 66 | PyAwaitable_New(void); 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /include/pyawaitable/backport.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_BACKPORT_H 2 | #define PYAWAITABLE_BACKPORT_H 3 | 4 | #include 5 | #include 6 | 7 | #ifndef Py_NewRef 8 | static inline PyObject * 9 | _PyAwaitable_NO_MANGLE(Py_NewRef)(PyObject *o) 10 | { 11 | Py_INCREF(o); 12 | return o; 13 | } 14 | 15 | #endif 16 | 17 | #ifndef Py_XNewRef 18 | static inline PyObject * 19 | _PyAwaitable_NO_MANGLE(Py_XNewRef)(PyObject *o) 20 | { 21 | Py_XINCREF(o); 22 | return o; 23 | } 24 | #endif 25 | 26 | #if PY_VERSION_HEX < 0x030c0000 27 | static PyObject * 28 | _PyAwaitable_NO_MANGLE(PyErr_GetRaisedException)(void) 29 | { 30 | PyObject *type, *val, *tb; 31 | PyErr_Fetch(&type, &val, &tb); 32 | PyErr_NormalizeException(&type, &val, &tb); 33 | Py_XDECREF(type); 34 | Py_XDECREF(tb); 35 | // technically some entry in the traceback might be lost; ignore that 36 | return val; 37 | } 38 | 39 | static void 40 | _PyAwaitable_NO_MANGLE(PyErr_SetRaisedException)(PyObject *err) 41 | { 42 | // NOTE: We need to incref the type object here, even though 43 | // this function steals a reference to err. 44 | PyErr_Restore(Py_NewRef((PyObject *) Py_TYPE(err)), err, NULL); 45 | } 46 | #endif 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /include/pyawaitable/coro.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_CORO_H 2 | #define PYAWAITABLE_CORO_H 3 | 4 | #include 5 | #include 6 | 7 | #ifndef _PYAWAITABLE_VENDOR 8 | _PyAwaitable_INTERNAL_DATA(PyMethodDef) pyawaitable_methods[]; 9 | #endif 10 | _PyAwaitable_INTERNAL_DATA(PyAsyncMethods) pyawaitable_async_methods; 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /include/pyawaitable/dist.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_DIST_H 2 | #define PYAWAITABLE_DIST_H 3 | 4 | #if PY_MINOR_VERSION < 9 5 | #error \ 6 | "Python 3.8 and older are no longer supported, please use Python 3.9 or newer." 7 | #endif 8 | 9 | #ifdef _PYAWAITABLE_VENDOR 10 | #define _PyAwaitable_API(ret) static ret 11 | #define _PyAwaitable_INTERNAL(ret) static ret 12 | #define _PyAwaitable_INTERNAL_DATA(tp) static tp 13 | #define _PyAwaitable_INTERNAL_DATA_DEF(tp) static tp 14 | #else 15 | /* These are for IDEs */ 16 | #define _PyAwaitable_API(ret) ret 17 | #define _PyAwaitable_INTERNAL(ret) ret 18 | #define _PyAwaitable_INTERNAL_DATA(tp) extern tp 19 | #define _PyAwaitable_INTERNAL_DATA_DEF(tp) tp 20 | #define PyAwaitable_MAGIC_NUMBER 0 21 | #endif 22 | 23 | #define _PyAwaitable_MANGLE(name) name 24 | #define _PyAwaitable_NO_MANGLE(name) name 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /include/pyawaitable/genwrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_GENWRAPPER_H 2 | #define PYAWAITABLE_GENWRAPPER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | _PyAwaitable_INTERNAL_DATA(PyTypeObject) _PyAwaitableGenWrapperType; 9 | 10 | typedef struct _GenWrapperObject { 11 | PyObject_HEAD 12 | PyAwaitableObject *gw_aw; 13 | PyObject *gw_current_await; 14 | } _PyAwaitable_MANGLE(GenWrapperObject); 15 | 16 | _PyAwaitable_INTERNAL(PyObject *) 17 | _PyAwaitableGenWrapper_Next(PyObject * self); 18 | 19 | _PyAwaitable_INTERNAL(int) 20 | _PyAwaitableGenWrapper_FireErrCallback( 21 | PyObject * self, 22 | PyAwaitable_Error err_callback 23 | ); 24 | 25 | _PyAwaitable_INTERNAL(PyObject *) 26 | genwrapper_new(PyAwaitableObject * aw); 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /include/pyawaitable/init.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_INIT_H 2 | #define PYAWAITABLE_INIT_H 3 | 4 | #include 5 | #include 6 | 7 | _PyAwaitable_INTERNAL(PyObject *) 8 | _PyAwaitable_GetState(void); 9 | 10 | _PyAwaitable_API(PyTypeObject *) 11 | PyAwaitable_GetType(void); 12 | 13 | _PyAwaitable_INTERNAL(PyTypeObject *) 14 | _PyAwaitable_GetGenWrapperType(void); 15 | 16 | _PyAwaitable_API(int) 17 | PyAwaitable_Init(void); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /include/pyawaitable/optimize.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_OPTIMIZE_H 2 | #define PYAWAITABLE_OPTIMIZE_H 3 | 4 | #if (defined(__GNUC__) && __GNUC__ >= 15) || defined(__clang__) && \ 5 | __clang__ >= 13 6 | #define PyAwaitable_MUSTTAIL [[clang::musttail]] 7 | #else 8 | #define PyAwaitable_MUSTTAIL 9 | #endif 10 | 11 | #if defined(__GNUC__) || defined(__clang__) 12 | /* Called often */ 13 | #define PyAwaitable_HOT __attribute__((hot)) 14 | /* Depends only on input and memory state (i.e. makes no memory allocations */ 15 | #define PyAwaitable_PURE __attribute__((pure)) 16 | /* Depends only on inputs */ 17 | #define PyAwaitable_CONST __attribute__((const)) 18 | /* Called rarely */ 19 | #define PyAwaitable_COLD __attribute__((cold)) 20 | #else 21 | #define PyAwaitable_HOT 22 | #define PyAwaitable_PURE 23 | #define PyAwaitable_CONST 24 | #define PyAwaitable_COLD 25 | #endif 26 | 27 | #if defined(__GNUC__) || defined(__clang__) 28 | #include 29 | #define PyAwaitable_UNLIKELY(x) (__builtin_expect(!!(x), false)) 30 | #define PyAwaitable_LIKELY(x) (__builtin_expect(!!(x), true)) 31 | #elif (defined(__cplusplus) && (__cplusplus >= 202002L)) || \ 32 | (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) 33 | #define PyAwaitable_UNLIKELY(x) (x)[[unlikely]] 34 | #define PyAwaitable_LIKELY(x) (x)[[likely]] 35 | #else 36 | #define PyAwaitable_UNLIKELY(x) (x) 37 | #define PyAwaitable_LIKELY(x) (x) 38 | #endif 39 | 40 | #ifdef thread_local 41 | # define PyAwaitable_thread_local thread_local 42 | #elif __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) 43 | # define PyAwaitable_thread_local _Thread_local 44 | #elif defined(_MSC_VER) /* AKA NT_THREADS */ 45 | # define PyAwaitable_thread_local __declspec(thread) 46 | #elif defined(__GNUC__) /* includes clang */ 47 | # define PyAwaitable_thread_local __thread 48 | # else 49 | #error \ 50 | "no thread-local storage classifier is available" 51 | #endif 52 | #endif 53 | -------------------------------------------------------------------------------- /include/pyawaitable/values.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_VALUES_H 2 | #define PYAWAITABLE_VALUES_H 3 | 4 | #include // PyObject, Py_ssize_t 5 | #include 6 | 7 | /* Object values */ 8 | 9 | _PyAwaitable_API(int) 10 | PyAwaitable_SaveValues( 11 | PyObject * awaitable, 12 | Py_ssize_t nargs, 13 | ... 14 | ); 15 | 16 | _PyAwaitable_API(int) 17 | PyAwaitable_UnpackValues(PyObject * awaitable, ...); 18 | 19 | _PyAwaitable_API(int) 20 | PyAwaitable_SetValue( 21 | PyObject * awaitable, 22 | Py_ssize_t index, 23 | PyObject * new_value 24 | ); 25 | _PyAwaitable_API(PyObject *) 26 | PyAwaitable_GetValue( 27 | PyObject * awaitable, 28 | Py_ssize_t index 29 | ); 30 | 31 | /* Arbitrary values */ 32 | 33 | _PyAwaitable_API(int) 34 | PyAwaitable_SaveArbValues( 35 | PyObject * awaitable, 36 | Py_ssize_t nargs, 37 | ... 38 | ); 39 | 40 | _PyAwaitable_API(int) 41 | PyAwaitable_UnpackArbValues(PyObject * awaitable, ...); 42 | 43 | _PyAwaitable_API(int) 44 | PyAwaitable_SetArbValue( 45 | PyObject * awaitable, 46 | Py_ssize_t index, 47 | void *new_value 48 | ); 49 | 50 | _PyAwaitable_API(void *) 51 | PyAwaitable_GetArbValue( 52 | PyObject * awaitable, 53 | Py_ssize_t index 54 | ); 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /include/pyawaitable/with.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_WITH_H 2 | #define PYAWAITABLE_WITH_H 3 | 4 | #include // PyObject 5 | #include // PyAwaitable_Callback, PyAwaitable_Error 6 | 7 | _PyAwaitable_API(int) 8 | PyAwaitable_AsyncWith( 9 | PyObject * aw, 10 | PyObject * ctx, 11 | PyAwaitable_Callback cb, 12 | PyAwaitable_Error err 13 | ); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PyAwaitable 2 | site_url: https://pyawaitable.zintensity.dev 3 | repo_url: https://github.com/ZeroIntensity/pyawaitable 4 | repo_name: ZeroIntensity/pyawaitable 5 | 6 | nav: 7 | - Home: index.md 8 | - Installation: installation.md 9 | - Making a C Function Asynchronous: c_async.md 10 | - Executing Asynchronous Calls from C: adding_awaits.md 11 | - Managing State Between Callbacks: value_storage.md 12 | 13 | theme: 14 | name: readthedocs 15 | 16 | markdown_extensions: 17 | - toc: 18 | permalink: true 19 | - admonition 20 | 21 | plugins: 22 | - search 23 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "mkdocs build" 3 | publish = "site" -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatch", "hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pyawaitable" 7 | description = "Call asynchronous code from an extension module." 8 | authors = [ 9 | { name = "Peter Bierma", email = "zintensitydev@gmail.com" }, 10 | ] 11 | readme = "README.md" 12 | license = { file = "LICENSE" } 13 | classifiers = [ 14 | "Programming Language :: Python", 15 | "Programming Language :: Python :: 3.9", 16 | "Programming Language :: Python :: 3.10", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | "Programming Language :: Python :: 3.13", 20 | "Programming Language :: Python :: Implementation :: CPython", 21 | ] 22 | dependencies = [] 23 | dynamic = ["version"] 24 | requires-python = ">=3.9" 25 | 26 | [project.urls] 27 | Documentation = "https://pyawaitable.zintensity.dev" 28 | Issues = "https://github.com/ZeroIntensity/pyawaitable/issues" 29 | Source = "https://github.com/ZeroIntensity/pyawaitable" 30 | 31 | [project.scripts] 32 | pyawaitable = "pyawaitable.__main__:main" 33 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Requirements for Netlify 2 | mkdocs 3 | pymdown-extensions -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | 3.9 2 | -------------------------------------------------------------------------------- /src/_pyawaitable/array.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static inline void 5 | call_deallocator_maybe(pyawaitable_array *array, Py_ssize_t index) 6 | { 7 | if (array->deallocator != NULL && array->items[index] != NULL) { 8 | array->deallocator(array->items[index]); 9 | array->items[index] = NULL; 10 | } 11 | } 12 | 13 | _PyAwaitable_INTERNAL(int) 14 | pyawaitable_array_init_with_size( 15 | pyawaitable_array * array, 16 | pyawaitable_array_deallocator deallocator, 17 | Py_ssize_t initial 18 | ) 19 | { 20 | assert(array != NULL); 21 | assert(initial > 0); 22 | void **items = PyMem_Calloc(sizeof(void *), initial); 23 | if (PyAwaitable_UNLIKELY(items == NULL)) { 24 | return -1; 25 | } 26 | 27 | array->capacity = initial; 28 | array->items = items; 29 | array->length = 0; 30 | array->deallocator = deallocator; 31 | 32 | return 0; 33 | } 34 | 35 | static int 36 | resize_if_needed(pyawaitable_array *array) 37 | { 38 | if (array->length == array->capacity) { 39 | // Need to resize 40 | array->capacity *= 2; 41 | void **new_items = PyMem_Realloc( 42 | array->items, 43 | sizeof(void *) * array->capacity 44 | ); 45 | if (PyAwaitable_UNLIKELY(new_items == NULL)) { 46 | return -1; 47 | } 48 | 49 | array->items = new_items; 50 | } 51 | 52 | return 0; 53 | } 54 | 55 | _PyAwaitable_INTERNAL(int) PyAwaitable_PURE 56 | pyawaitable_array_append(pyawaitable_array *array, void *item) 57 | { 58 | pyawaitable_array_ASSERT_VALID(array); 59 | array->items[array->length++] = item; 60 | if (resize_if_needed(array) < 0) { 61 | array->items[--array->length] = NULL; 62 | return -1; 63 | } 64 | return 0; 65 | } 66 | 67 | _PyAwaitable_INTERNAL(int) 68 | pyawaitable_array_insert( 69 | pyawaitable_array * array, 70 | Py_ssize_t index, 71 | void *item 72 | ) 73 | { 74 | pyawaitable_array_ASSERT_VALID(array); 75 | pyawaitable_array_ASSERT_INDEX(array, index); 76 | ++array->length; 77 | if (resize_if_needed(array) < 0) { 78 | // Grow the array beforehand, otherwise it's 79 | // going to be a mess putting it back together if 80 | // allocation fails. 81 | --array->length; 82 | return -1; 83 | } 84 | 85 | for (Py_ssize_t i = array->length - 1; i > index; --i) { 86 | array->items[i] = array->items[i - 1]; 87 | } 88 | 89 | array->items[index] = item; 90 | return 0; 91 | } 92 | 93 | _PyAwaitable_INTERNAL(void) 94 | pyawaitable_array_set(pyawaitable_array * array, Py_ssize_t index, void *item) 95 | { 96 | pyawaitable_array_ASSERT_VALID(array); 97 | pyawaitable_array_ASSERT_INDEX(array, index); 98 | call_deallocator_maybe(array, index); 99 | array->items[index] = item; 100 | } 101 | 102 | static void 103 | remove_no_dealloc(pyawaitable_array *array, Py_ssize_t index) 104 | { 105 | for (Py_ssize_t i = index; i < array->length - 1; ++i) { 106 | array->items[i] = array->items[i + 1]; 107 | } 108 | --array->length; 109 | } 110 | 111 | _PyAwaitable_INTERNAL(void) 112 | pyawaitable_array_remove(pyawaitable_array * array, Py_ssize_t index) 113 | { 114 | pyawaitable_array_ASSERT_VALID(array); 115 | pyawaitable_array_ASSERT_INDEX(array, index); 116 | call_deallocator_maybe(array, index); 117 | remove_no_dealloc(array, index); 118 | } 119 | 120 | _PyAwaitable_INTERNAL(void *) 121 | pyawaitable_array_pop(pyawaitable_array * array, Py_ssize_t index) 122 | { 123 | pyawaitable_array_ASSERT_VALID(array); 124 | pyawaitable_array_ASSERT_INDEX(array, index); 125 | void *item = array->items[index]; 126 | remove_no_dealloc(array, index); 127 | return item; 128 | } 129 | 130 | _PyAwaitable_INTERNAL(void) 131 | pyawaitable_array_clear_items(pyawaitable_array * array) 132 | { 133 | pyawaitable_array_ASSERT_VALID(array); 134 | for (Py_ssize_t i = 0; i < array->length; ++i) { 135 | call_deallocator_maybe(array, i); 136 | array->items[i] = NULL; 137 | } 138 | 139 | array->length = 0; 140 | } 141 | 142 | _PyAwaitable_INTERNAL(void) 143 | pyawaitable_array_clear(pyawaitable_array * array) 144 | { 145 | pyawaitable_array_ASSERT_VALID(array); 146 | pyawaitable_array_clear_items(array); 147 | PyMem_Free(array->items); 148 | 149 | // It would be nice if others could reuse the allocation for another 150 | // dynarray later, so clear all the fields. 151 | array->items = NULL; 152 | array->length = 0; 153 | array->capacity = 0; 154 | array->deallocator = NULL; 155 | } 156 | -------------------------------------------------------------------------------- /src/_pyawaitable/awaitable.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | static void 13 | callback_dealloc(void *ptr) 14 | { 15 | assert(ptr != NULL); 16 | pyawaitable_callback *cb = (pyawaitable_callback *) ptr; 17 | Py_CLEAR(cb->coro); 18 | PyMem_Free(cb); 19 | } 20 | 21 | static PyObject * 22 | awaitable_new_func(PyTypeObject *tp, PyObject *args, PyObject *kwds) 23 | { 24 | assert(tp != NULL); 25 | assert(tp->tp_alloc != NULL); 26 | 27 | PyObject *self = tp->tp_alloc(tp, 0); 28 | if (PyAwaitable_UNLIKELY(self == NULL)) { 29 | return NULL; 30 | } 31 | 32 | PyAwaitableObject *aw = (PyAwaitableObject *) self; 33 | aw->aw_gen = NULL; 34 | aw->aw_done = false; 35 | aw->aw_state = 0; 36 | aw->aw_result = NULL; 37 | aw->aw_recently_cancelled = 0; 38 | 39 | if (pyawaitable_array_init(&aw->aw_callbacks, callback_dealloc) < 0) { 40 | goto error; 41 | } 42 | 43 | if ( 44 | pyawaitable_array_init( 45 | &aw->aw_object_values, 46 | (pyawaitable_array_deallocator) Py_DecRef 47 | ) < 0 48 | ) { 49 | goto error; 50 | } 51 | 52 | if (pyawaitable_array_init(&aw->aw_arbitrary_values, NULL) < 0) { 53 | goto error; 54 | } 55 | 56 | return self; 57 | error: 58 | PyErr_NoMemory(); 59 | Py_DECREF(self); 60 | return NULL; 61 | } 62 | 63 | _PyAwaitable_INTERNAL(PyObject *) 64 | awaitable_next(PyObject * self) 65 | { 66 | PyAwaitableObject *aw = (PyAwaitableObject *)self; 67 | if (aw->aw_done) { 68 | PyErr_SetString( 69 | PyExc_RuntimeError, 70 | "PyAwaitable: Cannot reuse awaitable" 71 | ); 72 | return NULL; 73 | } 74 | aw->aw_awaited = true; 75 | PyObject *gen = genwrapper_new(aw); 76 | aw->aw_gen = Py_XNewRef(gen); 77 | return gen; 78 | } 79 | 80 | static int 81 | awaitable_traverse(PyObject *self, visitproc visit, void *arg) 82 | { 83 | PyAwaitableObject *aw = (PyAwaitableObject *)self; 84 | pyawaitable_array *array = &aw->aw_object_values; 85 | if (array->items != NULL) { 86 | for (Py_ssize_t i = 0; i < pyawaitable_array_LENGTH(array); ++i) { 87 | PyObject *ref = pyawaitable_array_GET_ITEM(array, i); 88 | Py_VISIT(ref); 89 | } 90 | } 91 | Py_VISIT(aw->aw_gen); 92 | Py_VISIT(aw->aw_result); 93 | return 0; 94 | } 95 | 96 | static int 97 | awaitable_clear(PyObject *self) 98 | { 99 | PyAwaitableObject *aw = (PyAwaitableObject *)self; 100 | pyawaitable_array *array = &aw->aw_object_values; 101 | if (array->items != NULL) { 102 | pyawaitable_array_clear(array); 103 | } 104 | Py_CLEAR(aw->aw_gen); 105 | Py_CLEAR(aw->aw_result); 106 | return 0; 107 | } 108 | 109 | static void 110 | awaitable_dealloc(PyObject *self) 111 | { 112 | PyAwaitableObject *aw = (PyAwaitableObject *)self; 113 | #define CLEAR_IF_NON_NULL(array) \ 114 | if (array.items != NULL) { \ 115 | pyawaitable_array_clear(&array); \ 116 | } 117 | CLEAR_IF_NON_NULL(aw->aw_callbacks); 118 | CLEAR_IF_NON_NULL(aw->aw_arbitrary_values); 119 | #undef CLEAR_IF_NON_NULL 120 | 121 | (void)awaitable_clear(self); 122 | 123 | if (!aw->aw_awaited) { 124 | if ( 125 | PyErr_WarnEx( 126 | PyExc_ResourceWarning, 127 | "PyAwaitable object was never awaited", 128 | 1 129 | ) < 0 130 | ) { 131 | PyErr_WriteUnraisable(self); 132 | } 133 | } 134 | 135 | Py_TYPE(self)->tp_free(self); 136 | } 137 | 138 | _PyAwaitable_API(void) 139 | PyAwaitable_Cancel(PyObject * self) 140 | { 141 | assert(self != NULL); 142 | PyAwaitableObject *aw = (PyAwaitableObject *) self; 143 | pyawaitable_array_clear_items(&aw->aw_callbacks); 144 | aw->aw_state = 0; 145 | if (aw->aw_gen != NULL) { 146 | GenWrapperObject *gw = (GenWrapperObject *)aw->aw_gen; 147 | Py_CLEAR(gw->gw_current_await); 148 | } 149 | 150 | aw->aw_recently_cancelled = 1; 151 | aw->aw_awaited = 1; 152 | } 153 | 154 | _PyAwaitable_API(int) 155 | PyAwaitable_AddAwait( 156 | PyObject * self, 157 | PyObject * coro, 158 | PyAwaitable_Callback cb, 159 | PyAwaitable_Error err 160 | ) 161 | { 162 | PyAwaitableObject *aw = (PyAwaitableObject *) self; 163 | assert(Py_IS_TYPE(Py_TYPE(self), PyAwaitable_GetType())); 164 | if (coro == NULL) { 165 | PyErr_SetString( 166 | PyExc_ValueError, 167 | "PyAwaitable: NULL passed to PyAwaitable_AddAwait()! " 168 | "Did you forget an error check?" 169 | ); 170 | return -1; 171 | } 172 | 173 | if (coro == self) { 174 | PyErr_Format( 175 | PyExc_ValueError, 176 | "PyAwaitable: Self (%R) was passed to PyAwaitable_AddAwait()! " 177 | "This would result in a recursive nightmare.", 178 | self 179 | ); 180 | return -1; 181 | } 182 | 183 | if (!PyObject_HasAttrString(coro, "__await__")) { 184 | PyErr_Format( 185 | PyExc_TypeError, 186 | "PyAwaitable: %R is not an awaitable object", 187 | coro 188 | ); 189 | return -1; 190 | } 191 | 192 | pyawaitable_callback *aw_c = PyMem_Malloc(sizeof(pyawaitable_callback)); 193 | if (aw_c == NULL) { 194 | PyErr_NoMemory(); 195 | return -1; 196 | } 197 | 198 | aw_c->coro = Py_NewRef(coro); 199 | aw_c->callback = cb; 200 | aw_c->err_callback = err; 201 | aw_c->done = false; 202 | 203 | if (pyawaitable_array_append(&aw->aw_callbacks, aw_c) < 0) { 204 | PyMem_Free(aw_c); 205 | PyErr_NoMemory(); 206 | return -1; 207 | } 208 | 209 | return 0; 210 | } 211 | 212 | _PyAwaitable_API(int) 213 | PyAwaitable_DeferAwait(PyObject * awaitable, PyAwaitable_Defer cb) 214 | { 215 | PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; 216 | assert(Py_IS_TYPE(Py_TYPE(awaitable), PyAwaitable_GetType())); 217 | pyawaitable_callback *aw_c = PyMem_Malloc(sizeof(pyawaitable_callback)); 218 | if (aw_c == NULL) { 219 | PyErr_NoMemory(); 220 | return -1; 221 | } 222 | 223 | aw_c->coro = NULL; 224 | aw_c->callback = (PyAwaitable_Callback)cb; 225 | aw_c->err_callback = NULL; 226 | aw_c->done = false; 227 | 228 | if (pyawaitable_array_append(&aw->aw_callbacks, aw_c) < 0) { 229 | PyMem_Free(aw_c); 230 | PyErr_NoMemory(); 231 | return -1; 232 | } 233 | 234 | return 0; 235 | } 236 | 237 | _PyAwaitable_API(int) 238 | PyAwaitable_SetResult(PyObject * awaitable, PyObject * result) 239 | { 240 | PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; 241 | assert(Py_IS_TYPE(Py_TYPE(awaitable), PyAwaitable_GetType())); 242 | aw->aw_result = Py_NewRef(result); 243 | return 0; 244 | } 245 | 246 | _PyAwaitable_API(PyObject *) 247 | PyAwaitable_New(void) 248 | { 249 | // XXX Use a freelist? 250 | PyTypeObject *type = PyAwaitable_GetType(); 251 | if (PyAwaitable_UNLIKELY(type == NULL)) { 252 | return NULL; 253 | } 254 | PyObject *result = awaitable_new_func(type, NULL, NULL); 255 | return result; 256 | } 257 | 258 | _PyAwaitable_INTERNAL_DATA_DEF(PyTypeObject) PyAwaitable_Type = { 259 | PyVarObject_HEAD_INIT(NULL, 0) 260 | .tp_name = "_PyAwaitableType", 261 | .tp_basicsize = sizeof(PyAwaitableObject), 262 | .tp_dealloc = awaitable_dealloc, 263 | .tp_as_async = &pyawaitable_async_methods, 264 | .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, 265 | .tp_doc = PyDoc_STR("Awaitable transport utility for the C API."), 266 | .tp_iternext = awaitable_next, 267 | .tp_new = awaitable_new_func, 268 | .tp_clear = awaitable_clear, 269 | .tp_traverse = awaitable_traverse, 270 | .tp_methods = pyawaitable_methods 271 | }; 272 | -------------------------------------------------------------------------------- /src/_pyawaitable/coro.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static PyObject * 9 | awaitable_send_with_arg(PyObject *self, PyObject *value) 10 | { 11 | PyAwaitableObject *aw = (PyAwaitableObject *) self; 12 | if (aw->aw_gen == NULL) { 13 | PyObject *gen = awaitable_next(self); 14 | if (PyAwaitable_UNLIKELY(gen == NULL)) { 15 | return NULL; 16 | } 17 | 18 | if (PyAwaitable_UNLIKELY(value != Py_None)) { 19 | PyErr_SetString( 20 | PyExc_RuntimeError, 21 | "can't send non-None value to a just-started awaitable" 22 | ); 23 | return NULL; 24 | } 25 | 26 | Py_DECREF(gen); 27 | Py_RETURN_NONE; 28 | } 29 | 30 | return _PyAwaitableGenWrapper_Next(aw->aw_gen); 31 | } 32 | 33 | static PyObject * 34 | awaitable_send(PyObject *self, PyObject *value) 35 | { 36 | return awaitable_send_with_arg(self, value); 37 | } 38 | 39 | static PyObject * 40 | awaitable_close(PyObject *self, PyObject *args) 41 | { 42 | PyAwaitable_Cancel(self); 43 | PyAwaitableObject *aw = (PyAwaitableObject *) self; 44 | aw->aw_done = true; 45 | Py_RETURN_NONE; 46 | } 47 | 48 | static PyObject * 49 | awaitable_throw(PyObject *self, PyObject *args) 50 | { 51 | PyObject *type; 52 | PyObject *value = NULL; 53 | PyObject *traceback = NULL; 54 | 55 | if (!PyArg_ParseTuple(args, "O|OO", &type, &value, &traceback)) { 56 | return NULL; 57 | } 58 | 59 | if (PyType_Check(type)) { 60 | PyObject *err = PyObject_CallOneArg(type, value); 61 | if (PyAwaitable_UNLIKELY(err == NULL)) { 62 | return NULL; 63 | } 64 | 65 | if (traceback != NULL) { 66 | if (PyException_SetTraceback(err, traceback) < 0) { 67 | Py_DECREF(err); 68 | return NULL; 69 | } 70 | } 71 | 72 | PyErr_Restore(err, NULL, NULL); 73 | } 74 | else { 75 | PyErr_Restore( 76 | Py_NewRef(type), 77 | Py_XNewRef(value), 78 | Py_XNewRef(traceback) 79 | ); 80 | } 81 | 82 | PyAwaitableObject *aw = (PyAwaitableObject *)self; 83 | if ((aw->aw_gen != NULL) && (aw->aw_state != 0)) { 84 | GenWrapperObject *gw = (GenWrapperObject *)aw->aw_gen; 85 | pyawaitable_callback *cb = 86 | pyawaitable_array_GET_ITEM(&aw->aw_callbacks, aw->aw_state - 1); 87 | if (cb == NULL) { 88 | return NULL; 89 | } 90 | 91 | if (_PyAwaitableGenWrapper_FireErrCallback( 92 | self, 93 | cb->err_callback 94 | ) < 0) { 95 | return NULL; 96 | } 97 | } 98 | else { 99 | return NULL; 100 | } 101 | 102 | assert(PyErr_Occurred()); 103 | return NULL; 104 | } 105 | 106 | #if PY_MINOR_VERSION > 9 107 | static PySendResult 108 | awaitable_am_send(PyObject *self, PyObject *arg, PyObject **presult) 109 | { 110 | PyObject *send_res = awaitable_send_with_arg(self, arg); 111 | if (send_res == NULL) { 112 | if (PyErr_ExceptionMatches(PyExc_StopIteration)) { 113 | PyObject *occurred = PyErr_GetRaisedException(); 114 | PyObject *item = PyObject_GetAttrString(occurred, "value"); 115 | Py_DECREF(occurred); 116 | 117 | if (PyAwaitable_UNLIKELY(item == NULL)) { 118 | return PYGEN_ERROR; 119 | } 120 | 121 | *presult = item; 122 | return PYGEN_RETURN; 123 | } 124 | *presult = NULL; 125 | return PYGEN_ERROR; 126 | } 127 | *presult = send_res; 128 | 129 | return PYGEN_NEXT; 130 | } 131 | 132 | #endif 133 | 134 | _PyAwaitable_INTERNAL_DATA_DEF(PyMethodDef) pyawaitable_methods[] = { 135 | {"send", awaitable_send, METH_O, NULL}, 136 | {"close", awaitable_close, METH_NOARGS, NULL}, 137 | {"throw", awaitable_throw, METH_VARARGS, NULL}, 138 | {NULL, NULL, 0, NULL} 139 | }; 140 | 141 | _PyAwaitable_INTERNAL_DATA_DEF(PyAsyncMethods) pyawaitable_async_methods = { 142 | #if PY_MINOR_VERSION > 9 143 | .am_await = awaitable_next, 144 | .am_send = awaitable_am_send 145 | #else 146 | .am_await = awaitable_next 147 | #endif 148 | }; 149 | -------------------------------------------------------------------------------- /src/_pyawaitable/genwrapper.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #define DONE(cb) \ 9 | do { cb->done = true; \ 10 | Py_CLEAR(cb->coro); \ 11 | Py_CLEAR(g->gw_current_await); } while (0) 12 | #define AW_DONE() \ 13 | do { \ 14 | aw->aw_done = true; \ 15 | Py_CLEAR(g->gw_aw); \ 16 | } while (0) 17 | #define DONE_IF_OK(cb) \ 18 | if (PyAwaitable_LIKELY(cb != NULL)) { \ 19 | DONE(cb); \ 20 | } 21 | #define DONE_IF_OK_AND_CHECK(cb) \ 22 | if (PyAwaitable_UNLIKELY(aw->aw_recently_cancelled)) { \ 23 | cb = NULL; \ 24 | } \ 25 | else { \ 26 | DONE(cb); \ 27 | } 28 | /* If we recently cancelled, then cb is no longer valid */ 29 | #define CLEAR_CALLBACK_IF_CANCELLED() \ 30 | if (PyAwaitable_UNLIKELY(aw->aw_recently_cancelled)) { \ 31 | cb = NULL; \ 32 | } \ 33 | 34 | #define FIRE_ERROR_CALLBACK_AND_NEXT() \ 35 | if ( \ 36 | _PyAwaitableGenWrapper_FireErrCallback( \ 37 | (PyObject *) aw, \ 38 | cb->err_callback \ 39 | ) < 0 \ 40 | ) { \ 41 | DONE_IF_OK_AND_CHECK(cb); \ 42 | AW_DONE(); \ 43 | return NULL; \ 44 | } \ 45 | DONE_IF_OK_AND_CHECK(cb); \ 46 | return _PyAwaitableGenWrapper_Next(self); 47 | #define RETURN_ADVANCE_GENERATOR() \ 48 | DONE_IF_OK(cb); \ 49 | PyAwaitable_MUSTTAIL return _PyAwaitableGenWrapper_Next(self); 50 | 51 | static PyObject * 52 | gen_new(PyTypeObject *tp, PyObject *args, PyObject *kwds) 53 | { 54 | assert(tp != NULL); 55 | assert(tp->tp_alloc != NULL); 56 | 57 | PyObject *self = tp->tp_alloc(tp, 0); 58 | if (PyAwaitable_UNLIKELY(self == NULL)) { 59 | return NULL; 60 | } 61 | 62 | GenWrapperObject *g = (GenWrapperObject *) self; 63 | g->gw_aw = NULL; 64 | g->gw_current_await = NULL; 65 | 66 | return (PyObject *) g; 67 | } 68 | 69 | static int 70 | genwrapper_traverse(PyObject *self, visitproc visit, void *arg) 71 | { 72 | GenWrapperObject *gw = (GenWrapperObject *) self; 73 | Py_VISIT(gw->gw_current_await); 74 | Py_VISIT(gw->gw_aw); 75 | return 0; 76 | } 77 | 78 | static int 79 | genwrapper_clear(PyObject *self) 80 | { 81 | GenWrapperObject *gw = (GenWrapperObject *) self; 82 | Py_CLEAR(gw->gw_current_await); 83 | Py_CLEAR(gw->gw_aw); 84 | return 0; 85 | } 86 | 87 | static void 88 | gen_dealloc(PyObject *self) 89 | { 90 | PyObject_GC_UnTrack(self); 91 | (void)genwrapper_clear(self); 92 | Py_TYPE(self)->tp_free(self); 93 | } 94 | 95 | _PyAwaitable_INTERNAL(PyObject *) 96 | genwrapper_new(PyAwaitableObject * aw) 97 | { 98 | assert(aw != NULL); 99 | PyTypeObject *type = _PyAwaitable_GetGenWrapperType(); 100 | if (PyAwaitable_UNLIKELY(type == NULL)) { 101 | return NULL; 102 | } 103 | GenWrapperObject *g = (GenWrapperObject *) gen_new( 104 | type, 105 | NULL, 106 | NULL 107 | ); 108 | 109 | if (PyAwaitable_UNLIKELY(g == NULL)) { 110 | return NULL; 111 | } 112 | 113 | g->gw_aw = (PyAwaitableObject *) Py_NewRef((PyObject *) aw); 114 | return (PyObject *) g; 115 | } 116 | 117 | _PyAwaitable_INTERNAL(int) 118 | _PyAwaitableGenWrapper_FireErrCallback( 119 | PyObject * self, 120 | PyAwaitable_Error err_callback 121 | ) 122 | { 123 | assert(PyErr_Occurred() != NULL); 124 | if (err_callback == NULL) { 125 | return -1; 126 | } 127 | 128 | PyObject *err = PyErr_GetRaisedException(); 129 | if (PyAwaitable_UNLIKELY(err == NULL)) { 130 | PyErr_SetString( 131 | PyExc_SystemError, 132 | "PyAwaitable: Something returned -1 without an exception set" 133 | ); 134 | return -1; 135 | } 136 | 137 | Py_INCREF(self); 138 | int e_res = err_callback(self, err); 139 | Py_DECREF(self); 140 | 141 | if (e_res < 0) { 142 | // If the res is -1, the error is restored. 143 | // Otherwise, it is not. 144 | if (e_res == -1) { 145 | PyErr_SetRaisedException(err); 146 | } 147 | else { 148 | Py_DECREF(err); 149 | } 150 | return -1; 151 | } 152 | 153 | Py_DECREF(err); 154 | return 0; 155 | } 156 | 157 | static inline pyawaitable_callback * 158 | genwrapper_advance(GenWrapperObject *gw) 159 | { 160 | return pyawaitable_array_GET_ITEM( 161 | &gw->gw_aw->aw_callbacks, 162 | gw->gw_aw->aw_state++ 163 | ); 164 | } 165 | 166 | static PyObject * 167 | get_generator_return_value(void) 168 | { 169 | PyObject *value; 170 | if (PyErr_Occurred()) { 171 | value = PyErr_GetRaisedException(); 172 | assert(value != NULL); 173 | assert(PyObject_IsInstance(value, PyExc_StopIteration)); 174 | PyObject *tmp = PyObject_GetAttrString(value, "value"); 175 | if (PyAwaitable_UNLIKELY(tmp == NULL)) { 176 | Py_DECREF(value); 177 | return NULL; 178 | } 179 | Py_DECREF(value); 180 | value = tmp; 181 | } 182 | else { 183 | value = Py_NewRef(Py_None); 184 | } 185 | 186 | return value; 187 | } 188 | 189 | static int 190 | maybe_set_result(PyAwaitableObject *aw) 191 | { 192 | if (pyawaitable_array_LENGTH(&aw->aw_callbacks) == aw->aw_state) { 193 | PyErr_SetObject( 194 | PyExc_StopIteration, 195 | aw->aw_result ? aw->aw_result : Py_None 196 | ); 197 | return 1; 198 | } 199 | 200 | return 0; 201 | } 202 | 203 | static inline PyAwaitable_COLD PyObject * 204 | bad_callback(void) 205 | { 206 | PyErr_SetString( 207 | PyExc_SystemError, 208 | "PyAwaitable: User callback returned -1 without exception set" 209 | ); 210 | return NULL; 211 | } 212 | 213 | static inline PyObject * 214 | get_awaitable_iterator(PyObject *op) 215 | { 216 | if ( 217 | PyAwaitable_UNLIKELY( 218 | Py_TYPE(op)->tp_as_async == NULL || 219 | Py_TYPE(op)->tp_as_async->am_await == NULL 220 | ) 221 | ) { 222 | // Fall back to the dunder 223 | // XXX Is this case possible? 224 | PyObject *__await__ = PyObject_GetAttrString(op, "__await__"); 225 | if (__await__ == NULL) { 226 | return NULL; 227 | } 228 | 229 | PyObject *res = PyObject_CallNoArgs(__await__); 230 | Py_DECREF(__await__); 231 | return res; 232 | } 233 | 234 | return Py_TYPE(op)->tp_as_async->am_await(op); 235 | } 236 | 237 | _PyAwaitable_INTERNAL(PyObject *) PyAwaitable_HOT 238 | _PyAwaitableGenWrapper_Next(PyObject *self) 239 | { 240 | GenWrapperObject *g = (GenWrapperObject *)self; 241 | PyAwaitableObject *aw = g->gw_aw; 242 | 243 | if (PyAwaitable_UNLIKELY(aw == NULL)) { 244 | PyErr_SetString( 245 | PyExc_RuntimeError, 246 | "PyAwaitable: Generator cannot be awaited after returning" 247 | ); 248 | return NULL; 249 | } 250 | 251 | pyawaitable_callback *cb; 252 | 253 | if (g->gw_current_await == NULL) { 254 | if (maybe_set_result(aw)) { 255 | // Coroutine is done, woohoo! 256 | AW_DONE(); 257 | return NULL; 258 | } 259 | 260 | cb = genwrapper_advance(g); 261 | assert(cb != NULL); 262 | assert(cb->done == false); 263 | 264 | if (cb->callback != NULL && cb->coro == NULL) { 265 | int def_res = ((PyAwaitable_Defer)cb->callback)((PyObject *)aw); 266 | CLEAR_CALLBACK_IF_CANCELLED(); 267 | if (def_res < 0) { 268 | DONE_IF_OK(cb); 269 | AW_DONE(); 270 | return NULL; 271 | } 272 | 273 | // Callback is done. 274 | RETURN_ADVANCE_GENERATOR(); 275 | } 276 | 277 | assert(cb->coro != NULL); 278 | g->gw_current_await = get_awaitable_iterator(cb->coro); 279 | if (g->gw_current_await == NULL) { 280 | FIRE_ERROR_CALLBACK_AND_NEXT(); 281 | } 282 | } 283 | else { 284 | cb = pyawaitable_array_GET_ITEM(&aw->aw_callbacks, aw->aw_state - 1); 285 | } 286 | 287 | PyObject *result = Py_TYPE( 288 | g->gw_current_await 289 | )->tp_iternext(g->gw_current_await); 290 | 291 | if (result != NULL) { 292 | // Yield! 293 | return result; 294 | } 295 | 296 | // Rare, but it's possible that the generator cancelled us 297 | CLEAR_CALLBACK_IF_CANCELLED(); 298 | 299 | PyObject *occurred = PyErr_Occurred(); 300 | if (!occurred) { 301 | // Coro is done, no result. 302 | if (cb == NULL || !cb->callback) { 303 | // No callback, skip trying to handle anything 304 | RETURN_ADVANCE_GENERATOR(); 305 | } 306 | } 307 | 308 | if (occurred && !PyErr_ExceptionMatches(PyExc_StopIteration)) { 309 | // An error occurred! 310 | FIRE_ERROR_CALLBACK_AND_NEXT(); 311 | } 312 | 313 | // Coroutine is done, but with a non-None result. 314 | if (cb == NULL || cb->callback == NULL) { 315 | // We can disregard the result if there's no callback. 316 | PyErr_Clear(); 317 | RETURN_ADVANCE_GENERATOR(); 318 | } 319 | 320 | assert(cb != NULL); 321 | // Deduce the return value of the coroutine 322 | PyObject *value = get_generator_return_value(); 323 | if (value == NULL) { 324 | DONE(cb); 325 | AW_DONE(); 326 | return NULL; 327 | } 328 | 329 | // Preserve the error callback in case we get cancelled 330 | PyAwaitable_Error err_callback = cb->err_callback; 331 | Py_INCREF(aw); 332 | int res = cb->callback((PyObject *) aw, value); 333 | Py_DECREF(aw); 334 | Py_DECREF(value); 335 | 336 | CLEAR_CALLBACK_IF_CANCELLED(); 337 | 338 | // Sanity check to make sure that there's actually 339 | // an error set. 340 | if (res < 0) { 341 | if (!PyErr_Occurred()) { 342 | DONE(cb); 343 | AW_DONE(); 344 | return bad_callback(); 345 | } 346 | } 347 | 348 | if (res < -1) { 349 | // -2 or lower denotes that the error should be deferred, 350 | // regardless of whether a handler is present. 351 | DONE_IF_OK(cb); 352 | AW_DONE(); 353 | return NULL; 354 | } 355 | 356 | if (res < 0) { 357 | FIRE_ERROR_CALLBACK_AND_NEXT(); 358 | } 359 | 360 | RETURN_ADVANCE_GENERATOR(); 361 | } 362 | 363 | _PyAwaitable_INTERNAL_DATA_DEF(PyTypeObject) _PyAwaitableGenWrapperType = { 364 | PyVarObject_HEAD_INIT(NULL, 0) 365 | .tp_name = "_PyAwaitableGenWrapperType", 366 | .tp_basicsize = sizeof(GenWrapperObject), 367 | .tp_dealloc = gen_dealloc, 368 | .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, 369 | .tp_iter = PyObject_SelfIter, 370 | .tp_iternext = _PyAwaitableGenWrapper_Next, 371 | .tp_clear = genwrapper_clear, 372 | .tp_traverse = genwrapper_traverse, 373 | .tp_new = gen_new, 374 | }; 375 | -------------------------------------------------------------------------------- /src/_pyawaitable/init.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | static int 7 | dict_add_type(PyObject *state, PyTypeObject *obj) 8 | { 9 | assert(obj != NULL); 10 | assert(state != NULL); 11 | assert(PyDict_Check(state)); 12 | assert(obj->tp_name != NULL); 13 | 14 | Py_INCREF(obj); 15 | if (PyType_Ready(obj) < 0) { 16 | Py_DECREF(obj); 17 | return -1; 18 | } 19 | 20 | if (PyDict_SetItemString(state, obj->tp_name, (PyObject *)obj) < 0) { 21 | Py_DECREF(obj); 22 | return -1; 23 | } 24 | Py_DECREF(obj); 25 | return 0; 26 | } 27 | 28 | static int 29 | init_state(PyObject *state) 30 | { 31 | assert(state != NULL); 32 | assert(PyDict_Check(state)); 33 | if (dict_add_type(state, &PyAwaitable_Type) < 0) { 34 | return -1; 35 | } 36 | 37 | if (dict_add_type(state, &_PyAwaitableGenWrapperType) < 0) { 38 | return -1; 39 | } 40 | 41 | PyObject *version = PyLong_FromLong(PyAwaitable_MAGIC_NUMBER); 42 | if (version == NULL) { 43 | return -1; 44 | } 45 | 46 | if (PyDict_SetItemString(state, "magic_version", version) < 0) { 47 | Py_DECREF(version); 48 | return -1; 49 | } 50 | 51 | Py_DECREF(version); 52 | return 0; 53 | } 54 | 55 | static PyObject * 56 | create_state(void) 57 | { 58 | PyObject *state = PyDict_New(); 59 | if (state == NULL) { 60 | return NULL; 61 | } 62 | 63 | if (init_state(state) < 0) { 64 | Py_DECREF(state); 65 | return NULL; 66 | } 67 | 68 | return state; 69 | } 70 | 71 | static PyObject * 72 | interp_get_dict(void) 73 | { 74 | PyInterpreterState *interp = PyInterpreterState_Get(); 75 | assert(interp != NULL); 76 | PyObject *interp_state = PyInterpreterState_GetDict(interp); 77 | if (interp_state == NULL) { 78 | // Would be a memory error or something more exotic, but 79 | // there's nothing we can do. 80 | PyErr_SetString( 81 | PyExc_RuntimeError, 82 | "PyAwaitable: Interpreter failed to provide a state dictionary" 83 | ); 84 | return NULL; 85 | } 86 | 87 | return interp_state; 88 | } 89 | 90 | static inline PyObject * 91 | not_initialized(void) 92 | { 93 | PyErr_SetString( 94 | PyExc_RuntimeError, 95 | "PyAwaitable hasn't been initialized yet! " 96 | "Did you forget to call PyAwaitable_Init()?" 97 | ); 98 | return NULL; 99 | } 100 | 101 | static inline int 102 | state_corrupted(const char *err, PyObject *found) 103 | { 104 | assert(err != NULL); 105 | assert(found != NULL); 106 | PyErr_Format( 107 | PyExc_SystemError, 108 | "PyAwaitable corruption! %s: %R", 109 | err, 110 | found 111 | ); 112 | Py_DECREF(found); 113 | return -1; 114 | } 115 | 116 | static PyObject * 117 | get_state_value(PyObject *state, const char *name) 118 | { 119 | assert(name != NULL); 120 | PyObject *str = PyUnicode_FromString(name); 121 | if (str == NULL) { 122 | return NULL; 123 | } 124 | 125 | PyObject *version = PyDict_GetItemWithError(state, str); 126 | Py_DECREF(str); 127 | return version; 128 | } 129 | 130 | static long 131 | get_state_version(PyObject *state) 132 | { 133 | assert(state != NULL); 134 | assert(PyDict_Check(state)); 135 | 136 | PyObject *version = get_state_value(state, "magic_version"); 137 | if (version == NULL) { 138 | return -1; 139 | } 140 | 141 | if (!PyLong_CheckExact(version)) { 142 | return state_corrupted("Non-int version number", version); 143 | } 144 | 145 | long version_num = PyLong_AsLong(version); 146 | if (version_num == -1 && PyErr_Occurred()) { 147 | Py_DECREF(version); 148 | return -1; 149 | } 150 | 151 | if (version_num < 0) { 152 | return state_corrupted("Found <0 version somehow", version); 153 | } 154 | 155 | return version_num; 156 | } 157 | 158 | static PyObject * 159 | find_module_for_version(PyObject *interp_dict, long version) 160 | { 161 | PyObject *list = PyDict_GetItemString(interp_dict, "_pyawaitable_states"); 162 | if (list == NULL) { 163 | return not_initialized(); 164 | } 165 | 166 | if (!PyList_CheckExact(list)) { 167 | state_corrupted("_pyawaitable_states is not a list", list); 168 | return NULL; 169 | } 170 | 171 | for (Py_ssize_t i = 0; i < PyList_GET_SIZE(list); ++i) { 172 | PyObject *mod = PyList_GET_ITEM(list, i); 173 | long got_version = get_state_version(mod); 174 | 175 | if (got_version == -1) { 176 | return NULL; 177 | } 178 | 179 | if (got_version == version) { 180 | return mod; 181 | } 182 | } 183 | 184 | return not_initialized(); 185 | } 186 | 187 | static PyObject * 188 | find_top_level_state(PyObject **interp_dict) 189 | { 190 | PyObject *dict = interp_get_dict(); 191 | if (dict == NULL) { 192 | return NULL; 193 | } 194 | 195 | PyObject *mod = PyDict_GetItemString(dict, "_pyawaitable_state"); 196 | if (mod == NULL) { 197 | return not_initialized(); 198 | } 199 | 200 | if (interp_dict != NULL) { 201 | *interp_dict = dict; 202 | } 203 | return mod; 204 | } 205 | 206 | static PyAwaitable_thread_local PyObject *pyawaitable_fast_state = NULL; 207 | 208 | _PyAwaitable_INTERNAL(PyObject *) 209 | _PyAwaitable_GetState(void) 210 | { 211 | if (pyawaitable_fast_state != NULL) { 212 | return pyawaitable_fast_state; 213 | } 214 | 215 | PyObject *interp_dict; 216 | PyObject *state = find_top_level_state(&interp_dict); // Borrowed reference 217 | if (state == NULL) { 218 | return NULL; 219 | } 220 | 221 | long version = get_state_version(state); 222 | if (version == -1) { 223 | return NULL; 224 | } 225 | 226 | if (version != PyAwaitable_MAGIC_NUMBER) { 227 | // Not our module! 228 | state = find_module_for_version(interp_dict, PyAwaitable_MAGIC_NUMBER); 229 | if (state == NULL) { 230 | return NULL; 231 | } 232 | } 233 | 234 | // We want this to be a borrowed reference 235 | pyawaitable_fast_state = state; 236 | return state; 237 | } 238 | 239 | static PyAwaitable_thread_local PyTypeObject *pyawaitable_fast_aw = NULL; 240 | static PyAwaitable_thread_local PyTypeObject *pyawaitable_fast_gw = NULL; 241 | 242 | _PyAwaitable_API(PyTypeObject *) 243 | PyAwaitable_GetType(void) 244 | { 245 | if (pyawaitable_fast_aw != NULL) { 246 | return pyawaitable_fast_aw; 247 | } 248 | PyObject *state = _PyAwaitable_GetState(); 249 | if (state == NULL) { 250 | return NULL; 251 | } 252 | 253 | PyTypeObject *pyawaitable_type = (PyTypeObject *)get_state_value( 254 | state, 255 | "_PyAwaitableType" 256 | ); 257 | if (pyawaitable_type == NULL) { 258 | return NULL; 259 | } 260 | 261 | // Should be an immortal reference 262 | pyawaitable_fast_aw = pyawaitable_type; 263 | return pyawaitable_type; 264 | } 265 | 266 | 267 | _PyAwaitable_INTERNAL(PyTypeObject *) 268 | _PyAwaitable_GetGenWrapperType(void) 269 | { 270 | if (pyawaitable_fast_gw != NULL) { 271 | return pyawaitable_fast_gw; 272 | } 273 | PyObject *state = _PyAwaitable_GetState(); 274 | if (state == NULL) { 275 | return NULL; 276 | } 277 | 278 | PyTypeObject *gw_type = (PyTypeObject *)get_state_value( 279 | state, 280 | "_PyAwaitableGenWrapperType" 281 | ); 282 | if (gw_type == NULL) { 283 | return NULL; 284 | } 285 | 286 | pyawaitable_fast_gw = gw_type; 287 | return (PyTypeObject *)gw_type; 288 | } 289 | 290 | static int 291 | add_state_to_list(PyObject *interp_dict, PyObject *state) 292 | { 293 | assert(interp_dict != NULL); 294 | assert(state != NULL); 295 | assert(PyDict_Check(interp_dict)); 296 | assert(PyDict_Check(state)); 297 | 298 | PyObject *pyawaitable_list = Py_XNewRef( 299 | PyDict_GetItemString( 300 | interp_dict, 301 | "_pyawaitable_states" 302 | ) 303 | ); 304 | if (pyawaitable_list == NULL) { 305 | // No list has been set 306 | pyawaitable_list = PyList_New(1); 307 | if (pyawaitable_list == NULL) { 308 | // Memory error 309 | return -1; 310 | } 311 | 312 | if ( 313 | PyDict_SetItemString( 314 | interp_dict, 315 | "_pyawaitable_states", 316 | pyawaitable_list 317 | ) < 0 318 | ) { 319 | Py_DECREF(pyawaitable_list); 320 | return -1; 321 | } 322 | } 323 | 324 | if (PyList_Append(pyawaitable_list, state) < 0) { 325 | Py_DECREF(pyawaitable_list); 326 | return -1; 327 | } 328 | 329 | Py_DECREF(pyawaitable_list); 330 | return 0; 331 | } 332 | 333 | _PyAwaitable_API(int) 334 | PyAwaitable_Init(void) 335 | { 336 | PyObject *dict = interp_get_dict(); 337 | if (dict == NULL) { 338 | return -1; 339 | } 340 | 341 | PyObject *state = create_state(); 342 | if (state == NULL) { 343 | return -1; 344 | } 345 | 346 | PyObject *existing = PyDict_GetItemString(dict, "_pyawaitable_state"); 347 | if (existing != NULL) { 348 | /* Oh no, PyAwaitable has been used twice! */ 349 | long version = get_state_version(existing); 350 | if (version == -1) { 351 | Py_DECREF(state); 352 | return -1; 353 | } 354 | 355 | if (version == PyAwaitable_MAGIC_NUMBER) { 356 | // Yay! It just happens that it's the same version as us. 357 | // Let's just reuse it. 358 | Py_DECREF(state); 359 | return 0; 360 | } 361 | 362 | if (add_state_to_list(dict, state) < 0) { 363 | Py_DECREF(state); 364 | return -1; 365 | } 366 | 367 | Py_DECREF(state); 368 | return 0; 369 | } 370 | 371 | if (PyDict_SetItemString(dict, "_pyawaitable_state", state) < 0) { 372 | Py_DECREF(state); 373 | return -1; 374 | } 375 | 376 | Py_DECREF(state); 377 | return 0; 378 | } 379 | -------------------------------------------------------------------------------- /src/_pyawaitable/values.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define NOTHING 11 | 12 | #define SAVE(field, type, extra) \ 13 | PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; \ 14 | pyawaitable_array *array = &aw->field; \ 15 | va_list vargs; \ 16 | va_start(vargs, nargs); \ 17 | for (Py_ssize_t i = 0; i < nargs; ++i) { \ 18 | type ptr = va_arg(vargs, type); \ 19 | assert((void *)ptr != NULL); \ 20 | if (pyawaitable_array_append(array, (void *)ptr) < 0) { \ 21 | PyErr_NoMemory(); \ 22 | return -1; \ 23 | } \ 24 | extra; \ 25 | } \ 26 | va_end(vargs); \ 27 | return 0 28 | 29 | #define UNPACK(field, type) \ 30 | PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; \ 31 | pyawaitable_array *array = &aw->field; \ 32 | if (pyawaitable_array_LENGTH(array) == 0) { \ 33 | PyErr_SetString( \ 34 | PyExc_RuntimeError, \ 35 | "PyAwaitable: Object has no stored values" \ 36 | ); \ 37 | return -1; \ 38 | } \ 39 | va_list vargs; \ 40 | va_start(vargs, awaitable); \ 41 | for (Py_ssize_t i = 0; i < pyawaitable_array_LENGTH(array); ++i) { \ 42 | type *ptr = va_arg(vargs, type *); \ 43 | if (ptr == NULL) { \ 44 | continue; \ 45 | } \ 46 | *ptr = (type)pyawaitable_array_GET_ITEM(array, i); \ 47 | } \ 48 | va_end(vargs); \ 49 | return 0 50 | 51 | #define SET(field, type) \ 52 | assert(awaitable != NULL); \ 53 | PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; \ 54 | pyawaitable_array *array = &aw->field; \ 55 | if (check_index(index, array) < 0) { \ 56 | return -1; \ 57 | } \ 58 | pyawaitable_array_set(array, index, (void *)(new_value)); \ 59 | return 0 60 | 61 | #define GET(field, type) \ 62 | assert(awaitable != NULL); \ 63 | PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; \ 64 | pyawaitable_array *array = &aw->field; \ 65 | if (check_index(index, array) < 0) { \ 66 | return (type)NULL; \ 67 | } \ 68 | return (type)pyawaitable_array_GET_ITEM(array, index) 69 | 70 | static int 71 | check_index(Py_ssize_t index, pyawaitable_array *array) 72 | { 73 | assert(array != NULL); 74 | if (PyAwaitable_UNLIKELY(index < 0)) { 75 | PyErr_SetString( 76 | PyExc_IndexError, 77 | "PyAwaitable: Cannot set negative index" 78 | ); 79 | return -1; 80 | } 81 | 82 | if (PyAwaitable_UNLIKELY(index >= pyawaitable_array_LENGTH(array))) { 83 | PyErr_SetString( 84 | PyExc_IndexError, 85 | "PyAwaitable: Cannot set index that is out of bounds" 86 | ); 87 | return -1; 88 | } 89 | 90 | return 0; 91 | } 92 | 93 | _PyAwaitable_API(int) 94 | PyAwaitable_UnpackValues(PyObject * awaitable, ...) 95 | { 96 | UNPACK(aw_object_values, PyObject *); 97 | } 98 | 99 | _PyAwaitable_API(int) 100 | PyAwaitable_SaveValues(PyObject * awaitable, Py_ssize_t nargs, ...) 101 | { 102 | SAVE(aw_object_values, PyObject *, Py_INCREF(ptr)); 103 | } 104 | 105 | _PyAwaitable_API(int) 106 | PyAwaitable_SetValue( 107 | PyObject * awaitable, 108 | Py_ssize_t index, 109 | PyObject * new_value 110 | ) 111 | { 112 | SET(aw_object_values, Py_NewRef); 113 | } 114 | 115 | _PyAwaitable_API(PyObject *) 116 | PyAwaitable_GetValue( 117 | PyObject * awaitable, 118 | Py_ssize_t index 119 | ) 120 | { 121 | GET(aw_object_values, PyObject *); 122 | } 123 | 124 | /* Arbitrary Values */ 125 | 126 | _PyAwaitable_API(int) 127 | PyAwaitable_UnpackArbValues(PyObject * awaitable, ...) 128 | { 129 | UNPACK(aw_arbitrary_values, void *); 130 | } 131 | 132 | _PyAwaitable_API(int) 133 | PyAwaitable_SaveArbValues(PyObject * awaitable, Py_ssize_t nargs, ...) 134 | { 135 | SAVE(aw_arbitrary_values, void *, NOTHING); 136 | } 137 | 138 | _PyAwaitable_API(int) 139 | PyAwaitable_SetArbValue( 140 | PyObject * awaitable, 141 | Py_ssize_t index, 142 | void *new_value 143 | ) 144 | { 145 | SET(aw_arbitrary_values, void *); 146 | } 147 | 148 | _PyAwaitable_API(void *) 149 | PyAwaitable_GetArbValue( 150 | PyObject * awaitable, 151 | Py_ssize_t index 152 | ) 153 | { 154 | GET(aw_arbitrary_values, void *); 155 | } 156 | -------------------------------------------------------------------------------- /src/_pyawaitable/with.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | static int 7 | async_with_inner(PyObject *aw, PyObject *res) 8 | { 9 | PyAwaitable_Callback cb; 10 | PyAwaitable_Error err; 11 | PyObject *exit; 12 | if (PyAwaitable_UnpackArbValues(aw, &cb, &err) < 0) { 13 | return -1; 14 | } 15 | 16 | if (PyAwaitable_UnpackValues(aw, &exit) < 0) { 17 | return -1; 18 | } 19 | 20 | Py_INCREF(aw); 21 | Py_INCREF(res); 22 | int callback_result = cb != NULL ? cb(aw, res) : 0; 23 | Py_DECREF(res); 24 | Py_DECREF(aw); 25 | 26 | if (callback_result < 0) { 27 | PyObject *tp, *val, *tb; 28 | PyErr_Fetch(&tp, &val, &tb); 29 | PyErr_NormalizeException(&tp, &val, &tb); 30 | 31 | if (tp == NULL) { 32 | PyErr_SetString( 33 | PyExc_SystemError, 34 | "PyAwaitable: `async with` callback returned -1 without exception set" 35 | ); 36 | return -1; 37 | } 38 | 39 | // Traceback can still be NULL 40 | if (tb == NULL) 41 | tb = Py_NewRef(Py_None); 42 | 43 | PyObject *coro = PyObject_Vectorcall( 44 | exit, 45 | (PyObject *[]) { tp, val, tb }, 46 | 3, 47 | NULL 48 | ); 49 | Py_DECREF(tp); 50 | Py_DECREF(val); 51 | Py_DECREF(tb); 52 | if (coro == NULL) { 53 | return -1; 54 | } 55 | 56 | if (PyAwaitable_AddAwait(aw, coro, NULL, NULL) < 0) { 57 | Py_DECREF(coro); 58 | return -1; 59 | } 60 | 61 | Py_DECREF(coro); 62 | return 0; 63 | } 64 | else { 65 | // OK 66 | PyObject *coro = PyObject_Vectorcall( 67 | exit, 68 | (PyObject *[]) { Py_None, Py_None, Py_None }, 69 | 3, 70 | NULL 71 | ); 72 | if (coro == NULL) { 73 | return -1; 74 | } 75 | 76 | if (PyAwaitable_AddAwait(aw, coro, NULL, NULL) < 0) { 77 | Py_DECREF(coro); 78 | return -1; 79 | } 80 | Py_DECREF(coro); 81 | return 0; 82 | } 83 | } 84 | 85 | _PyAwaitable_API(int) 86 | PyAwaitable_AsyncWith( 87 | PyObject * aw, 88 | PyObject * ctx, 89 | PyAwaitable_Callback cb, 90 | PyAwaitable_Error err 91 | ) 92 | { 93 | PyObject *with = PyObject_GetAttrString(ctx, "__aenter__"); 94 | if (with == NULL) { 95 | PyErr_Format( 96 | PyExc_TypeError, 97 | "PyAwaitable: %R is not an async context manager (missing __aenter__)", 98 | ctx 99 | ); 100 | return -1; 101 | } 102 | PyObject *exit = PyObject_GetAttrString(ctx, "__aexit__"); 103 | if (exit == NULL) { 104 | Py_DECREF(with); 105 | PyErr_Format( 106 | PyExc_TypeError, 107 | "PyAwaitable: %R is not an async context manager (missing __aexit__)", 108 | ctx 109 | ); 110 | return -1; 111 | } 112 | 113 | PyObject *inner_aw = PyAwaitable_New(); 114 | 115 | if (inner_aw == NULL) { 116 | Py_DECREF(with); 117 | Py_DECREF(exit); 118 | return -1; 119 | } 120 | 121 | if (PyAwaitable_SaveArbValues(inner_aw, 2, cb, err) < 0) { 122 | Py_DECREF(inner_aw); 123 | Py_DECREF(with); 124 | Py_DECREF(exit); 125 | return -1; 126 | } 127 | 128 | if (PyAwaitable_SaveValues(inner_aw, 1, exit) < 0) { 129 | Py_DECREF(inner_aw); 130 | Py_DECREF(exit); 131 | Py_DECREF(with); 132 | return -1; 133 | } 134 | 135 | Py_DECREF(exit); 136 | 137 | PyObject *coro = PyObject_CallNoArgs(with); 138 | Py_DECREF(with); 139 | 140 | if (coro == NULL) { 141 | Py_DECREF(inner_aw); 142 | return -1; 143 | } 144 | 145 | // Note: Errors in __aenter__ are not sent to __aexit__ 146 | if ( 147 | PyAwaitable_AddAwait( 148 | inner_aw, 149 | coro, 150 | async_with_inner, 151 | NULL 152 | ) < 0 153 | ) { 154 | Py_DECREF(inner_aw); 155 | Py_DECREF(coro); 156 | return -1; 157 | } 158 | 159 | Py_DECREF(coro); 160 | 161 | if (PyAwaitable_AddAwait(aw, inner_aw, NULL, err) < 0) { 162 | Py_DECREF(inner_aw); 163 | return -1; 164 | } 165 | 166 | Py_DECREF(inner_aw); 167 | return 0; 168 | } 169 | -------------------------------------------------------------------------------- /src/pyawaitable/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyAwaitable - Call asynchronous code from an extension module. 3 | 4 | It's unlikely that you want to import this module from Python, other than 5 | for use in setuptools. 6 | 7 | Docs: https://pyawaitable.zintensity.dev/ 8 | Source: https://github.com/ZeroIntensity/pyawaitable 9 | """ 10 | 11 | __all__ = ("include",) 12 | __version__ = "2.1.0-dev0" 13 | __author__ = "Peter Bierma" 14 | 15 | 16 | def include(*, suppress_error: bool = False) -> str: 17 | """ 18 | Get the directory containing the `pyawaitable.h` file. 19 | """ 20 | import os 21 | from pathlib import Path 22 | 23 | directory = Path(__file__).parent 24 | if "pyawaitable.h" not in os.listdir(directory) and not suppress_error: 25 | raise RuntimeError( 26 | "pyawaitable.h wasn't found! Are you sure your installation is correct?" 27 | ) 28 | 29 | return str(directory.absolute()) 30 | -------------------------------------------------------------------------------- /src/pyawaitable/__main__.py: -------------------------------------------------------------------------------- 1 | import optparse 2 | from typing import Literal 3 | from pyawaitable import __version__, include as get_include 4 | 5 | DEFAULT_MESSAGE = f"""PyAwaitable {__version__} 6 | Documentation: https://pyawaitable.zintensity.dev 7 | Source: https://github.com/ZeroIntensity/pyawaitable""" 8 | 9 | def option_main(include: bool, version: bool) -> None: 10 | if include: 11 | print(get_include()) 12 | if version: 13 | print(__version__) 14 | 15 | if not include and not version: 16 | print(DEFAULT_MESSAGE) 17 | 18 | 19 | def option_as_bool(value: None | Literal[True]) -> bool: 20 | if value is None: 21 | return False 22 | return True 23 | 24 | 25 | def main(): 26 | parser = optparse.OptionParser() 27 | parser.add_option("--include", action="store_true") 28 | parser.add_option("--version", action="store_true") 29 | opts, _ = parser.parse_args() 30 | option_main(option_as_bool(opts.include), option_as_bool(opts.version),) 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /tests/builds/ensure_build_worked.py: -------------------------------------------------------------------------------- 1 | """ 2 | Script to ensure that an extension module containing PyAwaitable was successfully built. 3 | Intended to be run by Hatch in CI. 4 | """ 5 | import optparse 6 | import sys 7 | import asyncio 8 | import traceback 9 | import importlib 10 | import site 11 | import os 12 | from typing import TypeVar 13 | 14 | WINDOWS_HATCH_BUG = """PyAwaitable couldn't be imported. 15 | This is probably a bug in Hatch environments, so this is being skipped 16 | on Windows.""" 17 | 18 | try: 19 | import pyawaitable 20 | except ImportError: 21 | if os.name != 'nt': 22 | raise 23 | 24 | print(WINDOWS_HATCH_BUG) 25 | sys.exit(0) 26 | 27 | T = TypeVar("T") 28 | 29 | def not_none(value: T | None) -> T: 30 | assert value is not None, "got None where None shouldn't be" 31 | return value 32 | 33 | def debug_directory(what: str, path: str) -> None: 34 | print(f"{what}: {path}", file=sys.stderr) 35 | print(f" Contents of {what}:", file=sys.stderr) 36 | for root, _, files in os.walk(path): 37 | for file in files: 38 | print(f" {what}: {os.path.join(root, file)}", file=sys.stderr) 39 | 40 | def debug_import_error(error: ImportError) -> None: 41 | debug_directory("PyAwaitable Include", pyawaitable.include()) 42 | debug_directory("User Site", not_none(site.USER_SITE)) 43 | debug_directory("User Base", not_none(site.USER_BASE)) 44 | print(error) 45 | 46 | def main(): 47 | parser = optparse.OptionParser() 48 | _, args = parser.parse_args() 49 | 50 | if not args: 51 | parser.error("Expected one argument.") 52 | 53 | mod = args[0] 54 | 55 | called = False 56 | async def dummy(): 57 | await asyncio.sleep(0) 58 | nonlocal called 59 | called = True 60 | 61 | try: 62 | asyncio.run(importlib.import_module(mod).async_function(dummy())) 63 | except BaseException as err: 64 | traceback.print_exc() 65 | if isinstance(err, ImportError): 66 | debug_import_error(err) 67 | print("Failed to import the module!", file=sys.stderr) 68 | else: 69 | print("Build failed!", file=sys.stderr) 70 | sys.exit(1) 71 | 72 | if not called: 73 | print("Build doesn't work!", file=sys.stderr) 74 | sys.exit(1) 75 | 76 | print("Success!") 77 | 78 | if __name__ == "__main__": 79 | main() 80 | -------------------------------------------------------------------------------- /tests/builds/meson/meson.build: -------------------------------------------------------------------------------- 1 | project('_meson_module', 'c') 2 | 3 | py = import('python').find_installation(pure: false) 4 | pyawaitable_include = run_command('pyawaitable', '--include', check: true).stdout().strip() 5 | 6 | 7 | py.extension_module( 8 | '_meson_module', 9 | 'module.c', 10 | install: true, 11 | include_directories: [pyawaitable_include], 12 | ) -------------------------------------------------------------------------------- /tests/builds/meson/module.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static int 5 | module_exec(PyObject *mod) 6 | { 7 | return PyAwaitable_Init(); 8 | } 9 | 10 | /* 11 | Equivalent to the following Python function: 12 | 13 | async def async_function(coro: collections.abc.Awaitable) -> None: 14 | await coro 15 | 16 | */ 17 | static PyObject * 18 | async_function(PyObject *self, PyObject *coro) 19 | { 20 | PyObject *awaitable = PyAwaitable_New(); 21 | if (awaitable == NULL) { 22 | return NULL; 23 | } 24 | 25 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 26 | Py_DECREF(awaitable); 27 | return NULL; 28 | } 29 | 30 | return awaitable; 31 | } 32 | 33 | static PyModuleDef_Slot module_slots[] = { 34 | {Py_mod_exec, module_exec}, 35 | {0, NULL} 36 | }; 37 | 38 | static PyMethodDef module_methods[] = { 39 | {"async_function", async_function, METH_O, NULL}, 40 | {NULL, NULL, 0, NULL}, 41 | }; 42 | 43 | static PyModuleDef module = { 44 | .m_base = PyModuleDef_HEAD_INIT, 45 | .m_size = 0, 46 | .m_slots = module_slots, 47 | .m_methods = module_methods 48 | }; 49 | 50 | PyMODINIT_FUNC 51 | PyInit__meson_module() 52 | { 53 | return PyModuleDef_Init(&module); 54 | } -------------------------------------------------------------------------------- /tests/builds/meson/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["meson-python"] 3 | build-backend = 'mesonpy' 4 | 5 | [project] 6 | name = "pyawaitable_test_meson" 7 | version = "0.0.0" 8 | -------------------------------------------------------------------------------- /tests/builds/scikit-build-core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15...3.30) 2 | project(${SKBUILD_PROJECT_NAME} LANGUAGES C) 3 | 4 | find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) 5 | 6 | Python_add_library(_sbc_module MODULE module.c WITH_SOABI) 7 | target_include_directories(_sbc_module PRIVATE $ENV{PYAWAITABLE_INCLUDE}) 8 | install(TARGETS _sbc_module DESTINATION .) 9 | -------------------------------------------------------------------------------- /tests/builds/scikit-build-core/module.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static int 5 | module_exec(PyObject *mod) 6 | { 7 | return PyAwaitable_Init(); 8 | } 9 | 10 | /* 11 | Equivalent to the following Python function: 12 | 13 | async def async_function(coro: collections.abc.Awaitable) -> None: 14 | await coro 15 | 16 | */ 17 | static PyObject * 18 | async_function(PyObject *self, PyObject *coro) 19 | { 20 | PyObject *awaitable = PyAwaitable_New(); 21 | if (awaitable == NULL) { 22 | return NULL; 23 | } 24 | 25 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 26 | Py_DECREF(awaitable); 27 | return NULL; 28 | } 29 | 30 | return awaitable; 31 | } 32 | 33 | static PyModuleDef_Slot module_slots[] = { 34 | {Py_mod_exec, module_exec}, 35 | {0, NULL} 36 | }; 37 | 38 | static PyMethodDef module_methods[] = { 39 | {"async_function", async_function, METH_O, NULL}, 40 | {NULL, NULL, 0, NULL}, 41 | }; 42 | 43 | static PyModuleDef module = { 44 | .m_base = PyModuleDef_HEAD_INIT, 45 | .m_size = 0, 46 | .m_slots = module_slots, 47 | .m_methods = module_methods 48 | }; 49 | 50 | PyMODINIT_FUNC 51 | PyInit__sbc_module() 52 | { 53 | return PyModuleDef_Init(&module); 54 | } -------------------------------------------------------------------------------- /tests/builds/scikit-build-core/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["scikit-build-core"] 3 | build-backend = "scikit_build_core.build" 4 | 5 | [project] 6 | name = "pyawaitable_test_sbc" 7 | version = "0.0.0" 8 | -------------------------------------------------------------------------------- /tests/module.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "pyawaitable_test.h" 4 | 5 | static int 6 | _pyawaitable_test_exec(PyObject *mod) 7 | { 8 | #define ADD_TESTS(name) \ 9 | do { \ 10 | if (PyModule_AddFunctions(mod, _pyawaitable_test_ ## name) < 0) { \ 11 | return -1; \ 12 | } \ 13 | } while (0) 14 | 15 | ADD_TESTS(awaitable); 16 | ADD_TESTS(callbacks); 17 | ADD_TESTS(values); 18 | #undef ADD_TESTS 19 | return PyAwaitable_Init(); 20 | } 21 | 22 | static PyModuleDef_Slot _pyawaitable_test_slots[] = { 23 | {Py_mod_exec, _pyawaitable_test_exec}, 24 | {0, NULL} 25 | }; 26 | 27 | static PyModuleDef _pyawaitable_test_module = { 28 | .m_base = PyModuleDef_HEAD_INIT, 29 | .m_size = 0, 30 | .m_slots = _pyawaitable_test_slots, 31 | }; 32 | 33 | PyMODINIT_FUNC 34 | PyInit__pyawaitable_test() 35 | { 36 | return PyModuleDef_Init(&_pyawaitable_test_module); 37 | } 38 | -------------------------------------------------------------------------------- /tests/pyawaitable_test.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_TEST_H 2 | #define PYAWAITABLE_TEST_H 3 | 4 | #include 5 | #include "test_util.h" 6 | 7 | extern TESTS(awaitable); 8 | extern TESTS(callbacks); 9 | extern TESTS(values); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /tests/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pyawaitable_test" 7 | version = "0.0.0" 8 | -------------------------------------------------------------------------------- /tests/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension 2 | from pathlib import Path 3 | from glob import glob 4 | 5 | NOT_FOUND = """pyawaitable.h wasn't found! It probably wasn't built. 6 | 7 | To build a working copy, you can either install the package 8 | locally (via `pip install .`), or by executing the `hatch_build.py` file. 9 | """ 10 | 11 | def find_local_pyawaitable() -> str: 12 | """Find the directory containing the local copy of pyawaitable.h""" 13 | top_level = Path(__file__).parent.parent 14 | source = top_level / "src" / "pyawaitable" 15 | if not (source / "pyawaitable.h").exists(): 16 | raise RuntimeError(NOT_FOUND) 17 | 18 | return str(source.absolute()) 19 | 20 | if __name__ == "__main__": 21 | setup( 22 | ext_modules=[ 23 | Extension( 24 | "_pyawaitable_test", 25 | glob("*.c"), 26 | include_dirs=[find_local_pyawaitable()], 27 | extra_compile_args=["-O0", "-g3"] 28 | ) 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /tests/test_awaitable.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "pyawaitable_test.h" 4 | 5 | static PyObject * 6 | generic_awaitable(PyObject *self, PyObject *coro) 7 | { 8 | PyObject *awaitable = PyAwaitable_New(); 9 | if (awaitable == NULL) { 10 | return NULL; 11 | } 12 | 13 | if (coro != Py_None) { 14 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 15 | Py_DECREF(awaitable); 16 | return NULL; 17 | } 18 | } 19 | 20 | return awaitable; 21 | } 22 | 23 | static PyObject * 24 | test_awaitable_new(PyObject *self, PyObject *nothing) 25 | { 26 | PyObject *awaitable = PyAwaitable_New(); 27 | if (awaitable == NULL) { 28 | return NULL; 29 | } 30 | 31 | TEST_ASSERT(Py_IS_TYPE(awaitable, PyAwaitable_GetType())); 32 | PyAwaitable_Cancel(awaitable); // Prevent warning 33 | Py_DECREF(awaitable); 34 | 35 | Test_SetNoMemory(); 36 | PyObject *fail_alloc = PyAwaitable_New(); 37 | Test_UnSetNoMemory(); 38 | EXPECT_ERROR(PyExc_MemoryError); 39 | TEST_ASSERT(fail_alloc == NULL); 40 | Py_RETURN_NONE; 41 | } 42 | 43 | static PyObject * 44 | test_set_result(PyObject *self, PyObject *nothing) 45 | { 46 | PyObject *awaitable = PyAwaitable_New(); 47 | if (awaitable == NULL) { 48 | return NULL; 49 | } 50 | 51 | PyObject *value = PyLong_FromLong(42); 52 | if (value == NULL) { 53 | Py_DECREF(awaitable); 54 | return NULL; 55 | } 56 | 57 | if (PyAwaitable_SetResult(awaitable, value) < 0) { 58 | Py_DECREF(value); 59 | Py_DECREF(awaitable); 60 | return NULL; 61 | } 62 | 63 | TEST_ASSERT(Py_REFCNT(value) > 1); 64 | Py_DECREF(value); 65 | return Test_RunAndCheck(awaitable, value); 66 | } 67 | 68 | static PyObject * 69 | test_add_await(PyObject *self, PyObject *coro) 70 | { 71 | PyObject *awaitable = PyAwaitable_New(); 72 | if (awaitable == NULL) { 73 | return NULL; 74 | } 75 | 76 | TEST_ASSERT(PyAwaitable_AddAwait(awaitable, self, NULL, NULL) < 0); 77 | EXPECT_ERROR(PyExc_TypeError); 78 | 79 | TEST_ASSERT(PyAwaitable_AddAwait(awaitable, NULL, NULL, NULL) < 0); 80 | EXPECT_ERROR(PyExc_ValueError); 81 | 82 | TEST_ASSERT(PyAwaitable_AddAwait(awaitable, awaitable, NULL, NULL) < 0); 83 | EXPECT_ERROR(PyExc_ValueError); 84 | 85 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 86 | Py_DECREF(awaitable); 87 | return NULL; 88 | } 89 | 90 | return Test_RunAwaitable(awaitable); 91 | } 92 | 93 | static PyObject * 94 | test_add_await_special_cases(PyObject *self, PyObject *coro) 95 | { 96 | PyObject *awaitable = PyAwaitable_New(); 97 | if (awaitable == NULL) { 98 | return NULL; 99 | } 100 | 101 | // Exhaust any preallocated buffers 102 | for (int i = 0; i < 16; ++i) { 103 | PyObject *dummy = PyAwaitable_New(); 104 | if (dummy == NULL) { 105 | return NULL; 106 | } 107 | 108 | if (PyAwaitable_AddAwait(awaitable, dummy, NULL, NULL) < 0) { 109 | Py_DECREF(awaitable); 110 | Py_DECREF(dummy); 111 | return NULL; 112 | } 113 | 114 | TEST_ASSERT(Py_REFCNT(dummy) > 1); 115 | Py_DECREF(dummy); 116 | } 117 | #if PY_MINOR_VERSION < 11 118 | /* Apparently, it's not a MemoryError for exceptions on <3.11 */ 119 | #define EXPECT_ERROR_NOMEM(exc) EXPECT_ERROR(exc) 120 | #else 121 | #define EXPECT_ERROR_NOMEM(exc) EXPECT_ERROR(PyExc_MemoryError) 122 | #endif 123 | 124 | int res; 125 | Test_SetNoMemory(); 126 | res = PyAwaitable_AddAwait(awaitable, NULL, NULL, NULL); 127 | Test_UnSetNoMemory(); 128 | EXPECT_ERROR_NOMEM(PyExc_ValueError); 129 | TEST_ASSERT(res < 0); 130 | 131 | Test_SetNoMemory(); 132 | res = PyAwaitable_AddAwait(awaitable, awaitable, NULL, NULL); 133 | Test_UnSetNoMemory(); 134 | EXPECT_ERROR_NOMEM(PyExc_ValueError); 135 | TEST_ASSERT(res < 0); 136 | 137 | Test_SetNoMemory(); 138 | res = PyAwaitable_AddAwait(awaitable, coro, NULL, NULL); 139 | Test_UnSetNoMemory(); 140 | EXPECT_ERROR_NOMEM(PyExc_TypeError); 141 | TEST_ASSERT(res < 0); 142 | 143 | // Actually await it to prevent the warning 144 | if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { 145 | Py_DECREF(awaitable); 146 | return NULL; 147 | } 148 | 149 | return Test_RunAwaitable(awaitable); 150 | } 151 | 152 | static PyObject * 153 | coroutine_trampoline(PyObject *self, PyObject *coro) 154 | { 155 | TEST_ASSERT(Py_IS_TYPE(coro, &PyCoro_Type)); 156 | PyObject *awaitable = Test_NewAwaitableWithCoro(coro, NULL, NULL); 157 | return awaitable; 158 | } 159 | 160 | TESTS(awaitable) = { 161 | TEST_UTIL(generic_awaitable), 162 | TEST(test_awaitable_new), 163 | TEST(test_set_result), 164 | TEST_CORO(test_add_await), 165 | TEST_CORO(test_add_await_special_cases), 166 | TEST_UTIL(coroutine_trampoline), 167 | {NULL} 168 | }; 169 | -------------------------------------------------------------------------------- /tests/test_callbacks.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "pyawaitable_test.h" 4 | 5 | static int PyAwaitable_thread_local callback_called = 0; 6 | static int PyAwaitable_thread_local error_callback_called = 0; 7 | 8 | static int 9 | simple_callback(PyObject *awaitable, PyObject *value) 10 | { 11 | TEST_ASSERT_INT(awaitable != NULL); 12 | TEST_ASSERT_INT(value == Py_None); 13 | TEST_ASSERT_INT(Py_IS_TYPE(awaitable, PyAwaitable_GetType())); 14 | TEST_ASSERT_INT(callback_called == 0); 15 | callback_called = 1; 16 | return 0; 17 | } 18 | 19 | static int 20 | aborting_callback(PyObject *awaitable, PyObject *value) 21 | { 22 | _PyObject_Dump(value); 23 | Py_FatalError("Test case shouldn't have ever reached here!"); 24 | return 0; 25 | } 26 | 27 | static int 28 | error_callback(PyObject *awaitable, PyObject *err) 29 | { 30 | TEST_ASSERT_INT(!PyErr_Occurred()); 31 | TEST_ASSERT_INT(awaitable != NULL); 32 | TEST_ASSERT_INT(Py_IS_TYPE(awaitable, PyAwaitable_GetType())); 33 | TEST_ASSERT_INT(err != NULL); 34 | TEST_ASSERT_INT( 35 | Py_IS_TYPE( 36 | err, 37 | (PyTypeObject *)PyExc_ZeroDivisionError 38 | ) 39 | ); 40 | TEST_ASSERT_INT(error_callback_called == 0); 41 | error_callback_called = 1; 42 | return 0; 43 | } 44 | 45 | static int 46 | failing_callback(PyObject *awaitable, PyObject *value) 47 | { 48 | PyErr_SetNone(PyExc_ZeroDivisionError); 49 | return -1; 50 | } 51 | 52 | static int 53 | failing_callback_no_error(PyObject *awaitable, PyObject *value) 54 | { 55 | return -1; 56 | } 57 | 58 | static int 59 | failing_callback_force(PyObject *awaitable, PyObject *value) 60 | { 61 | PyErr_SetNone(PyExc_ZeroDivisionError); 62 | return -2; 63 | } 64 | 65 | static int 66 | repropagating_error_callback(PyObject *awaitable, PyObject *err) 67 | { 68 | TEST_ASSERT_INT(!PyErr_Occurred()); 69 | TEST_ASSERT_INT(awaitable != NULL); 70 | TEST_ASSERT_INT(Py_IS_TYPE(awaitable, PyAwaitable_GetType())); 71 | TEST_ASSERT_INT(err != NULL); 72 | TEST_ASSERT_INT( 73 | Py_IS_TYPE( 74 | err, 75 | (PyTypeObject *)PyExc_ZeroDivisionError 76 | ) 77 | ); 78 | TEST_ASSERT_INT(error_callback_called == 0); 79 | error_callback_called = 1; 80 | return -1; 81 | } 82 | 83 | static int 84 | overwriting_error_callback(PyObject *awaitable, PyObject *err) 85 | { 86 | TEST_ASSERT_INT(!PyErr_Occurred()); 87 | TEST_ASSERT_INT(awaitable != NULL); 88 | TEST_ASSERT_INT(Py_IS_TYPE(awaitable, PyAwaitable_GetType())); 89 | TEST_ASSERT_INT(err != NULL); 90 | TEST_ASSERT_INT( 91 | Py_IS_TYPE( 92 | err, 93 | (PyTypeObject *)PyExc_ZeroDivisionError 94 | ) 95 | ); 96 | TEST_ASSERT_INT(error_callback_called == 0); 97 | error_callback_called = 1; 98 | // Some random exception that probably won't occur elsewhere 99 | PyErr_SetNone(PyExc_FileExistsError); 100 | return -2; 101 | } 102 | 103 | static PyObject * 104 | test_callback_is_called(PyObject *self, PyObject *coro) 105 | { 106 | callback_called = 0; 107 | PyObject *awaitable = Test_NewAwaitableWithCoro( 108 | coro, 109 | simple_callback, 110 | aborting_callback 111 | ); 112 | if (awaitable == NULL) { 113 | return NULL; 114 | } 115 | PyObject *res = Test_RunAwaitable(awaitable); 116 | if (res == NULL) { 117 | return NULL; 118 | } 119 | Py_DECREF(res); 120 | TEST_ASSERT(callback_called == 1); 121 | Py_RETURN_NONE; 122 | } 123 | 124 | static PyObject * 125 | test_callback_not_invoked_when_exception(PyObject *self, PyObject *coro) 126 | { 127 | PyObject *awaitable = Test_NewAwaitableWithCoro( 128 | coro, 129 | aborting_callback, 130 | NULL 131 | ); 132 | if (awaitable == NULL) { 133 | return NULL; 134 | } 135 | PyObject *res = Test_RunAwaitable(awaitable); 136 | EXPECT_ERROR(PyExc_ZeroDivisionError); 137 | Py_RETURN_NONE; 138 | } 139 | 140 | static PyObject * 141 | test_error_callback_not_invoked_when_ok(PyObject *self, PyObject *coro) 142 | { 143 | callback_called = 0; 144 | PyObject *awaitable = Test_NewAwaitableWithCoro( 145 | coro, 146 | simple_callback, 147 | aborting_callback 148 | ); 149 | if (awaitable == NULL) { 150 | return NULL; 151 | } 152 | PyObject *res = Test_RunAwaitable(awaitable); 153 | if (res == NULL) { 154 | return NULL; 155 | } 156 | Py_DECREF(res); 157 | TEST_ASSERT(callback_called == 1); 158 | Py_RETURN_NONE; 159 | } 160 | 161 | static PyObject * 162 | test_error_callback_gets_exception_from_coro(PyObject *self, PyObject *coro) 163 | { 164 | error_callback_called = 0; 165 | PyObject *awaitable = Test_NewAwaitableWithCoro( 166 | coro, 167 | aborting_callback, 168 | error_callback 169 | ); 170 | PyObject *res = Test_RunAwaitable(awaitable); 171 | if (res == NULL) { 172 | return NULL; 173 | } 174 | Py_DECREF(res); 175 | TEST_ASSERT(error_callback_called == 1); 176 | Py_RETURN_NONE; 177 | } 178 | 179 | static PyObject * 180 | test_failing_error_callback_repropagates_exception( 181 | PyObject *self, 182 | PyObject *coro 183 | ) 184 | { 185 | error_callback_called = 0; 186 | PyObject *awaitable = Test_NewAwaitableWithCoro( 187 | coro, 188 | aborting_callback, 189 | repropagating_error_callback 190 | ); 191 | PyObject *res = Test_RunAwaitable(awaitable); 192 | EXPECT_ERROR(PyExc_ZeroDivisionError); 193 | TEST_ASSERT(res == NULL); 194 | TEST_ASSERT(error_callback_called == 1); 195 | Py_RETURN_NONE; 196 | } 197 | 198 | static PyObject * 199 | test_error_callback_can_overwrite_exception( 200 | PyObject *self, 201 | PyObject *coro 202 | ) 203 | { 204 | error_callback_called = 0; 205 | PyObject *awaitable = Test_NewAwaitableWithCoro( 206 | coro, 207 | aborting_callback, 208 | overwriting_error_callback 209 | ); 210 | PyObject *res = Test_RunAwaitable(awaitable); 211 | EXPECT_ERROR(PyExc_FileExistsError); 212 | TEST_ASSERT(res == NULL); 213 | TEST_ASSERT(error_callback_called == 1); 214 | Py_RETURN_NONE; 215 | } 216 | 217 | static PyObject * 218 | test_failing_callback_gives_to_error_callback(PyObject *self, PyObject *coro) 219 | { 220 | error_callback_called = 0; 221 | PyObject *awaitable = Test_NewAwaitableWithCoro( 222 | coro, 223 | failing_callback, 224 | error_callback 225 | ); 226 | PyObject *res = Test_RunAwaitable(awaitable); 227 | if (res == NULL) { 228 | return NULL; 229 | } 230 | Py_DECREF(res); 231 | TEST_ASSERT(error_callback_called == 1); 232 | Py_RETURN_NONE; 233 | } 234 | 235 | static PyObject * 236 | test_failing_callback_with_no_exception(PyObject *self, PyObject *coro) 237 | { 238 | PyObject *awaitable = Test_NewAwaitableWithCoro( 239 | coro, 240 | failing_callback_no_error, 241 | aborting_callback 242 | ); 243 | PyObject *res = Test_RunAwaitable(awaitable); 244 | EXPECT_ERROR(PyExc_SystemError); 245 | TEST_ASSERT(res == NULL); 246 | Py_RETURN_NONE; 247 | } 248 | 249 | static PyObject * 250 | test_forcefully_propagating_callback_error(PyObject *self, PyObject *coro) 251 | { 252 | PyObject *awaitable = Test_NewAwaitableWithCoro( 253 | coro, 254 | failing_callback_force, 255 | aborting_callback 256 | ); 257 | PyObject *res = Test_RunAwaitable(awaitable); 258 | EXPECT_ERROR(PyExc_ZeroDivisionError); 259 | TEST_ASSERT(res == NULL); 260 | Py_RETURN_NONE; 261 | } 262 | 263 | TESTS(callbacks) = { 264 | TEST_CORO(test_callback_is_called), 265 | TEST_RAISING_CORO(test_callback_not_invoked_when_exception), 266 | TEST_CORO(test_error_callback_not_invoked_when_ok), 267 | TEST_RAISING_CORO(test_error_callback_gets_exception_from_coro), 268 | TEST_RAISING_CORO(test_failing_error_callback_repropagates_exception), 269 | TEST_RAISING_CORO(test_error_callback_can_overwrite_exception), 270 | TEST_CORO(test_failing_callback_gives_to_error_callback), 271 | TEST_CORO(test_failing_callback_with_no_exception), 272 | TEST_CORO(test_forcefully_propagating_callback_error), 273 | {NULL} 274 | }; 275 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any, Callable 3 | from collections.abc import Awaitable, Coroutine 4 | import inspect 5 | from pytest import raises, warns 6 | 7 | NOT_FOUND = """ 8 | The PyAwaitable test package wasn't built! 9 | Please install it with `pip install ./tests` 10 | """ 11 | try: 12 | import _pyawaitable_test 13 | except ImportError as err: 14 | raise RuntimeError(NOT_FOUND) from err 15 | 16 | def test_awaitable_semantics(): 17 | awaitable = _pyawaitable_test.generic_awaitable(None) 18 | assert isinstance(awaitable, Awaitable) 19 | assert isinstance(awaitable, Coroutine) 20 | # It's not a *native* coroutine 21 | assert inspect.iscoroutine(awaitable) is False 22 | 23 | with warns(ResourceWarning): 24 | del awaitable 25 | 26 | called = False 27 | async def dummy(): 28 | await asyncio.sleep(0) 29 | nonlocal called 30 | called = True 31 | 32 | assert asyncio.run(_pyawaitable_test.generic_awaitable(dummy())) is None 33 | assert called is True 34 | 35 | 36 | async def raising_coroutine() -> None: 37 | await asyncio.sleep(0) 38 | raise ZeroDivisionError() 39 | 40 | async def dummy_coroutine() -> None: 41 | await asyncio.sleep(0) 42 | 43 | 44 | def test_coroutine_propagates_exception(): 45 | awaitable = _pyawaitable_test.coroutine_trampoline(raising_coroutine()) 46 | with raises(ZeroDivisionError): 47 | asyncio.run(awaitable) 48 | 49 | 50 | def coro_wrap_call(method: Callable[[Awaitable[Any]], Any], corofunc: Callable[[], Awaitable[Any]]) -> Callable[[], None]: 51 | def wrapper(*_: Any) -> None: 52 | method(corofunc()) 53 | 54 | return wrapper 55 | 56 | def shim_c_function(testfunc: Callable[[], Any]) -> Any: 57 | def shim(): 58 | testfunc() 59 | 60 | return shim 61 | 62 | for method in dir(_pyawaitable_test): 63 | if not method.startswith("test_"): 64 | continue 65 | 66 | case: Callable[..., None] = getattr(_pyawaitable_test, method) 67 | if method.endswith("needs_coro"): 68 | globals()[method.rstrip("_needs_coro")] = coro_wrap_call(case, dummy_coroutine) 69 | elif method.endswith("needs_rcoro"): 70 | globals()[method.rstrip("_needs_rcoro")] = coro_wrap_call(case, raising_coroutine) 71 | else: 72 | # Wrap it with a Python function for pytest, because it can't handle C 73 | # functions for some reason. 74 | globals()[method] = shim_c_function(case) 75 | -------------------------------------------------------------------------------- /tests/test_util.h: -------------------------------------------------------------------------------- 1 | #ifndef PYAWAITABLE_TEST_UTIL_H 2 | #define PYAWAITABLE_TEST_UTIL_H 3 | 4 | #include 5 | #include 6 | 7 | #define TEST(name) {#name, name, METH_NOARGS, NULL} 8 | #define TEST_UTIL(name) {#name, name, METH_O, NULL} 9 | #define TEST_CORO(name) {#name "_needs_coro", name, METH_O, NULL} 10 | #define TEST_RAISING_CORO(name) {#name "_needs_rcoro", name, METH_O, NULL} 11 | #define TEST_ERROR(msg) \ 12 | PyErr_Format( \ 13 | PyExc_AssertionError, \ 14 | "%s (" __FILE__ ":%d): " msg, \ 15 | __func__, \ 16 | __LINE__ \ 17 | ); 18 | #define TEST_ASSERT_RETVAL(cond, retval) \ 19 | do { \ 20 | if (!(cond)) { \ 21 | PyErr_Format( \ 22 | PyExc_AssertionError, \ 23 | "assertion failed in %s (" __FILE__ ":%d): " #cond, \ 24 | __func__, \ 25 | __LINE__ \ 26 | ); \ 27 | return retval; \ 28 | } \ 29 | } while (0) 30 | #define TEST_ASSERT(cond) TEST_ASSERT_RETVAL(cond, NULL) 31 | #define TEST_ASSERT_INT(cond) TEST_ASSERT_RETVAL(cond, -1) 32 | #define TESTS(name) PyMethodDef _pyawaitable_test_ ## name [] 33 | #define EXPECT_ERROR(tp) \ 34 | do { \ 35 | if (!PyErr_Occurred()) { \ 36 | TEST_ERROR( \ 37 | "expected " #tp " to be raised, but nothing happened" \ 38 | ); \ 39 | return NULL; \ 40 | } \ 41 | if (!PyErr_ExceptionMatches((PyObject *)tp)) { \ 42 | /* Let the unexpected error fall through */ \ 43 | return NULL; \ 44 | } \ 45 | PyErr_Clear(); \ 46 | } while (0) 47 | 48 | void Test_SetNoMemory(void); 49 | void Test_UnSetNoMemory(void); 50 | PyObject *Test_RunAwaitable(PyObject *awaitable); 51 | 52 | PyObject * 53 | _Test_RunAndCheck( 54 | PyObject *awaitable, 55 | PyObject *expected, 56 | const char *func, 57 | const char *file, 58 | int line 59 | ); 60 | 61 | #define Test_RunAndCheck(aw, ex) \ 62 | _Test_RunAndCheck( \ 63 | aw, \ 64 | ex, \ 65 | __func__, \ 66 | __FILE__, \ 67 | __LINE__ \ 68 | ); 69 | 70 | PyObject * 71 | Test_NewAwaitableWithCoro( 72 | PyObject *coro, 73 | PyAwaitable_Callback callback, 74 | PyAwaitable_Error error 75 | ); 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /tests/test_values.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "pyawaitable_test.h" 4 | 5 | static PyObject * 6 | test_store_and_load_object_values(PyObject *self, PyObject *nothing) 7 | { 8 | PyObject *awaitable = PyAwaitable_New(); 9 | if (awaitable == NULL) { 10 | return NULL; 11 | } 12 | 13 | PyObject *num = PyLong_FromLong(999); 14 | if (num == NULL) { 15 | Py_DECREF(awaitable); 16 | return NULL; 17 | } 18 | 19 | PyObject *str = PyUnicode_FromString("hello"); 20 | if (str == NULL) { 21 | Py_DECREF(awaitable); 22 | Py_DECREF(num); 23 | return NULL; 24 | } 25 | 26 | if (PyAwaitable_SaveValues(awaitable, 2, num, str) < 0) { 27 | Py_DECREF(awaitable); 28 | Py_DECREF(num); 29 | Py_DECREF(str); 30 | return NULL; 31 | } 32 | Py_DECREF(num); 33 | Py_DECREF(str); 34 | TEST_ASSERT(Py_REFCNT(num) >= 1); 35 | TEST_ASSERT(Py_REFCNT(str) >= 1); 36 | 37 | PyObject *num_unpacked; 38 | PyObject *str_unpacked; 39 | 40 | if ( 41 | PyAwaitable_UnpackValues( 42 | awaitable, 43 | &num_unpacked, 44 | &str_unpacked 45 | ) < 0 46 | ) { 47 | Py_DECREF(awaitable); 48 | return NULL; 49 | } 50 | 51 | TEST_ASSERT(num_unpacked == num); 52 | TEST_ASSERT(str_unpacked == str); 53 | PyAwaitable_Cancel(awaitable); 54 | Py_DECREF(awaitable); 55 | Py_RETURN_NONE; 56 | } 57 | 58 | static PyObject * 59 | test_object_values_can_outlive_awaitable(PyObject *self, PyObject *nothing) 60 | { 61 | PyObject *awaitable = PyAwaitable_New(); 62 | if (awaitable == NULL) { 63 | return NULL; 64 | } 65 | 66 | PyObject *dummy = 67 | PyUnicode_FromString("nobody expects the spanish inquisition"); 68 | if (dummy == NULL) { 69 | Py_DECREF(awaitable); 70 | return NULL; 71 | } 72 | 73 | if (PyAwaitable_SaveValues(awaitable, 1, dummy) < 0) { 74 | Py_DECREF(awaitable); 75 | Py_DECREF(dummy); 76 | return NULL; 77 | } 78 | 79 | PyAwaitable_Cancel(awaitable); 80 | Py_DECREF(awaitable); 81 | assert( 82 | PyUnicode_CompareWithASCIIString( 83 | dummy, 84 | "nobody expects the spanish inquisition" 85 | ) 86 | ); 87 | Py_DECREF(dummy); 88 | Py_RETURN_NONE; 89 | } 90 | 91 | static PyObject * 92 | test_store_and_load_arbitrary_values(PyObject *self, PyObject *nothing) 93 | { 94 | PyObject *awaitable = PyAwaitable_New(); 95 | if (awaitable == NULL) { 96 | return NULL; 97 | } 98 | 99 | int *ptr = malloc(sizeof(int)); 100 | if (ptr == NULL) { 101 | Py_DECREF(awaitable); 102 | PyErr_NoMemory(); 103 | return NULL; 104 | } 105 | 106 | *ptr = 42; 107 | if (PyAwaitable_SaveArbValues(awaitable, 2, ptr, NULL) < 0) { 108 | Py_DECREF(awaitable); 109 | free(ptr); 110 | return NULL; 111 | } 112 | 113 | int *ptr_unpacked; 114 | void *null_unpacked; 115 | if ( 116 | PyAwaitable_UnpackArbValues( 117 | awaitable, 118 | &ptr_unpacked, 119 | &null_unpacked 120 | ) < 0 121 | ) { 122 | Py_DECREF(awaitable); 123 | free(ptr); 124 | return NULL; 125 | } 126 | TEST_ASSERT(ptr_unpacked == ptr); 127 | TEST_ASSERT((*ptr_unpacked) == 42); 128 | TEST_ASSERT(null_unpacked == NULL); 129 | free(ptr); 130 | PyAwaitable_Cancel(awaitable); 131 | Py_DECREF(awaitable); 132 | Py_RETURN_NONE; 133 | } 134 | 135 | static PyObject * 136 | test_load_fails_when_no_values(PyObject *self, PyObject *nothing) 137 | { 138 | PyObject *awaitable = PyAwaitable_New(); 139 | PyAwaitable_Cancel(awaitable); 140 | 141 | int fail = PyAwaitable_UnpackValues(awaitable); 142 | EXPECT_ERROR(PyExc_RuntimeError); 143 | TEST_ASSERT(fail == -1); 144 | 145 | int fail_arb = PyAwaitable_UnpackArbValues(awaitable); 146 | EXPECT_ERROR(PyExc_RuntimeError); 147 | TEST_ASSERT(fail_arb == -1); 148 | 149 | Py_DECREF(awaitable); 150 | Py_RETURN_NONE; 151 | } 152 | 153 | static PyObject * 154 | test_load_arbitrary_null_pointer(PyObject *self, PyObject *nothing) 155 | { 156 | PyObject *awaitable = PyAwaitable_New(); 157 | PyAwaitable_Cancel(awaitable); 158 | 159 | // It doesn't matter what this is, it just needs to be unique 160 | int sentinel; 161 | void *dummy = &sentinel; 162 | 163 | if (PyAwaitable_SaveArbValues(awaitable, 3, dummy, dummy, dummy) < 0) { 164 | Py_DECREF(awaitable); 165 | return NULL; 166 | } 167 | 168 | void *only_unpack; 169 | if (PyAwaitable_UnpackArbValues(awaitable, NULL, &only_unpack, NULL) < 0) { 170 | Py_DECREF(awaitable); 171 | return NULL; 172 | } 173 | 174 | TEST_ASSERT(only_unpack == dummy); 175 | Py_DECREF(awaitable); 176 | Py_RETURN_NONE; 177 | } 178 | 179 | static PyObject * 180 | test_get_and_set_arbitrary_values(PyObject *self, PyObject *nothing) 181 | { 182 | PyObject *awaitable = PyAwaitable_New(); 183 | PyAwaitable_Cancel(awaitable); 184 | 185 | int sentinel; 186 | void *dummy = &sentinel; 187 | 188 | if (PyAwaitable_SaveArbValues(awaitable, 3, dummy, NULL, dummy) < 0) { 189 | Py_DECREF(awaitable); 190 | return NULL; 191 | } 192 | 193 | TEST_ASSERT(PyAwaitable_GetArbValue(awaitable, 0) == dummy); 194 | TEST_ASSERT(PyAwaitable_GetArbValue(awaitable, 1) == NULL); 195 | TEST_ASSERT(PyAwaitable_GetArbValue(awaitable, 2) == dummy); 196 | 197 | if (PyAwaitable_SetArbValue(awaitable, 1, dummy) < 0) { 198 | Py_DECREF(awaitable); 199 | return NULL; 200 | } 201 | 202 | TEST_ASSERT(PyAwaitable_GetArbValue(awaitable, 1) == dummy); 203 | 204 | void *fail = PyAwaitable_GetArbValue(awaitable, 3); 205 | EXPECT_ERROR(PyExc_IndexError); 206 | TEST_ASSERT(fail == NULL); 207 | 208 | int other_fail = PyAwaitable_SetArbValue(awaitable, 3, dummy); 209 | EXPECT_ERROR(PyExc_IndexError); 210 | TEST_ASSERT(other_fail < 0); 211 | 212 | Py_DECREF(awaitable); 213 | Py_RETURN_NONE; 214 | } 215 | 216 | static PyObject * 217 | test_get_and_set_object_values(PyObject *self, PyObject *nothing) 218 | { 219 | PyObject *awaitable = PyAwaitable_New(); 220 | PyAwaitable_Cancel(awaitable); 221 | 222 | PyObject *one = PyLong_FromLong(1); 223 | if (one == NULL) { 224 | Py_DECREF(awaitable); 225 | return NULL; 226 | } 227 | 228 | PyObject *str = PyUnicode_FromString("hello world"); 229 | if (str == NULL) { 230 | Py_DECREF(one); 231 | Py_DECREF(awaitable); 232 | return NULL; 233 | } 234 | 235 | if (PyAwaitable_SaveValues(awaitable, 2, one, str) < 0) { 236 | Py_DECREF(awaitable); 237 | Py_DECREF(one); 238 | Py_DECREF(str); 239 | return NULL; 240 | } 241 | TEST_ASSERT(Py_REFCNT(one) >= 2); 242 | TEST_ASSERT(Py_REFCNT(str) >= 2); 243 | Py_DECREF(one); 244 | Py_DECREF(str); 245 | 246 | TEST_ASSERT(PyAwaitable_GetValue(awaitable, 0) == one); 247 | TEST_ASSERT(PyAwaitable_GetValue(awaitable, 1) == str); 248 | 249 | if (PyAwaitable_SetValue(awaitable, 1, one) < 0) { 250 | Py_DECREF(awaitable); 251 | return NULL; 252 | } 253 | 254 | TEST_ASSERT(PyAwaitable_GetValue(awaitable, 1) == one); 255 | TEST_ASSERT(Py_REFCNT(one) >= 2); 256 | 257 | PyObject *fail = PyAwaitable_GetValue(awaitable, 3); 258 | EXPECT_ERROR(PyExc_IndexError); 259 | TEST_ASSERT(fail == NULL); 260 | 261 | int other_fail = PyAwaitable_SetValue(awaitable, 3, one); 262 | EXPECT_ERROR(PyExc_IndexError); 263 | TEST_ASSERT(other_fail < 0); 264 | 265 | Py_DECREF(awaitable); 266 | Py_RETURN_NONE; 267 | } 268 | 269 | TESTS(values) = { 270 | TEST(test_store_and_load_object_values), 271 | TEST(test_object_values_can_outlive_awaitable), 272 | TEST(test_store_and_load_arbitrary_values), 273 | TEST(test_load_fails_when_no_values), 274 | TEST(test_load_arbitrary_null_pointer), 275 | TEST(test_get_and_set_arbitrary_values), 276 | TEST(test_get_and_set_object_values), 277 | {NULL} 278 | }; 279 | -------------------------------------------------------------------------------- /tests/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | typedef struct { 5 | PyMemAllocatorEx raw; 6 | PyMemAllocatorEx mem; 7 | PyMemAllocatorEx obj; 8 | } AllocHook; 9 | 10 | // TODO: Make this thread-safe for concurrent tests 11 | AllocHook hook; 12 | 13 | static void * 14 | malloc_fail(void *ctx, size_t size) 15 | { 16 | return NULL; 17 | } 18 | 19 | static void * 20 | calloc_fail(void *ctx, size_t nitems, size_t size) 21 | { 22 | return NULL; 23 | } 24 | 25 | static void * 26 | realloc_fail(void *ctx, void *ptr, size_t newsize) 27 | { 28 | return NULL; 29 | } 30 | 31 | static void 32 | wrapped_free(void *ctx, void *ptr) 33 | { 34 | PyMemAllocatorEx *alloc = (PyMemAllocatorEx *)ctx; 35 | alloc->free(alloc->ctx, ptr); 36 | } 37 | 38 | void 39 | Test_SetNoMemory(void) 40 | { 41 | PyMemAllocatorEx alloc; 42 | alloc.malloc = malloc_fail; 43 | alloc.calloc = calloc_fail; 44 | alloc.realloc = realloc_fail; 45 | alloc.free = wrapped_free; 46 | 47 | PyMem_GetAllocator(PYMEM_DOMAIN_RAW, &hook.raw); 48 | PyMem_GetAllocator(PYMEM_DOMAIN_MEM, &hook.mem); 49 | PyMem_GetAllocator(PYMEM_DOMAIN_OBJ, &hook.obj); 50 | 51 | alloc.ctx = &hook.raw; 52 | PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc); 53 | 54 | alloc.ctx = &hook.raw; 55 | PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc); 56 | 57 | alloc.ctx = &hook.raw; 58 | PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &alloc); 59 | } 60 | 61 | void 62 | Test_UnSetNoMemory(void) 63 | { 64 | PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &hook.raw); 65 | PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &hook.mem); 66 | PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &hook.obj); 67 | } 68 | 69 | PyObject * 70 | Test_RunAwaitable(PyObject *awaitable) 71 | { 72 | PyObject *asyncio = PyImport_ImportModule("asyncio"); 73 | if (asyncio == NULL) { 74 | return NULL; 75 | } 76 | 77 | PyObject *loop = PyObject_CallMethod(asyncio, "new_event_loop", ""); 78 | Py_DECREF(asyncio); 79 | if (loop == NULL) { 80 | return NULL; 81 | } 82 | 83 | PyObject *res = PyObject_CallMethod( 84 | loop, 85 | "run_until_complete", 86 | "O", 87 | awaitable 88 | ); 89 | // Temporarily remove the error so we can close the loop 90 | PyObject *err = PyErr_GetRaisedException(); 91 | PyObject *close_res = PyObject_CallMethod(loop, "close", ""); 92 | Py_DECREF(loop); 93 | 94 | if (res == NULL) { 95 | assert(err != NULL); 96 | PyErr_SetRaisedException(err); 97 | return NULL; 98 | } 99 | 100 | if (close_res == NULL) { 101 | Py_DECREF(res); 102 | return NULL; 103 | } 104 | 105 | Py_DECREF(close_res); 106 | return res; 107 | } 108 | 109 | PyObject * 110 | _Test_RunAndCheck( 111 | PyObject *awaitable, 112 | PyObject *expected, 113 | const char *func, 114 | const char *file, 115 | int line 116 | ) 117 | { 118 | PyObject *res = Test_RunAwaitable(awaitable); 119 | Py_DECREF(awaitable); 120 | if (res == NULL) { 121 | return NULL; 122 | } 123 | if (res != expected) { 124 | PyErr_Format( 125 | PyExc_AssertionError, 126 | "test %s at %s:%d expected awaitable to return %R, got %R", 127 | func, 128 | file, 129 | line, 130 | expected, 131 | res 132 | ); 133 | Py_DECREF(res); 134 | return NULL; 135 | } 136 | Py_DECREF(res); 137 | Py_RETURN_NONE; 138 | } 139 | 140 | PyObject * 141 | Test_NewAwaitableWithCoro( 142 | PyObject *coro, 143 | PyAwaitable_Callback callback, 144 | PyAwaitable_Error error 145 | ) 146 | { 147 | PyObject *awaitable = PyAwaitable_New(); 148 | if (awaitable == NULL) { 149 | return NULL; 150 | } 151 | 152 | if (PyAwaitable_AddAwait(awaitable, coro, callback, error) < 0) { 153 | Py_DECREF(awaitable); 154 | return NULL; 155 | } 156 | 157 | return awaitable; 158 | } 159 | -------------------------------------------------------------------------------- /uncrustify.cfg: -------------------------------------------------------------------------------- 1 | # PEP 7 Compliant Uncrustify Configuration 2 | 3 | # General code size 4 | code_width = 79 5 | input_tab_size = 8 6 | output_tab_size = 4 7 | indent_columns = 4 8 | indent_with_tabs = 0 9 | 10 | # Ugly Newlines 11 | nl_struct_brace = remove 12 | nl_else_brace = remove 13 | nl_brace_else = force 14 | nl_if_brace = remove 15 | nl_else_if = remove 16 | nl_for_brace = remove 17 | nl_while_brace = remove 18 | nl_before_opening_brace_func_class_def = force 19 | nl_after_case = true 20 | nl_case_colon_brace = remove 21 | 22 | # Misc Newlines 23 | nl_multi_line_sparen_open = force 24 | nl_multi_line_sparen_close = force 25 | nl_after_func_body = 2 26 | nl_after_struct = 1 27 | nl_assign_leave_one_liners = true 28 | nl_assign_brace = remove 29 | 30 | # Operators 31 | sp_arith = force 32 | sp_assign = force 33 | sp_compare = force 34 | sp_before_byref = force 35 | sp_after_byref = remove 36 | indent_ternary_operator = 2 37 | 38 | # Parens 39 | sp_before_sparen = force 40 | sp_inside_sparen = remove 41 | indent_paren_close = 2 42 | 43 | # Semicolons 44 | sp_before_semi = remove 45 | sp_before_semi_for = remove 46 | sp_before_semi_for_empty = remove 47 | sp_between_semi_for_empty = remove 48 | 49 | # Square Brackets 50 | sp_before_square = remove 51 | sp_before_vardef_square = remove 52 | sp_inside_square = remove 53 | 54 | # Commas 55 | sp_before_comma = remove 56 | sp_after_comma = force 57 | sp_paren_comma = remove 58 | 59 | # Braces 60 | sp_inside_braces_empty = remove 61 | sp_sparen_brace = force 62 | nl_assign_brace = remove 63 | sp_brace_else = force 64 | sp_else_brace = force 65 | 66 | # Pointers 67 | sp_before_ptr_star = force 68 | sp_before_unnamed_ptr_star = force 69 | sp_after_ptr_star = remove 70 | sp_ptr_star_paren = remove 71 | sp_between_ptr_star = remove 72 | 73 | # Function Definitions 74 | sp_type_func = force 75 | nl_func_def_paren = remove 76 | nl_func_type_name = force 77 | nl_func_leave_one_liners = true 78 | nl_func_def_args_multi_line = true 79 | nl_func_def_start_multi_line = true 80 | nl_func_def_end_multi_line = true 81 | donot_indent_func_def_close_paren = true 82 | 83 | # Function Declarations 84 | nl_func_decl_start_multi_line = true 85 | nl_func_decl_args_multi_line = true 86 | nl_func_decl_end_multi_line = true 87 | 88 | # Macros 89 | sp_macro = force 90 | sp_macro_func = remove 91 | indent_macro_brace = true 92 | nl_multi_line_define = true 93 | nl_define_macro = true 94 | align_nl_cont = 1 95 | 96 | # Function Calls 97 | nl_func_call_empty = remove 98 | nl_func_call_paren = remove 99 | nl_func_call_start_multi_line = true 100 | nl_func_call_end_multi_line = true 101 | nl_func_call_start_multi_line = true 102 | nl_func_call_args_multi_line = true 103 | indent_func_call_param = true 104 | indent_paren_after_func_call = false 105 | indent_align_paren = false 106 | --------------------------------------------------------------------------------