diff --git a/.clang-format b/.clang-format deleted file mode 100644 index e384528..0000000 --- a/.clang-format +++ /dev/null @@ -1 +0,0 @@ -DisableFormat: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee234a3..a01c0a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,102 +1,59 @@ -name: Build Wheels +name: Build on: - push: - tags: - - v* - branches: - - master - paths: - - 'src/**' - pull_request: - branches: - - master - paths: - - '.github/workflows/build.yml' + push: + tags: + - v* + branches: + - master + paths: + - 'src/**' + pull_request: + branches: + - master concurrency: - group: build-${{ github.head_ref }} - cancel-in-progress: true - -env: - CIBW_BUILD: cp3{9,10,11,12,13}-* - PYAWAITABLE_OPTIMIZED: 1 + group: build-${{ github.head_ref }} + cancel-in-progress: true jobs: - binary-wheels-standard: - name: Binary wheels for ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - uses: actions/checkout@v2 - with: - # Fetch all tags - fetch-depth: 0 - - - name: Build wheels - uses: pypa/cibuildwheel@v2.21.3 - env: - CIBW_ARCHS_MACOS: x86_64 - HATCH_BUILD_HOOKS_ENABLE: "true" - - - uses: actions/upload-artifact@v4 - with: - name: artifacts - path: wheelhouse/*.whl - if-no-files-found: error - - binary-wheels-arm: - name: Build Linux wheels for ARM - runs-on: ubuntu-latest - if: > - github.event_name == 'push' - && - (github.ref == 'refs/heads/master' || startsWith(github.event.ref, 'refs/tags')) - - steps: - - uses: actions/checkout@v2 - with: - # Fetch all tags - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - with: - platforms: arm64 - - - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 - env: - CIBW_ARCHS_LINUX: aarch64 - HATCH_BUILD_HOOKS_ENABLE: "true" - - - uses: actions/upload-artifact@v4 - with: - name: artifacts - path: wheelhouse/*.whl - if-no-files-found: error - - publish: - name: Publish release - needs: - - binary-wheels-standard - - binary-wheels-arm - runs-on: ubuntu-latest - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') - - steps: - - uses: actions/download-artifact@v3 - with: - name: artifacts - path: dist - - - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@v1.10.3 - with: - skip_existing: true - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + pure-python-wheel-and-sdist: + name: Build a pure Python wheel and source distribution + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install build dependencies + run: python -m pip install --upgrade build + + - name: Build + run: python -m build + + - uses: actions/upload-artifact@v4 + with: + name: artifacts + path: dist/* + if-no-files-found: error + + publish: + name: Publish release + needs: + - pure-python-wheel-and-sdist + runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + + steps: + - uses: actions/download-artifact@v4 + with: + name: artifacts + path: dist + + - name: Push build artifacts to PyPI + uses: pypa/gh-action-pypi-publish@v1.12.4 + with: + skip_existing: true + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7f3707c..d11a910 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,10 +5,8 @@ on: branches: - master pull_request: - types: - - "opened" - - "reopened" - - "synchronize" + branches: + - master concurrency: group: test-${{ github.head_ref }} @@ -35,8 +33,6 @@ jobs: filters: | source: - 'src/**' - csource: - - 'src/_pyawaitable/**' run-tests: needs: changes @@ -56,82 +52,14 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install Pytest - run: pip install pytest pytest-asyncio typing_extensions - - name: Build PyAwaitable - run: pip install . + run: pip install . --verbose - name: Build PyAwaitable Test Package - run: pip install setuptools wheel && pip install ./tests/extension/ --no-build-isolation + run: pip install ./tests --verbose - name: Run tests - run: pytest -W error - - memory-errors: - needs: - - changes - - run-tests - if: ${{ needs.changes.outputs.csource == 'true' }} - name: Check for memory errors - runs-on: ubuntu-latest - env: - PYTHONMALLOC: malloc - steps: - - uses: actions/checkout@v2 - - - name: Set up Python 3.12 - uses: actions/setup-python@v2 - with: - python-version: 3.12 - - - name: Install Pytest - run: | - pip install pytest pytest-asyncio pytest-memray typing_extensions - shell: bash - - - name: Build PyAwaitable - run: pip install . - - - name: Build PyAwaitable Test Package - run: pip install setuptools wheel && pip install tests/extension/ --no-build-isolation - - - name: Install Valgrind - run: sudo apt-get update && sudo apt-get -y install valgrind - - - name: Run tests with Valgrind - run: valgrind --suppressions=valgrind-python.supp --error-exitcode=1 pytest -x - - memory-leaks: - needs: - - changes - - memory-errors - if: ${{ needs.changes.outputs.csource == 'true' }} - name: Check for memory leaks - runs-on: ubuntu-latest - env: - PYTHONMALLOC: malloc - steps: - - uses: actions/checkout@v2 - - - name: Set up Python 3.12 - uses: actions/setup-python@v2 - with: - python-version: 3.12 - - - name: Install Pytest - run: | - pip install pytest pytest-asyncio pytest-memray typing_extensions - shell: bash - - - name: Build PyAwaitable - run: pip install . - - - name: Build PyAwaitable Test Package - run: pip install setuptools wheel && pip install tests/extension/ --no-build-isolation - - - name: Run tests with Memray tracking - run: pytest --enable-leak-tracking -W error --stacks=50 --native + run: python -W error -m unittest tests/main.py --verbose tests-pass: runs-on: ubuntu-latest @@ -140,12 +68,10 @@ jobs: needs: - run-tests - - memory-errors - - memory-leaks steps: - name: Check whether all tests passed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} - allowed-skips: ${{ toJSON(needs) }} \ No newline at end of file + allowed-skips: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index e728fa7..46251f6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,10 @@ __pycache__/ *.egg-info test/ dist/ -pyawaitable-vendor/ *.so +src/pyawaitable/pyawaitable.h +pcbuild/ +*.o # LSP compile_flags.txt @@ -17,6 +19,7 @@ build/ *.sln *.user *.vcxproj* +.clang-format # Misc test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fddb6f6..1cf519d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ # Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +- Moved away from function pointer tables for loading PyAwaitable--everything is now vendored upon installation. +- Improved performance with compiler optimizations. +- `PyAwaitable_` prefixes are now required, and the old `pyawaitable_*` functions have been removed. +- The warning emitted when a PyAwaitable object is not awaited is now a `ResourceWarning` (was a `RuntimeWarning`). +- `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. +- **Breaking Change:** `PyAwaitable_Init` no longer takes a module object. +- **Breaking Change:** Renamed `awaitcallback` to `PyAwaitable_Callback` +- **Breaking Change:** Renamed `awaitcallback_err` to `PyAwaitable_Error` +- **Breaking Change:** Renamed `defercallback` to `PyAwaitable_Defer` ## [1.4.0] - 2025-02-09 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 081763f..a1b740b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,20 +48,8 @@ It's highly recommended to do this inside of a [virtual environment](https://doc ## Running Tests -PyAwaitable uses three libraries for unit testing: - -- [pytest](https://docs.pytest.org/en/8.2.x/), as the general testing framework. -- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/en/latest/), for asynchronous tests. -- [pytest-memray](https://pytest-memray.readthedocs.io/en/latest/), for detection of memory leaks. Note this isn't available for Windows, so simply omit this in your installation. - -Installation is trivial: - -``` -$ pip install pytest pytest-asyncio pytest-memray -``` - -Tests generally access the PyAwaitable API functions using [ctypes](https://docs.python.org/3/library/ctypes.html), but there's also an extension module solely built for tests called `_pyawaitable_test`. You can install this with the following command: +PyAwaitable uses [Hatch](https://hatch.pypa.io), so that will handle everything for you: ``` -$ pip install setuptools wheel && pip install ./test/extension/ --no-build-isolation +$ hatch test ``` diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b910558..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include src/pyawaitable/*.h \ No newline at end of file diff --git a/README.md b/README.md index 1a2b6fc..988ce57 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Call asynchronous code from an extension module -[![Build Wheels](https://github.com/ZeroIntensity/pyawaitable/actions/workflows/build.yml/badge.svg)](https://github.com/ZeroIntensity/pyawaitable/actions/workflows/build.yml) +[![Build](https://github.com/ZeroIntensity/pyawaitable/actions/workflows/build.yml/badge.svg)](https://github.com/ZeroIntensity/pyawaitable/actions/workflows/build.yml) ![Tests](https://github.com/ZeroIntensity/pyawaitable/actions/workflows/tests.yml/badge.svg) - [Docs](https://awaitable.zintensity.dev) @@ -13,7 +13,7 @@ PyAwaitable is the *only* library to support writing and calling asynchronous Python functions from pure C code (with the exception of manually implementing an awaitable class from scratch, which is essentially what PyAwaitable does). -It was originally designed to be directly part of CPython - you can read the [scrapped PEP](https://gist.github.com/ZeroIntensity/8d32e94b243529c7e1c27349e972d926) about it. Since this library only uses the public ABI, it's better fit outside of CPython, as a library. +It was originally designed to be directly part of CPython--you can read the [scrapped PEP](https://gist.github.com/ZeroIntensity/8d32e94b243529c7e1c27349e972d926) about it. Since this library only uses the public ABI, it's better fit outside of CPython, as a library. ## Installation @@ -24,10 +24,6 @@ Add it to your project's build process: [build-system] requires = ["setuptools", "pyawaitable"] build-backend = "setuptools.build_meta" - -[project] -# ... -dependencies = ["pyawaitable"] ``` Include it in your extension: @@ -46,40 +42,22 @@ if __name__ == "__main__": ## Example ```c -#define PYAWAITABLE_PYAPI #include -// Assuming that this is using METH_O +/* Usage from Python: await my_async_function(coro()) */ static PyObject * -hello(PyObject *self, PyObject *coro) { - // Make our awaitable object +my_async_function(PyObject *self, PyObject *coro) { + /* Make our awaitable object */ PyObject *awaitable = PyAwaitable_New(); - if (awaitable == NULL) { - return NULL; - } - - // Mark the coroutine for being awaited - if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { - Py_DECREF(awaitable); - return NULL; - } + /* Mark the coroutine for being awaited */ + PyAwaitable_AddAwait(awaitable, coro, NULL, NULL); - // Return the awaitable object to yield to the event loop + /* Return the awaitable object to yield to the event loop */ return awaitable; } ``` -```py -# Assuming top-level await -async def coro(): - await asyncio.sleep(1) - print("awaited from C!") - -# Use our C function to await it -await hello(coro()) -``` - ## Copyright `pyawaitable` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/SECURITY.md b/SECURITY.md index 1633f0a..5a7cd70 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,4 +6,4 @@ Breaking API changes are made *only* between major versions. Deprecations may be ## Reporting a Vulnerability -Depending on the severity of the vulnerability, you can make an issue on the [issue tracker](https://github.com/ZeroIntensity/pyawaitable/issues), or send an email explaining the vulnerability to +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 \ No newline at end of file diff --git a/docs/utilities.md b/docs/utilities.md index da0a0ed..c07785b 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -9,40 +9,6 @@ hide: So far, it might seem like this is a lot of boilerplate. That's unfortunately decently common for the C API, but you get used to it. With that being said, how can we help eliminate at least _some_ of this boilerplate? -## Calling - -In general, calling functions in the C API is a lot of work--is there any way to make it prettier in PyAwaitable? CPython has `PyObject_CallFunction`, which allows you to call a function with a format string similar to `Py_BuildValue`. For example: - -```c -static PyObject * -test(PyObject *self, PyObject *my_func) -{ - // Equivalent to my_func(42, -10) - PyObject *result = PyObject_CallFunction(my_func, "ii", 42, -10); - /* ... */ -} -``` - -For convenience, PyAwaitable has an analogue of this function, called `pyawaitable_await_function`, which calls a function with a format string _and_ marks the result (as in, the returned coroutine) for execution via `pyawaitable_await`. For example, if `my_func` from above was asynchronous: - -```c -static PyObject * -test(PyObject *self, PyObject *my_func) -{ - PyObject *awaitable = pyawaitable_new(); - - // Equivalent to await my_func(42, -10) - if (pyawaitable_await_function(awaitable, my_func, "ii", NULL, NULL, 42, -10) < 0) - { - Py_DECREF(awaitable); - return NULL; - } - /* ... */ -} -``` - -Much nicer, right? - ## Asynchronous Contexts What about using `async with` from C? Well, asynchronous context managers are sort of simple, you just have to deal with calling `__aenter__` and `__aexit__`. But that's no fun--can we do it automatically? Yes you can! diff --git a/hatch.toml b/hatch.toml new file mode 100644 index 0000000..1728ec1 --- /dev/null +++ b/hatch.toml @@ -0,0 +1,21 @@ +[version] +path = "src/pyawaitable/__init__.py" + +[build.targets.wheel] +packages = ["src/pyawaitable"] + +[build.targets.sdist] +only-include = ["src/pyawaitable", "include/", "src/_pyawaitable"] + +[build.targets.wheel.hooks.autorun] +dependencies = ["hatch-autorun"] +code = """ +import pyawaitable +import os + +os.environ['PYAWAITABLE_INCLUDE'] = pyawaitable.include(suppress_error=True) +""" + +[build.hooks.custom] +enable-by-default = true +dependencies = ["typing_extensions"] diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 0000000..d9fc7ab --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,422 @@ +""" +PyAwaitable Vendoring Script +""" + +import os +from pathlib import Path +from typing import Callable, TextIO, TypeVar + +try: + from typing import ParamSpec +except ImportError as err: + # Let's hope it's installed! + from typing_extensions import ParamSpec +import re +from contextlib import contextmanager +import functools +import textwrap + +try: + from hatchling.builders.hooks.plugin.interface import BuildHookInterface +except ImportError: + + class BuildHookInterface: + pass + + +DIST_PATH: str = "src/pyawaitable/pyawaitable.h" +HEADER_FILES: list[str] = [ + "optimize.h", + "dist.h", + "array.h", + "backport.h", + "coro.h", + "awaitableobject.h", + "genwrapper.h", + "values.h", + "with.h", + "init.h", +] +SOURCE_FILES: list[Path] = [ + Path("./src/_pyawaitable/array.c"), + Path("./src/_pyawaitable/coro.c"), + Path("./src/_pyawaitable/awaitable.c"), + Path("./src/_pyawaitable/genwrapper.c"), + Path("./src/_pyawaitable/values.c"), + Path("./src/_pyawaitable/with.c"), + Path("./src/_pyawaitable/init.c"), +] + +INCLUDE_REGEX = re.compile(r"#include <(.+)>") +FUNCTION_REGEX = re.compile(r"(.+)\(.*\).*") +INTERNAL_FUNCTION_REGEX = re.compile( + r"_PyAwaitable_INTERNAL\(.+\)\n(.+)\(.*\).*" +) +INTERNAL_DATA_REGEX = re.compile(r"_PyAwaitable_INTERNAL_DATA\(.+\) (.+)") +EXPLICIT_REGEX = re.compile(r".*_PyAwaitable_MANGLE\((.+)\).*") +NO_EXPLICIT_REGEX = re.compile(r".*_PyAwaitable_NO_MANGLE\((.+)\).*") +DEFINE_REGEX = re.compile(r" *# *define *(\w+)(\(.*\))?.*") + +HEADER_GUARD = """ +#if !defined(PYAWAITABLE_VENDOR_H) && !defined(Py_LIMITED_API) +#define PYAWAITABLE_VENDOR_H +#define _PYAWAITABLE_VENDOR +""" +HEADER = ( + lambda version: f"""\ +/* + * PyAwaitable - Autogenerated distribution copy of version {version} + * + * Docs: https://awaitable.zintensity.dev + * Source: https://github.com/ZeroIntensity/pyawaitable + */ +""" +) +MANGLED = "__PyAwaitable_Mangled_" + +_LOG_NEST = 0 + + +@contextmanager +def logging_context(): + global _LOG_NEST + assert _LOG_NEST >= 0 + try: + _LOG_NEST += 1 + yield + finally: + _LOG_NEST -= 1 + + +T = TypeVar("T") +P = ParamSpec("P") + + +def new_context(message: str) -> Callable[[Callable[P, T]], Callable[P, T]]: + def decorator_factory(func: Callable[P, T]) -> Callable[P, T]: + @functools.wraps(func) + def decorator(*args: P.args, **kwargs: P.kwargs): + log(message) + with logging_context(): + return func(*args, **kwargs) + + return decorator + + return decorator_factory + + +def log(*text: str) -> None: + indent = " " * _LOG_NEST + print(indent + " ".join(text)) + + +def write(fp: TextIO, value: str) -> None: + fp.write(value + "\n") + log(f"Wrote {len(value.encode('utf-8'))} bytes to {fp.name}") + + +@new_context("Finding includes...") +def find_includes(lines: list[str], includes: set[str]) -> None: + for line in lines.copy(): + match = INCLUDE_REGEX.match(line) + if match: + lines.remove(line) + include = match.group(1) + if include.startswith("pyawaitable/"): + # Won't exist in the vendor + continue + + includes.add(include) + + +@new_context("Finding source file macros...") +def find_defines(lines: list[str], defines: set[str]) -> None: + for line in lines: + match = DEFINE_REGEX.match(line) + if not match: + continue + + defines.add(match.group(1)) + + +def filter_name(name: str) -> bool: + return name.startswith("__") + + +@new_context("Processing explicitly marked name...") +def mangle_explicit(changed_names: dict[str, str], line: str) -> None: + explicit = EXPLICIT_REGEX.match(line) + if explicit is None: + raise RuntimeError( + f"{line} does not follow _PyAwaitable_MANGLE correctly" + ) + + name = explicit.group(1) + if filter_name(name): + return + changed_names[name] = MANGLED + name + log(f"Marked {name} for mangling") + + +@new_context("Processing _PyAwaitable_INTERNAL function...") +def mangle_internal( + changed_names: dict[str, str], lines: list[str], index: int +) -> None: + try: + func_def = INTERNAL_FUNCTION_REGEX.match( + lines[index] + "\n" + lines[index + 1] + ) + except IndexError: + return + + if func_def is None: + return + + name = func_def.group(1) + if filter_name(name): + return + changed_names[name] = "__PyAwaitable_Internal_" + name + log(f"Marked {name} for mangling") + + +@new_context("Processing internal data...") +def mangle_internal_data(changed_names: dict[str, str], line: str) -> None: + internal_data = INTERNAL_DATA_REGEX.match(line) + if internal_data is None: + raise RuntimeError( + f"{line} does not follow _PyAwaitable_INTERNAL_DATA correctly" + ) + + name = internal_data.group(1) + changed_names[name] = "__PyAwaitable_InternalData_" + name + log(f"Marked {name} for mangling") + + +@new_context("Processing static function...") +def mangle_static( + changed_names: dict[str, str], lines: list[str], index: int +) -> None: + try: + line = lines[index + 1] + except IndexError: + return + + if NO_EXPLICIT_REGEX.match(line) is not None: + return + + func_def = FUNCTION_REGEX.match(line) + + if func_def is None: + return + + name = func_def.group(1) + if filter_name(name): + return + changed_names[name] = "__PyAwaitable_Static_" + name + log(f"Marked {name} for mangling") + + +@new_context("Calculating mangled names...") +def mangle_names(changed_names: dict[str, str], lines: list[str]) -> None: + for index, line in enumerate(lines): + if line.startswith("#define"): + continue + + if "_PyAwaitable_MANGLE" in line: + mangle_explicit(changed_names, line) + elif "_PyAwaitable_INTERNAL" in line: + mangle_internal(changed_names, lines, index) + elif "_PyAwaitable_INTERNAL_DATA" in line: + mangle_internal_data(changed_names, line) + elif line.startswith("static"): + mangle_static(changed_names, lines, index) + + +def orderize_mangled(changed_names: dict[str, str]) -> dict[str, str]: + result: dict[str, str] = {} + orders: list[tuple[str, int]] = [] + + for name in changed_names.keys(): + # Count how many times other keys go into name + amount = 0 + for second_name in changed_names.keys(): + if second_name in name: + amount += 1 + + orders.append((name, amount)) + + orders.sort(key=lambda item: item[1]) + for index, data in enumerate(orders.copy()): + name, amount = data + if changed_names[name].startswith(MANGLED): + # Always do explicit mangles last + orders.insert(0, orders.pop(index)) + + for name, amount in reversed(orders): + result[name] = changed_names[name] + + return result + + +DOUBLE_MANGLE = "__PyAwaitable_Mangled___PyAwaitable_Mangled_" + + +def clean_mangled(text: str) -> str: + return text.replace(DOUBLE_MANGLE, MANGLED) + + +def process_files(fp: TextIO) -> None: + includes = set[str]() + to_write: list[str] = [] + log("Processing header files...") + changed_names: dict[str, str] = {} + source_macros: set[str] = set() + + with logging_context(): + for header_file in HEADER_FILES: + header_file = "include/pyawaitable" / Path(header_file) + log(f"Processing {header_file}") + lines: list[str] = header_file.read_text(encoding="utf-8").split( + "\n" + ) + find_includes(lines, includes) + mangle_names(changed_names, lines) + to_write.append("\n".join(lines)) + + log("Processing source files...") + with logging_context(): + for source_file in SOURCE_FILES: + lines: list[str] = source_file.read_text(encoding="utf-8").split( + "\n" + ) + log(f"Processing {source_file}") + find_includes(lines, includes) + mangle_names(changed_names, lines) + find_defines(lines, source_macros) + to_write.append("\n".join(lines)) + + log("Writing macros...") + with logging_context(): + for include in includes: + assert not include.startswith( + "pyawaitable/" + ), "found pyawaitable headers somehow" + write(fp, f"#include <{include}>") + + log("Writing mangled names...") + with logging_context(): + for name, new_name in orderize_mangled(changed_names).items(): + for index, line in enumerate(to_write): + to_write[index] = clean_mangled(line.replace(name, new_name)) + + for line in to_write: + write(fp, line) + + log("Writing macro cleanup...") + with logging_context(): + for define in source_macros: + write(fp, f"#undef {define}") + + +FINAL = "0xF" +BETA = "0xB" +ALPHA = "0xA" +RELEASE_LEVEL = re.compile(r"([A-z]+)([0-9])*") +RELEASE_LEVELS = { + "dev": ALPHA, + "alpha": ALPHA, + "a": ALPHA, + "beta": BETA, + "b": BETA, +} + + +def deduce_release_level(part: str) -> tuple[str, str]: + part = part.replace(".", "-") + dev = part.split("-", maxsplit=1) + if len(dev) == 1: + # No release level attached, assume final release + return FINAL, "0" + + release = dev[1] + match = RELEASE_LEVEL.match(release) + if not match: + raise RuntimeError(f"version did not match expression: {release}") + + name = match.group(1).lower() + level = RELEASE_LEVELS.get(name) + if not level: + raise RuntimeError(f"{name} is not a valid release level") + number = match.group(2) + + # Sanity check + if number and not number.isdigit(): + raise RuntimeError(f"{number} is not a valid number") + + if number == "": + number = "0" + + amount = 2 if level == BETA else 3 + number = ("0" * amount) + number + + return level, number + + +def clean_micro_version(micro: str) -> str: + return micro.replace(".", "-").split("-", maxsplit=1)[0] + + +def main(version: str) -> None: + dist = Path(DIST_PATH) + if dist.exists(): + log(f"{dist} already exists, removing it...") + os.remove(dist) + log("Creating vendored copy of pyawaitable...") + + major, minor, micro = version.split(".", maxsplit=2) + release_level, release_number = deduce_release_level(micro) + micro = clean_micro_version(micro) + + version_text = textwrap.dedent( + f""" + #define PyAwaitable_MAJOR_VERSION {major} + #define PyAwaitable_MINOR_VERSION {minor} + #define PyAwaitable_MICRO_VERSION {micro} + #define PyAwaitable_PATCH_VERSION PyAwaitable_MICRO_VERSION + #define PyAwaitable_RELEASE_LEVEL {release_level} + #define PyAwaitable_MAGIC_NUMBER {major}{minor}{micro}{release_number} + """ + ) + + with open(dist, "w", encoding="utf-8") as f: + with logging_context(): + write(f, HEADER(version) + HEADER_GUARD + version_text) + process_files(f) + write( + f, + """#else +#error "the limited API cannot be used with pyawaitable" +#endif /* PYAWAITABLE_VENDOR_H */""", + ) + + log(f"Created PyAwaitable distribution at {dist}") + + +class CustomBuildHook(BuildHookInterface): + PLUGIN_NAME = "PyAwaitable Build" + + def clean(self, _: list[str]) -> None: + dist = Path(DIST_PATH) + if dist.exists(): + os.remove(dist) + + def initialize(self, _: str, build_data: dict) -> None: + self.clean([]) + main(self.metadata.version) + build_data["force_include"][DIST_PATH] = DIST_PATH + + +if __name__ == "__main__": + from src.pyawaitable import __version__ + + main(__version__) diff --git a/include/pyawaitable/array.h b/include/pyawaitable/array.h index 795fcda..d77fc06 100644 --- a/include/pyawaitable/array.h +++ b/include/pyawaitable/array.h @@ -4,19 +4,21 @@ #include #include -#define pyawaitable_array_DEFAULT_SIZE 16 +#include +#include + +#define _pyawaitable_array_DEFAULT_SIZE 16 /* * Deallocator for items on a pyawaitable_array structure. A NULL pointer * will never be given to the deallocator. */ -typedef void (*pyawaitable_array_deallocator)(void *); +typedef void (*_PyAwaitable_MANGLE(pyawaitable_array_deallocator))(void *); /* - * Internal only dynamic array for CPython. + * Internal only dynamic array for PyAwaitable. */ -typedef struct -{ +typedef struct { /* * The actual items in the dynamic array. * Don't access this field publicly to get @@ -37,7 +39,7 @@ typedef struct * This may be NULL. */ pyawaitable_array_deallocator deallocator; -} pyawaitable_array; +} _PyAwaitable_MANGLE(pyawaitable_array); /* Zero out the array */ @@ -74,9 +76,9 @@ pyawaitable_array_ASSERT_INDEX(pyawaitable_array *array, Py_ssize_t index) * * Returns -1 upon failure, 0 otherwise. */ -int +_PyAwaitable_INTERNAL(int) pyawaitable_array_init_with_size( - pyawaitable_array *array, + pyawaitable_array * array, pyawaitable_array_deallocator deallocator, Py_ssize_t initial ); @@ -87,7 +89,8 @@ pyawaitable_array_init_with_size( * Returns -1 upon failure, 0 otherwise. * If this fails, the deallocator is not ran on the item. */ -int pyawaitable_array_append(pyawaitable_array *array, void *item); +_PyAwaitable_INTERNAL(int) +pyawaitable_array_append(pyawaitable_array * array, void *item); /* * Insert an item at the target index. The index @@ -96,16 +99,16 @@ int pyawaitable_array_append(pyawaitable_array *array, void *item); * Returns -1 upon failure, 0 otherwise. * If this fails, the deallocator is not ran on the item. */ -int +_PyAwaitable_INTERNAL(int) pyawaitable_array_insert( - pyawaitable_array *array, + pyawaitable_array * array, Py_ssize_t index, void *item ); /* Remove all items from the array. */ -void -pyawaitable_array_clear_items(pyawaitable_array *array); +_PyAwaitable_INTERNAL(void) +pyawaitable_array_clear_items(pyawaitable_array * array); /* * Clear all the fields on the array. @@ -116,7 +119,8 @@ pyawaitable_array_clear_items(pyawaitable_array *array); * It's safe to call pyawaitable_array_init() or init_with_size() again * on the array after calling this. */ -void pyawaitable_array_clear(pyawaitable_array *array); +_PyAwaitable_INTERNAL(void) +pyawaitable_array_clear(pyawaitable_array * array); /* * Set a value at index in the array. @@ -126,8 +130,8 @@ void pyawaitable_array_clear(pyawaitable_array *array); * * This cannot fail. */ -void -pyawaitable_array_set(pyawaitable_array *array, Py_ssize_t index, void *item); +_PyAwaitable_INTERNAL(void) +pyawaitable_array_set(pyawaitable_array * array, Py_ssize_t index, void *item); /* * Remove the item at the index, and call the deallocator on it (if the array @@ -135,8 +139,8 @@ pyawaitable_array_set(pyawaitable_array *array, Py_ssize_t index, void *item); * * This cannot fail. */ -void -pyawaitable_array_remove(pyawaitable_array *array, Py_ssize_t index); +_PyAwaitable_INTERNAL(void) +pyawaitable_array_remove(pyawaitable_array * array, Py_ssize_t index); /* * Remove the item at the index *without* deallocating it, and @@ -144,8 +148,8 @@ pyawaitable_array_remove(pyawaitable_array *array, Py_ssize_t index); * * This cannot fail. */ -void * -pyawaitable_array_pop(pyawaitable_array *array, Py_ssize_t index); +_PyAwaitable_INTERNAL(void *) +pyawaitable_array_pop(pyawaitable_array * array, Py_ssize_t index); /* * Clear all the fields on a dynamic array, and then @@ -175,7 +179,7 @@ pyawaitable_array_init( return pyawaitable_array_init_with_size( array, deallocator, - pyawaitable_array_DEFAULT_SIZE + _pyawaitable_array_DEFAULT_SIZE ); } @@ -192,13 +196,11 @@ pyawaitable_array_new_with_size( ) { pyawaitable_array *array = PyMem_Malloc(sizeof(pyawaitable_array)); - if (array == NULL) - { + if (PyAwaitable_UNLIKELY(array == NULL)) { return NULL; } - if (pyawaitable_array_init_with_size(array, deallocator, initial) < 0) - { + if (pyawaitable_array_init_with_size(array, deallocator, initial) < 0) { PyMem_Free(array); return NULL; } @@ -218,7 +220,7 @@ pyawaitable_array_new(pyawaitable_array_deallocator deallocator) { return pyawaitable_array_new_with_size( deallocator, - pyawaitable_array_DEFAULT_SIZE + _pyawaitable_array_DEFAULT_SIZE ); } @@ -238,7 +240,7 @@ pyawaitable_array_GET_ITEM(pyawaitable_array *array, Py_ssize_t index) /* * Get the length of the array. This cannot fail. */ -static inline Py_ssize_t +static inline Py_ssize_t PyAwaitable_PURE pyawaitable_array_LENGTH(pyawaitable_array *array) { pyawaitable_array_ASSERT_VALID(array); diff --git a/include/pyawaitable/awaitableobject.h b/include/pyawaitable/awaitableobject.h index e22ee83..b2a4a68 100644 --- a/include/pyawaitable/awaitableobject.h +++ b/include/pyawaitable/awaitableobject.h @@ -5,21 +5,20 @@ #include #include +#include -typedef int (*awaitcallback)(PyObject *, PyObject *); -typedef int (*awaitcallback_err)(PyObject *, PyObject *); -typedef int (*defer_callback)(PyObject *); +typedef int (*PyAwaitable_Callback)(PyObject *, PyObject *); +typedef int (*PyAwaitable_Error)(PyObject *, PyObject *); +typedef int (*PyAwaitable_Defer)(PyObject *); -typedef struct _pyawaitable_callback -{ +typedef struct _pyawaitable_callback { PyObject *coro; - awaitcallback callback; - awaitcallback_err err_callback; + PyAwaitable_Callback callback; + PyAwaitable_Error err_callback; bool done; -} pyawaitable_callback; +} _PyAwaitable_MANGLE(pyawaitable_callback); -struct _PyAwaitableObject -{ +struct _PyAwaitableObject { PyObject_HEAD pyawaitable_array aw_callbacks; @@ -42,35 +41,29 @@ struct _PyAwaitableObject }; typedef struct _PyAwaitableObject PyAwaitableObject; -extern PyTypeObject _PyAwaitableType; +_PyAwaitable_INTERNAL_DATA(PyTypeObject) PyAwaitable_Type; -int pyawaitable_set_result_impl(PyObject *awaitable, PyObject *result); +_PyAwaitable_API(int) +PyAwaitable_SetResult(PyObject * awaitable, PyObject * result); -int pyawaitable_await_impl( - PyObject *aw, - PyObject *coro, - awaitcallback cb, - awaitcallback_err err +_PyAwaitable_API(int) +PyAwaitable_AddAwait( + PyObject * aw, + PyObject * coro, + PyAwaitable_Callback cb, + PyAwaitable_Error err ); -int pyawaitable_defer_await_impl(PyObject *aw, defer_callback cb); +_PyAwaitable_API(int) +PyAwaitable_DeferAwait(PyObject * aw, PyAwaitable_Defer cb); -void pyawaitable_cancel_impl(PyObject *aw); +_PyAwaitable_API(void) +PyAwaitable_Cancel(PyObject * aw); -PyObject * -awaitable_next(PyObject *self); +_PyAwaitable_INTERNAL(PyObject *) +awaitable_next(PyObject * self); -PyObject * -pyawaitable_new_impl(void); - -int -pyawaitable_await_function_impl( - PyObject *awaitable, - PyObject *func, - const char *fmt, - awaitcallback cb, - awaitcallback_err err, - ... -); +_PyAwaitable_API(PyObject *) +PyAwaitable_New(void); #endif diff --git a/include/pyawaitable/backport.h b/include/pyawaitable/backport.h index 82c517c..7083235 100644 --- a/include/pyawaitable/backport.h +++ b/include/pyawaitable/backport.h @@ -2,36 +2,47 @@ #define PYAWAITABLE_BACKPORT_H #include +#include -#ifndef _PyObject_Vectorcall -#define PYAWAITABLE_NEEDS_VECTORCALL -PyObject *_PyObject_VectorcallBackport( - PyObject *obj, - PyObject **args, - size_t nargsf, - PyObject *kwargs -); +#ifndef Py_NewRef +static inline PyObject * +_PyAwaitable_NO_MANGLE(Py_NewRef)(PyObject *o) +{ + Py_INCREF(o); + return o; +} -#define PyObject_CallNoArgs(o) PyObject_CallObject(o, NULL) -#define PyObject_Vectorcall _PyObject_VectorcallBackport -#define PyObject_VectorcallDict _PyObject_FastCallDict #endif -#if PY_VERSION_HEX < 0x030c0000 -PyObject *PyErr_GetRaisedException(void); -void PyErr_SetRaisedException(PyObject *err); +#ifndef Py_XNewRef +static inline PyObject * +_PyAwaitable_NO_MANGLE(Py_XNewRef)(PyObject *o) +{ + Py_XINCREF(o); + return o; +} #endif -#ifndef Py_NewRef -#define PYAWAITABLE_NEEDS_NEWREF -PyObject *Py_NewRef_Backport(PyObject *o); -#define Py_NewRef Py_NewRef_Backport -#endif +#if PY_VERSION_HEX < 0x030c0000 +static PyObject * +_PyAwaitable_NO_MANGLE(PyErr_GetRaisedException)(void) +{ + PyObject *type, *val, *tb; + PyErr_Fetch(&type, &val, &tb); + PyErr_NormalizeException(&type, &val, &tb); + Py_XDECREF(type); + Py_XDECREF(tb); + // technically some entry in the traceback might be lost; ignore that + return val; +} -#ifndef Py_XNewRef -#define PYAWAITABLE_NEEDS_XNEWREF -PyObject *Py_XNewRef_Backport(PyObject *o); -#define Py_XNewRef Py_XNewRef_Backport +static void +_PyAwaitable_NO_MANGLE(PyErr_SetRaisedException)(PyObject *err) +{ + // NOTE: We need to incref the type object here, even though + // this function steals a reference to err. + PyErr_Restore(Py_NewRef((PyObject *) Py_TYPE(err)), err, NULL); +} #endif #endif diff --git a/include/pyawaitable/coro.h b/include/pyawaitable/coro.h index 5646d7e..a6c21d0 100644 --- a/include/pyawaitable/coro.h +++ b/include/pyawaitable/coro.h @@ -2,8 +2,11 @@ #define PYAWAITABLE_CORO_H #include +#include -extern PyMethodDef pyawaitable_methods[]; -extern PyAsyncMethods pyawaitable_async_methods; +#ifndef _PYAWAITABLE_VENDOR +_PyAwaitable_INTERNAL_DATA(PyMethodDef) pyawaitable_methods[]; +#endif +_PyAwaitable_INTERNAL_DATA(PyAsyncMethods) pyawaitable_async_methods; #endif diff --git a/include/pyawaitable/dist.h b/include/pyawaitable/dist.h new file mode 100644 index 0000000..8afd6d1 --- /dev/null +++ b/include/pyawaitable/dist.h @@ -0,0 +1,26 @@ +#ifndef PYAWAITABLE_DIST_H +#define PYAWAITABLE_DIST_H + +#if PY_MINOR_VERSION < 9 +#error \ + "Python 3.8 and older are no longer supported, please use Python 3.9 or newer." +#endif + +#ifdef _PYAWAITABLE_VENDOR +#define _PyAwaitable_API(ret) static ret +#define _PyAwaitable_INTERNAL(ret) static ret +#define _PyAwaitable_INTERNAL_DATA(tp) static tp +#define _PyAwaitable_INTERNAL_DATA_DEF(tp) static tp +#else +/* These are for IDEs */ +#define _PyAwaitable_API(ret) ret +#define _PyAwaitable_INTERNAL(ret) ret +#define _PyAwaitable_INTERNAL_DATA(tp) extern tp +#define _PyAwaitable_INTERNAL_DATA_DEF(tp) tp +#define PyAwaitable_MAGIC_NUMBER 0 +#endif + +#define _PyAwaitable_MANGLE(name) name +#define _PyAwaitable_NO_MANGLE(name) name + +#endif diff --git a/include/pyawaitable/genwrapper.h b/include/pyawaitable/genwrapper.h index 967eee7..0de8db9 100644 --- a/include/pyawaitable/genwrapper.h +++ b/include/pyawaitable/genwrapper.h @@ -3,25 +3,26 @@ #include #include +#include -extern PyTypeObject _PyAwaitableGenWrapperType; +_PyAwaitable_INTERNAL_DATA(PyTypeObject) _PyAwaitableGenWrapperType; -typedef struct _GenWrapperObject -{ +typedef struct _GenWrapperObject { PyObject_HEAD PyAwaitableObject *gw_aw; PyObject *gw_current_await; -} GenWrapperObject; +} _PyAwaitable_MANGLE(GenWrapperObject); -PyObject * -genwrapper_next(PyObject *self); +_PyAwaitable_INTERNAL(PyObject *) +_PyAwaitableGenWrapper_Next(PyObject * self); -int genwrapper_fire_err_callback( - PyObject *self, - awaitcallback_err err_callback +_PyAwaitable_INTERNAL(int) +_PyAwaitableGenWrapper_FireErrCallback( + PyObject * self, + PyAwaitable_Error err_callback ); -PyObject * -genwrapper_new(PyAwaitableObject *aw); +_PyAwaitable_INTERNAL(PyObject *) +genwrapper_new(PyAwaitableObject * aw); #endif diff --git a/include/pyawaitable/init.h b/include/pyawaitable/init.h new file mode 100644 index 0000000..fa795d7 --- /dev/null +++ b/include/pyawaitable/init.h @@ -0,0 +1,19 @@ +#ifndef PYAWAITABLE_INIT_H +#define PYAWAITABLE_INIT_H + +#include +#include + +_PyAwaitable_INTERNAL(PyObject *) +_PyAwaitable_GetState(void); + +_PyAwaitable_API(PyTypeObject *) +PyAwaitable_GetType(void); + +_PyAwaitable_INTERNAL(PyTypeObject *) +_PyAwaitable_GetGenWrapperType(void); + +_PyAwaitable_API(int) +PyAwaitable_Init(void); + +#endif diff --git a/include/pyawaitable/optimize.h b/include/pyawaitable/optimize.h new file mode 100644 index 0000000..a08b81c --- /dev/null +++ b/include/pyawaitable/optimize.h @@ -0,0 +1,52 @@ +#ifndef PYAWAITABLE_OPTIMIZE_H +#define PYAWAITABLE_OPTIMIZE_H + +#if (defined(__GNUC__) && __GNUC__ >= 15) || defined(__clang__) && \ + __clang__ >= 13 +#define PyAwaitable_MUSTTAIL [[clang::musttail]] +#else +#define PyAwaitable_MUSTTAIL +#endif + +#if defined(__GNUC__) || defined(__clang__) +/* Called often */ +#define PyAwaitable_HOT __attribute__((hot)) +/* Depends only on input and memory state (i.e. makes no memory allocations */ +#define PyAwaitable_PURE __attribute__((pure)) +/* Depends only on inputs */ +#define PyAwaitable_CONST __attribute__((const)) +/* Called rarely */ +#define PyAwaitable_COLD __attribute__((cold)) +#else +#define PyAwaitable_HOT +#define PyAwaitable_PURE +#define PyAwaitable_CONST +#define PyAwaitable_COLD +#endif + +#if defined(__GNUC__) || defined(__clang__) +#include +#define PyAwaitable_UNLIKELY(x) (__builtin_expect(!!(x), false)) +#define PyAwaitable_LIKELY(x) (__builtin_expect(!!(x), true)) +#elif (defined(__cplusplus) && (__cplusplus >= 202002L)) || \ + (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) +#define PyAwaitable_UNLIKELY(x) (x)[[unlikely]] +#define PyAwaitable_LIKELY(x) (x)[[likely]] +#else +#define PyAwaitable_UNLIKELY(x) (x) +#define PyAwaitable_LIKELY(x) (x) +#endif + +#ifdef thread_local +# define PyAwaitable_thread_local thread_local +#elif __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) +# define PyAwaitable_thread_local _Thread_local +#elif defined(_MSC_VER) /* AKA NT_THREADS */ +# define PyAwaitable_thread_local __declspec(thread) +#elif defined(__GNUC__) /* includes clang */ +# define PyAwaitable_thread_local __thread +# else +#error \ + "no thread-local storage classifier is available" +#endif +#endif diff --git a/include/pyawaitable/values.h b/include/pyawaitable/values.h index 80b2848..6fb71d8 100644 --- a/include/pyawaitable/values.h +++ b/include/pyawaitable/values.h @@ -2,45 +2,80 @@ #define PYAWAITABLE_VALUES_H #include // PyObject, Py_ssize_t +#include -#define SAVE(name) int name(PyObject * awaitable, Py_ssize_t nargs, ...) -#define UNPACK(name) int name(PyObject * awaitable, ...) -#define SET(name, tp) \ - int name( \ - PyObject * awaitable, \ - Py_ssize_t index, \ - tp new_value \ - ) -#define GET(name, tp) \ - tp name( \ - PyObject * awaitable, \ - Py_ssize_t index \ - ) - -// Normal values - -SAVE(pyawaitable_save_impl); -UNPACK(pyawaitable_unpack_impl); -SET(pyawaitable_set_impl, PyObject *); -GET(pyawaitable_get_impl, PyObject *); - -// Arbitrary values - -SAVE(pyawaitable_save_arb_impl); -UNPACK(pyawaitable_unpack_arb_impl); -SET(pyawaitable_set_arb_impl, void *); -GET(pyawaitable_get_arb_impl, void *); - -// Integer values - -SAVE(pyawaitable_save_int_impl); -UNPACK(pyawaitable_unpack_int_impl); -SET(pyawaitable_set_int_impl, long); -GET(pyawaitable_get_int_impl, long); - -#undef SAVE -#undef UNPACK -#undef GET -#undef SET +/* Object values */ + +_PyAwaitable_API(int) +PyAwaitable_SaveValues( + PyObject * awaitable, + Py_ssize_t nargs, + ... +); + +_PyAwaitable_API(int) +PyAwaitable_UnpackValues(PyObject * awaitable, ...); + +_PyAwaitable_API(int) +PyAwaitable_SetValue( + PyObject * awaitable, + Py_ssize_t index, + PyObject * new_value +); +_PyAwaitable_API(PyObject *) +PyAwaitable_GetValue( + PyObject * awaitable, + Py_ssize_t index +); + +/* Arbitrary values */ + +_PyAwaitable_API(int) +PyAwaitable_SaveArbValues( + PyObject * awaitable, + Py_ssize_t nargs, + ... +); + +_PyAwaitable_API(int) +PyAwaitable_UnpackArbValues(PyObject * awaitable, ...); + +_PyAwaitable_API(int) +PyAwaitable_SetArbValue( + PyObject * awaitable, + Py_ssize_t index, + void *new_value +); + +_PyAwaitable_API(void *) +PyAwaitable_GetArbValue( + PyObject * awaitable, + Py_ssize_t index +); + +/* Integer values */ + +_PyAwaitable_API(int) +PyAwaitable_SaveIntValues( + PyObject * awaitable, + Py_ssize_t nargs, + ... +); + +_PyAwaitable_API(int) +PyAwaitable_UnpackIntValues(PyObject * awaitable, ...); + +_PyAwaitable_API(int) +PyAwaitable_SetIntValue( + PyObject * awaitable, + Py_ssize_t index, + long new_value +); + +_PyAwaitable_API(long) +PyAwaitable_GetIntValue( + PyObject * awaitable, + Py_ssize_t index +); #endif diff --git a/include/pyawaitable/with.h b/include/pyawaitable/with.h index c472d47..ea59fe0 100644 --- a/include/pyawaitable/with.h +++ b/include/pyawaitable/with.h @@ -2,14 +2,14 @@ #define PYAWAITABLE_WITH_H #include // PyObject -#include // awaitcallback, awaitcallback_err +#include // PyAwaitable_Callback, PyAwaitable_Error -int -pyawaitable_async_with_impl( - PyObject *aw, - PyObject *ctx, - awaitcallback cb, - awaitcallback_err err +_PyAwaitable_API(int) +PyAwaitable_AsyncWith( + PyObject * aw, + PyObject * ctx, + PyAwaitable_Callback cb, + PyAwaitable_Error err ); #endif diff --git a/include/vendor.h b/include/vendor.h deleted file mode 100644 index 8645bf9..0000000 --- a/include/vendor.h +++ /dev/null @@ -1,95 +0,0 @@ -#ifndef PYAWAITABLE_VENDOR_H -#define PYAWAITABLE_VENDOR_H - -#include -#include -#include - -/* - * vendor.h is only for use by the vendor build tool, don't use it manually! - * (If you're seeing this message from a vendored copy, you're fine) - */ - -#define PYAWAITABLE_ADD_TYPE(m, tp) \ - do \ - { \ - Py_INCREF(&tp); \ - if (PyType_Ready(&tp) < 0) { \ - Py_DECREF(&tp); \ - return -1; \ - } \ - if (PyModule_AddObject(m, #tp, (PyObject *) &tp) < 0) { \ - Py_DECREF(&tp); \ - return -1; \ - } \ - } while (0) - -#define PYAWAITABLE_MAJOR_VERSION 1 -#define PYAWAITABLE_MINOR_VERSION 0 -#define PYAWAITABLE_MICRO_VERSION 1 -#define PYAWAITABLE_RELEASE_LEVEL 0xF - -#ifdef PYAWAITABLE_PYAPI -#define PyAwaitable_New pyawaitable_new -#define PyAwaitable_AddAwait pyawaitable_await -#define PyAwaitable_Cancel pyawaitable_cancel -#define PyAwaitable_SetResult pyawaitable_set_result -#define PyAwaitable_SaveValues pyawaitable_save -#define PyAwaitable_SaveArbValues pyawaitable_save_arb -#define PyAwaitable_UnpackValues pyawaitable_unpack -#define PyAwaitable_UnpackArbValues pyawaitable_unpack_arb -#define PyAwaitable_Init pyawaitable_init -#define PyAwaitable_ABI pyawaitable_abi -#define PyAwaitable_Type PyAwaitableType -#define PyAwaitable_AwaitFunction pyawaitable_await_function -#define PyAwaitable_VendorInit pyawaitable_vendor_init -#endif - -static int -pyawaitable_init() -{ - PyErr_SetString( - PyExc_SystemError, - "cannot use pyawaitable_init from a vendored copy, use pyawaitable_vendor_init instead!" - ); - return -1; -} - -static void -close_pool(PyObject *Py_UNUSED(capsule)) -{ - dealloc_awaitable_pool(); -} - -static int -pyawaitable_vendor_init(PyObject *mod) -{ - PYAWAITABLE_ADD_TYPE(mod, _PyAwaitableType); - PYAWAITABLE_ADD_TYPE(mod, _PyAwaitableGenWrapperType); - - PyObject *capsule = PyCapsule_New( - pyawaitable_vendor_init, // Any pointer, except NULL - "_pyawaitable.__do_not_touch", - close_pool - ); - - if (!capsule) - { - return -1; - } - - if (PyModule_AddObject(mod, "__do_not_touch", capsule) < 0) - { - Py_DECREF(capsule); - return -1; - } - - if (alloc_awaitable_pool() < 0) - { - return -1; - } - - return 0; -} - -#endif diff --git a/pyproject.toml b/pyproject.toml index 8a2a18f..8baacc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" +requires = ["hatch", "hatchling"] +build-backend = "hatchling.build" [project] name = "pyawaitable" @@ -26,4 +26,4 @@ requires-python = ">=3.9" [project.urls] Documentation = "https://awaitable.zintensity.dev" Issues = "https://github.com/ZeroIntensity/pyawaitable/issues" -Source = "https://github.com/ZeroIntensity/pyawaitable" +Source = "https://github.com/ZeroIntensity/pyawaitable" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9a8a4ca..074d3e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +# Requirements for Netlify mkdocs -mkdocs-material +mkdocs-material \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 2a66a9a..0000000 --- a/setup.py +++ /dev/null @@ -1,26 +0,0 @@ -from glob import glob - -from setuptools import Extension, setup -import os - -DEBUG_SYMBOLS = "/DEBUG" if os.name == "nt" else "-g" -_OPTIMIZED = "/O3" if os.name == "nt" else "-O3" -_NO_OPTIMIZATION = "" if os.name == "nt" else "-O0" -OPTIMIZATION = _OPTIMIZED if os.environ.get("PYAWAITABLE_OPTIMIZED") else _NO_OPTIMIZATION - -if __name__ == "__main__": - setup( - name="pyawaitable", - license="MIT", - version="1.4.0", - ext_modules=[ - Extension( - "_pyawaitable", - glob("./src/_pyawaitable/*.c"), - include_dirs=["./include/", "./src/pyawaitable/"], - extra_compile_args=[DEBUG_SYMBOLS, OPTIMIZATION], - ) - ], - package_dir={"": "src"}, - packages=["pyawaitable"], - ) diff --git a/src/_pyawaitable/array.c b/src/_pyawaitable/array.c index 14865c0..fa89573 100644 --- a/src/_pyawaitable/array.c +++ b/src/_pyawaitable/array.c @@ -1,18 +1,18 @@ #include +#include static inline void call_deallocator_maybe(pyawaitable_array *array, Py_ssize_t index) { - if (array->deallocator != NULL && array->items[index] != NULL) - { + if (array->deallocator != NULL && array->items[index] != NULL) { array->deallocator(array->items[index]); array->items[index] = NULL; } } -int +_PyAwaitable_INTERNAL(int) pyawaitable_array_init_with_size( - pyawaitable_array *array, + pyawaitable_array * array, pyawaitable_array_deallocator deallocator, Py_ssize_t initial ) @@ -20,8 +20,7 @@ pyawaitable_array_init_with_size( assert(array != NULL); assert(initial > 0); void **items = PyMem_Calloc(sizeof(void *), initial); - if (items == NULL) - { + if (PyAwaitable_UNLIKELY(items == NULL)) { return -1; } @@ -36,16 +35,14 @@ pyawaitable_array_init_with_size( static int resize_if_needed(pyawaitable_array *array) { - if (array->length == array->capacity) - { + if (array->length == array->capacity) { // Need to resize array->capacity *= 2; void **new_items = PyMem_Realloc( array->items, sizeof(void *) * array->capacity ); - if (new_items == NULL) - { + if (PyAwaitable_UNLIKELY(new_items == NULL)) { return -1; } @@ -55,22 +52,21 @@ resize_if_needed(pyawaitable_array *array) return 0; } -int +_PyAwaitable_INTERNAL(int) PyAwaitable_PURE pyawaitable_array_append(pyawaitable_array *array, void *item) { pyawaitable_array_ASSERT_VALID(array); array->items[array->length++] = item; - if (resize_if_needed(array) < 0) - { + if (resize_if_needed(array) < 0) { array->items[--array->length] = NULL; return -1; } return 0; } -int +_PyAwaitable_INTERNAL(int) pyawaitable_array_insert( - pyawaitable_array *array, + pyawaitable_array * array, Py_ssize_t index, void *item ) @@ -78,8 +74,7 @@ pyawaitable_array_insert( pyawaitable_array_ASSERT_VALID(array); pyawaitable_array_ASSERT_INDEX(array, index); ++array->length; - if (resize_if_needed(array) < 0) - { + if (resize_if_needed(array) < 0) { // Grow the array beforehand, otherwise it's // going to be a mess putting it back together if // allocation fails. @@ -87,8 +82,7 @@ pyawaitable_array_insert( return -1; } - for (Py_ssize_t i = array->length - 1; i > index; --i) - { + for (Py_ssize_t i = array->length - 1; i > index; --i) { array->items[i] = array->items[i - 1]; } @@ -96,8 +90,8 @@ pyawaitable_array_insert( return 0; } -void -pyawaitable_array_set(pyawaitable_array *array, Py_ssize_t index, void *item) +_PyAwaitable_INTERNAL(void) +pyawaitable_array_set(pyawaitable_array * array, Py_ssize_t index, void *item) { pyawaitable_array_ASSERT_VALID(array); pyawaitable_array_ASSERT_INDEX(array, index); @@ -108,15 +102,14 @@ pyawaitable_array_set(pyawaitable_array *array, Py_ssize_t index, void *item) static void remove_no_dealloc(pyawaitable_array *array, Py_ssize_t index) { - for (Py_ssize_t i = index; i < array->length - 1; ++i) - { + for (Py_ssize_t i = index; i < array->length - 1; ++i) { array->items[i] = array->items[i + 1]; } --array->length; } -void -pyawaitable_array_remove(pyawaitable_array *array, Py_ssize_t index) +_PyAwaitable_INTERNAL(void) +pyawaitable_array_remove(pyawaitable_array * array, Py_ssize_t index) { pyawaitable_array_ASSERT_VALID(array); pyawaitable_array_ASSERT_INDEX(array, index); @@ -124,8 +117,8 @@ pyawaitable_array_remove(pyawaitable_array *array, Py_ssize_t index) remove_no_dealloc(array, index); } -void * -pyawaitable_array_pop(pyawaitable_array *array, Py_ssize_t index) +_PyAwaitable_INTERNAL(void *) +pyawaitable_array_pop(pyawaitable_array * array, Py_ssize_t index) { pyawaitable_array_ASSERT_VALID(array); pyawaitable_array_ASSERT_INDEX(array, index); @@ -134,12 +127,11 @@ pyawaitable_array_pop(pyawaitable_array *array, Py_ssize_t index) return item; } -void -pyawaitable_array_clear_items(pyawaitable_array *array) +_PyAwaitable_INTERNAL(void) +pyawaitable_array_clear_items(pyawaitable_array * array) { pyawaitable_array_ASSERT_VALID(array); - for (Py_ssize_t i = 0; i < array->length; ++i) - { + for (Py_ssize_t i = 0; i < array->length; ++i) { call_deallocator_maybe(array, i); array->items[i] = NULL; } @@ -147,8 +139,8 @@ pyawaitable_array_clear_items(pyawaitable_array *array) array->length = 0; } -void -pyawaitable_array_clear(pyawaitable_array *array) +_PyAwaitable_INTERNAL(void) +pyawaitable_array_clear(pyawaitable_array * array) { pyawaitable_array_ASSERT_VALID(array); pyawaitable_array_clear_items(array); diff --git a/src/_pyawaitable/awaitable.c b/src/_pyawaitable/awaitable.c index 6fea0e3..043c5df 100644 --- a/src/_pyawaitable/awaitable.c +++ b/src/_pyawaitable/awaitable.c @@ -6,11 +6,8 @@ #include #include #include - -PyDoc_STRVAR( - awaitable_doc, - "Awaitable transport utility for the C API." -); +#include +#include static void callback_dealloc(void *ptr) @@ -28,8 +25,7 @@ awaitable_new_func(PyTypeObject *tp, PyObject *args, PyObject *kwds) assert(tp->tp_alloc != NULL); PyObject *self = tp->tp_alloc(tp, 0); - if (self == NULL) - { + if (PyAwaitable_UNLIKELY(self == NULL)) { return NULL; } @@ -40,8 +36,7 @@ awaitable_new_func(PyTypeObject *tp, PyObject *args, PyObject *kwds) aw->aw_result = NULL; aw->aw_recently_cancelled = 0; - if (pyawaitable_array_init(&aw->aw_callbacks, callback_dealloc) < 0) - { + if (pyawaitable_array_init(&aw->aw_callbacks, callback_dealloc) < 0) { goto error; } @@ -50,18 +45,15 @@ awaitable_new_func(PyTypeObject *tp, PyObject *args, PyObject *kwds) &aw->aw_object_values, (pyawaitable_array_deallocator) Py_DecRef ) < 0 - ) - { + ) { goto error; } - if (pyawaitable_array_init(&aw->aw_arbitrary_values, NULL) < 0) - { + if (pyawaitable_array_init(&aw->aw_arbitrary_values, NULL) < 0) { goto error; } - if (pyawaitable_array_init(&aw->aw_integer_values, NULL) < 0) - { + if (pyawaitable_array_init(&aw->aw_integer_values, NULL) < 0) { goto error; } @@ -72,15 +64,14 @@ awaitable_new_func(PyTypeObject *tp, PyObject *args, PyObject *kwds) return NULL; } -PyObject * -awaitable_next(PyObject *self) +_PyAwaitable_INTERNAL(PyObject *) +awaitable_next(PyObject * self) { PyAwaitableObject *aw = (PyAwaitableObject *)self; - if (aw->aw_awaited) - { + if (aw->aw_done) { PyErr_SetString( PyExc_RuntimeError, - "pyawaitable: cannot reuse awaitable" + "PyAwaitable: Cannot reuse awaitable" ); return NULL; } @@ -107,16 +98,14 @@ awaitable_dealloc(PyObject *self) Py_XDECREF(aw->aw_gen); Py_XDECREF(aw->aw_result); - if (!aw->aw_done) - { + if (!aw->aw_awaited) { if ( PyErr_WarnEx( - PyExc_RuntimeWarning, - "pyawaitable object was never awaited", + PyExc_ResourceWarning, + "PyAwaitable object was never awaited", 1 ) < 0 - ) - { + ) { PyErr_WriteUnraisable(self); } } @@ -124,35 +113,61 @@ awaitable_dealloc(PyObject *self) Py_TYPE(self)->tp_free(self); } -void -pyawaitable_cancel_impl(PyObject *self) +_PyAwaitable_API(void) +PyAwaitable_Cancel(PyObject * self) { assert(self != NULL); PyAwaitableObject *aw = (PyAwaitableObject *) self; pyawaitable_array_clear_items(&aw->aw_callbacks); aw->aw_state = 0; - if (aw->aw_gen != NULL) - { + if (aw->aw_gen != NULL) { GenWrapperObject *gw = (GenWrapperObject *)aw->aw_gen; Py_CLEAR(gw->gw_current_await); } aw->aw_recently_cancelled = 1; + aw->aw_awaited = 1; } -int -pyawaitable_await_impl( - PyObject *self, - PyObject *coro, - awaitcallback cb, - awaitcallback_err err +_PyAwaitable_API(int) +PyAwaitable_AddAwait( + PyObject * self, + PyObject * coro, + PyAwaitable_Callback cb, + PyAwaitable_Error err ) { PyAwaitableObject *aw = (PyAwaitableObject *) self; + if (coro == NULL) { + PyErr_SetString( + PyExc_ValueError, + "PyAwaitable: NULL passed to PyAwaitable_AddAwait()! " + "Did you forget an error check?" + ); + return -1; + } + + if (coro == self) { + PyErr_Format( + PyExc_ValueError, + "PyAwaitable: Self (%R) was passed to PyAwaitable_AddAwait()! " + "This would result in a recursive nightmare.", + self + ); + return -1; + } + + if (!PyObject_HasAttrString(coro, "__await__")) { + PyErr_Format( + PyExc_TypeError, + "PyAwaitable: %R is not an awaitable object", + coro + ); + return -1; + } pyawaitable_callback *aw_c = PyMem_Malloc(sizeof(pyawaitable_callback)); - if (aw_c == NULL) - { + if (aw_c == NULL) { PyErr_NoMemory(); return -1; } @@ -162,8 +177,7 @@ pyawaitable_await_impl( aw_c->err_callback = err; aw_c->done = false; - if (pyawaitable_array_append(&aw->aw_callbacks, aw_c) < 0) - { + if (pyawaitable_array_append(&aw->aw_callbacks, aw_c) < 0) { PyMem_Free(aw_c); PyErr_NoMemory(); return -1; @@ -172,24 +186,22 @@ pyawaitable_await_impl( return 0; } -int -pyawaitable_defer_await_impl(PyObject *awaitable, defer_callback cb) +_PyAwaitable_API(int) +PyAwaitable_DeferAwait(PyObject * awaitable, PyAwaitable_Defer cb) { PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; pyawaitable_callback *aw_c = PyMem_Malloc(sizeof(pyawaitable_callback)); - if (aw_c == NULL) - { + if (aw_c == NULL) { PyErr_NoMemory(); return -1; } aw_c->coro = NULL; - aw_c->callback = (awaitcallback)cb; + aw_c->callback = (PyAwaitable_Callback)cb; aw_c->err_callback = NULL; aw_c->done = false; - if (pyawaitable_array_append(&aw->aw_callbacks, aw_c) < 0) - { + if (pyawaitable_array_append(&aw->aw_callbacks, aw_c) < 0) { PyMem_Free(aw_c); PyErr_NoMemory(); return -1; @@ -198,82 +210,34 @@ pyawaitable_defer_await_impl(PyObject *awaitable, defer_callback cb) return 0; } -int -pyawaitable_set_result_impl(PyObject *awaitable, PyObject *result) +_PyAwaitable_API(int) +PyAwaitable_SetResult(PyObject * awaitable, PyObject * result) { PyAwaitableObject *aw = (PyAwaitableObject *) awaitable; aw->aw_result = Py_NewRef(result); return 0; } -PyObject * -pyawaitable_new_impl(void) +_PyAwaitable_API(PyObject *) +PyAwaitable_New(void) { // XXX Use a freelist? - return awaitable_new_func(&_PyAwaitableType, NULL, NULL); -} - -int -pyawaitable_await_function_impl( - PyObject *awaitable, - PyObject *func, - const char *fmt, - awaitcallback cb, - awaitcallback_err err, - ... -) -{ - size_t len = strlen(fmt); - size_t size = len + 3; - char *tup_format = PyMem_Malloc(size); - if (!tup_format) - { - PyErr_NoMemory(); - return -1; - } - - tup_format[0] = '('; - for (size_t i = 0; i < len; ++i) - { - tup_format[i + 1] = fmt[i]; - } - - tup_format[size - 2] = ')'; - tup_format[size - 1] = '\0'; - - va_list vargs; - va_start(vargs, err); - PyObject *args = Py_VaBuildValue(tup_format, vargs); - va_end(vargs); - PyMem_Free(tup_format); - - if (!args) - return -1; - PyObject *coro = PyObject_Call(func, args, NULL); - Py_DECREF(args); - - if (!coro) - return -1; - - if (pyawaitable_await_impl(awaitable, coro, cb, err) < 0) - { - Py_DECREF(coro); - return -1; + PyTypeObject *type = PyAwaitable_GetType(); + if (PyAwaitable_UNLIKELY(type == NULL)) { + return NULL; } - - Py_DECREF(coro); - return 0; + PyObject *result = awaitable_new_func(type, NULL, NULL); + return result; } -PyTypeObject _PyAwaitableType = -{ +_PyAwaitable_INTERNAL_DATA_DEF(PyTypeObject) PyAwaitable_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "_PyAwaitableType", .tp_basicsize = sizeof(PyAwaitableObject), .tp_dealloc = awaitable_dealloc, .tp_as_async = &pyawaitable_async_methods, .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = awaitable_doc, + .tp_doc = PyDoc_STR("Awaitable transport utility for the C API."), .tp_iternext = awaitable_next, .tp_new = awaitable_new_func, .tp_methods = pyawaitable_methods diff --git a/src/_pyawaitable/backport.c b/src/_pyawaitable/backport.c deleted file mode 100644 index f3a1545..0000000 --- a/src/_pyawaitable/backport.c +++ /dev/null @@ -1,69 +0,0 @@ -#include -#include - -#ifdef PYAWAITABLE_NEEDS_VECTORCALL -PyObject * -_PyObject_VectorcallBackport( - PyObject *obj, - PyObject **args, - size_t nargsf, - PyObject *kwargs -) -{ - PyObject *tuple = PyTuple_New(nargsf); - if (!tuple) - return NULL; - for (size_t i = 0; i < nargsf; i++) - { - Py_INCREF(args[i]); - PyTuple_SET_ITEM(tuple, i, args[i]); - } - PyObject *o = PyObject_Call(obj, tuple, kwargs); - Py_DECREF(tuple); - return o; -} - -#endif - -#if PY_VERSION_HEX < 0x030c0000 -PyObject * -PyErr_GetRaisedException(void) -{ - PyObject *type, *val, *tb; - PyErr_Fetch(&type, &val, &tb); - PyErr_NormalizeException(&type, &val, &tb); - Py_XDECREF(type); - Py_XDECREF(tb); - // technically some entry in the traceback might be lost; ignore that - return val; -} - -void -PyErr_SetRaisedException(PyObject *err) -{ - // NOTE: We need to incref the type object here, even though - // this function steals a reference to err. - PyErr_Restore(Py_NewRef((PyObject *) Py_TYPE(err)), err, NULL); -} - -#endif - -#ifdef PYAWAITABLE_NEEDS_NEWREF -PyObject * -Py_NewRef_Backport(PyObject *o) -{ - Py_INCREF(o); - return o; -} - -#endif - -#ifdef PYAWAITABLE_NEEDS_XNEWREF -PyObject * -Py_XNewRef_Backport(PyObject *o) -{ - Py_XINCREF(o); - return o; -} - -#endif diff --git a/src/_pyawaitable/coro.c b/src/_pyawaitable/coro.c index e569b1b..b56672d 100644 --- a/src/_pyawaitable/coro.c +++ b/src/_pyawaitable/coro.c @@ -1,18 +1,25 @@ #include -#include #include -#include +#include #include +#include +#include static PyObject * awaitable_send_with_arg(PyObject *self, PyObject *value) { PyAwaitableObject *aw = (PyAwaitableObject *) self; - if (aw->aw_gen == NULL) - { + if (aw->aw_gen == NULL) { PyObject *gen = awaitable_next(self); - if (gen == NULL) - { + if (PyAwaitable_UNLIKELY(gen == NULL)) { + return NULL; + } + + if (PyAwaitable_UNLIKELY(value != Py_None)) { + PyErr_SetString( + PyExc_RuntimeError, + "can't send non-None value to a just-started awaitable" + ); return NULL; } @@ -20,26 +27,19 @@ awaitable_send_with_arg(PyObject *self, PyObject *value) Py_RETURN_NONE; } - return genwrapper_next(aw->aw_gen); + return _PyAwaitableGenWrapper_Next(aw->aw_gen); } static PyObject * -awaitable_send(PyObject *self, PyObject *args) +awaitable_send(PyObject *self, PyObject *value) { - PyObject *value; - - if (!PyArg_ParseTuple(args, "O", &value)) - { - return NULL; - } - return awaitable_send_with_arg(self, value); } static PyObject * awaitable_close(PyObject *self, PyObject *args) { - pyawaitable_cancel_impl(self); + PyAwaitable_Cancel(self); PyAwaitableObject *aw = (PyAwaitableObject *) self; aw->aw_done = true; Py_RETURN_NONE; @@ -52,31 +52,26 @@ awaitable_throw(PyObject *self, PyObject *args) PyObject *value = NULL; PyObject *traceback = NULL; - if (!PyArg_ParseTuple(args, "O|OO", &type, &value, &traceback)) - { + if (!PyArg_ParseTuple(args, "O|OO", &type, &value, &traceback)) { return NULL; } - if (PyType_Check(type)) - { + if (PyType_Check(type)) { PyObject *err = PyObject_CallOneArg(type, value); - if (err == NULL) - { + if (PyAwaitable_UNLIKELY(err == NULL)) { return NULL; } - if (traceback) - { - if (PyException_SetTraceback(err, traceback) < 0) - { + if (traceback != NULL) { + if (PyException_SetTraceback(err, traceback) < 0) { Py_DECREF(err); return NULL; } } PyErr_Restore(err, NULL, NULL); - } else - { + } + else { PyErr_Restore( Py_NewRef(type), Py_XNewRef(value), @@ -85,22 +80,22 @@ awaitable_throw(PyObject *self, PyObject *args) } PyAwaitableObject *aw = (PyAwaitableObject *)self; - if ((aw->aw_gen != NULL) && (aw->aw_state != 0)) - { + if ((aw->aw_gen != NULL) && (aw->aw_state != 0)) { GenWrapperObject *gw = (GenWrapperObject *)aw->aw_gen; pyawaitable_callback *cb = pyawaitable_array_GET_ITEM(&aw->aw_callbacks, aw->aw_state - 1); - if (cb == NULL) - { + if (cb == NULL) { return NULL; } - if (genwrapper_fire_err_callback(self, cb->err_callback) < 0) - { + if (_PyAwaitableGenWrapper_FireErrCallback( + self, + cb->err_callback + ) < 0) { return NULL; } - } else - { + } + else { return NULL; } @@ -113,16 +108,13 @@ static PySendResult awaitable_am_send(PyObject *self, PyObject *arg, PyObject **presult) { PyObject *send_res = awaitable_send_with_arg(self, arg); - if (send_res == NULL) - { - if (PyErr_ExceptionMatches(PyExc_StopIteration)) - { + if (send_res == NULL) { + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { PyObject *occurred = PyErr_GetRaisedException(); PyObject *item = PyObject_GetAttrString(occurred, "value"); Py_DECREF(occurred); - if (item == NULL) - { + if (PyAwaitable_UNLIKELY(item == NULL)) { return PYGEN_ERROR; } @@ -139,16 +131,14 @@ awaitable_am_send(PyObject *self, PyObject *arg, PyObject **presult) #endif -PyMethodDef pyawaitable_methods[] = -{ - {"send", awaitable_send, METH_VARARGS, NULL}, - {"close", awaitable_close, METH_VARARGS, NULL}, +_PyAwaitable_INTERNAL_DATA_DEF(PyMethodDef) pyawaitable_methods[] = { + {"send", awaitable_send, METH_O, NULL}, + {"close", awaitable_close, METH_NOARGS, NULL}, {"throw", awaitable_throw, METH_VARARGS, NULL}, {NULL, NULL, 0, NULL} }; -PyAsyncMethods pyawaitable_async_methods = -{ +_PyAwaitable_INTERNAL_DATA_DEF(PyAsyncMethods) pyawaitable_async_methods = { #if PY_MINOR_VERSION > 9 .am_await = awaitable_next, .am_send = awaitable_am_send diff --git a/src/_pyawaitable/genwrapper.c b/src/_pyawaitable/genwrapper.c index 9bf0614..3fe13c3 100644 --- a/src/_pyawaitable/genwrapper.c +++ b/src/_pyawaitable/genwrapper.c @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #define DONE(cb) \ do { cb->done = true; \ @@ -12,10 +14,39 @@ aw->aw_done = true; \ Py_CLEAR(g->gw_aw); \ } while (0) -#define DONE_IF_OK(cb) \ - if (cb != NULL) { \ - DONE(cb); \ +#define DONE_IF_OK(cb) \ + if (PyAwaitable_LIKELY(cb != NULL)) { \ + DONE(cb); \ } +#define DONE_IF_OK_AND_CHECK(cb) \ + if (PyAwaitable_UNLIKELY(aw->aw_recently_cancelled)) { \ + cb = NULL; \ + } \ + else { \ + DONE(cb); \ + } +/* If we recently cancelled, then cb is no longer valid */ +#define CLEAR_CALLBACK_IF_CANCELLED() \ + if (PyAwaitable_UNLIKELY(aw->aw_recently_cancelled)) { \ + cb = NULL; \ + } \ + +#define FIRE_ERROR_CALLBACK_AND_NEXT() \ + if ( \ + _PyAwaitableGenWrapper_FireErrCallback( \ + (PyObject *) aw, \ + cb->err_callback \ + ) < 0 \ + ) { \ + DONE_IF_OK_AND_CHECK(cb); \ + AW_DONE(); \ + return NULL; \ + } \ + DONE_IF_OK_AND_CHECK(cb); \ + return _PyAwaitableGenWrapper_Next(self); +#define RETURN_ADVANCE_GENERATOR() \ + DONE_IF_OK(cb); \ + PyAwaitable_MUSTTAIL return _PyAwaitableGenWrapper_Next(self); static PyObject * gen_new(PyTypeObject *tp, PyObject *args, PyObject *kwds) @@ -24,8 +55,7 @@ gen_new(PyTypeObject *tp, PyObject *args, PyObject *kwds) assert(tp->tp_alloc != NULL); PyObject *self = tp->tp_alloc(tp, 0); - if (self == NULL) - { + if (PyAwaitable_UNLIKELY(self == NULL)) { return NULL; } @@ -62,32 +92,36 @@ gen_dealloc(PyObject *self) Py_TYPE(self)->tp_free(self); } -PyObject * -genwrapper_new(PyAwaitableObject *aw) +_PyAwaitable_INTERNAL(PyObject *) +genwrapper_new(PyAwaitableObject * aw) { assert(aw != NULL); + PyTypeObject *type = _PyAwaitable_GetGenWrapperType(); + if (PyAwaitable_UNLIKELY(type == NULL)) { + return NULL; + } GenWrapperObject *g = (GenWrapperObject *) gen_new( - &_PyAwaitableGenWrapperType, + type, NULL, NULL ); - if (!g) + if (PyAwaitable_UNLIKELY(g == NULL)) { return NULL; + } g->gw_aw = (PyAwaitableObject *) Py_NewRef((PyObject *) aw); return (PyObject *) g; } -int -genwrapper_fire_err_callback( - PyObject *self, - awaitcallback_err err_callback +_PyAwaitable_INTERNAL(int) +_PyAwaitableGenWrapper_FireErrCallback( + PyObject * self, + PyAwaitable_Error err_callback ) { assert(PyErr_Occurred() != NULL); - if (err_callback == NULL) - { + if (err_callback == NULL) { return -1; } @@ -97,15 +131,13 @@ genwrapper_fire_err_callback( int e_res = err_callback(self, err); Py_DECREF(self); - if (e_res < 0) - { + if (e_res < 0) { // If the res is -1, the error is restored. // Otherwise, it is not. - if (e_res == -1) - { + if (e_res == -1) { PyErr_SetRaisedException(err); - } else - { + } + else { Py_DECREF(err); } return -1; @@ -124,31 +156,95 @@ genwrapper_advance(GenWrapperObject *gw) ); } -PyObject * -genwrapper_next(PyObject *self) +static PyObject * +get_generator_return_value(void) +{ + PyObject *value; + if (PyErr_Occurred()) { + value = PyErr_GetRaisedException(); + assert(value != NULL); + assert(PyObject_IsInstance(value, PyExc_StopIteration)); + PyObject *tmp = PyObject_GetAttrString(value, "value"); + if (PyAwaitable_UNLIKELY(tmp == NULL)) { + Py_DECREF(value); + return NULL; + } + Py_DECREF(value); + value = tmp; + } + else { + value = Py_NewRef(Py_None); + } + + return value; +} + +static int +maybe_set_result(PyAwaitableObject *aw) +{ + if (pyawaitable_array_LENGTH(&aw->aw_callbacks) == aw->aw_state) { + PyErr_SetObject( + PyExc_StopIteration, + aw->aw_result ? aw->aw_result : Py_None + ); + return 1; + } + + return 0; +} + +static inline PyAwaitable_COLD PyObject * +bad_callback(void) +{ + PyErr_SetString( + PyExc_SystemError, + "PyAwaitable: User callback returned -1 without exception set" + ); + return NULL; +} + +static inline PyObject * +get_awaitable_iterator(PyObject *op) +{ + if ( + PyAwaitable_UNLIKELY( + Py_TYPE(op)->tp_as_async == NULL || + Py_TYPE(op)->tp_as_async->am_await == NULL + ) + ) { + // Fall back to the dunder + PyObject *__await__ = PyObject_GetAttrString(op, "__await__"); + if (__await__ == NULL) { + return NULL; + } + + PyObject *res = PyObject_CallNoArgs(__await__); + Py_DECREF(__await__); + return res; + } + + return Py_TYPE(op)->tp_as_async->am_await(op); +} + +_PyAwaitable_INTERNAL(PyObject *) PyAwaitable_HOT +_PyAwaitableGenWrapper_Next(PyObject *self) { GenWrapperObject *g = (GenWrapperObject *)self; PyAwaitableObject *aw = g->gw_aw; - if (!aw) - { + if (PyAwaitable_UNLIKELY(aw == NULL)) { PyErr_SetString( PyExc_RuntimeError, - "pyawaitable: genwrapper used after return" + "PyAwaitable: Generator cannot be awaited after returning" ); return NULL; } pyawaitable_callback *cb; - if (g->gw_current_await == NULL) - { - if (pyawaitable_array_LENGTH(&aw->aw_callbacks) == aw->aw_state) - { - PyErr_SetObject( - PyExc_StopIteration, - aw->aw_result ? aw->aw_result : Py_None - ); + if (g->gw_current_await == NULL) { + if (maybe_set_result(aw)) { + // Coroutine is done, woohoo! AW_DONE(); return NULL; } @@ -156,79 +252,27 @@ genwrapper_next(PyObject *self) cb = genwrapper_advance(g); assert(cb != NULL); assert(cb->done == false); - assert(cb->coro != NULL); - - if (cb->coro == NULL) - { - printf( - "len: %ld, state: %ld\n", - pyawaitable_array_LENGTH(&aw->aw_callbacks), - aw->aw_state - ); - } - - if (cb->callback != NULL && cb->coro == NULL) - { - int def_res = ((defer_callback)cb->callback)((PyObject*)aw); - // If we recently cancelled, then cb is no longer valid - if (aw->aw_recently_cancelled) - { - cb = NULL; - } - - if (def_res < 0 && !PyErr_Occurred()) - { - PyErr_SetString( - PyExc_SystemError, - "pyawaitable: callback returned -1 without exception set" - ); + if (cb->callback != NULL && cb->coro == NULL) { + int def_res = ((PyAwaitable_Defer)cb->callback)((PyObject *)aw); + CLEAR_CALLBACK_IF_CANCELLED(); + if (def_res < 0) { DONE_IF_OK(cb); + AW_DONE(); return NULL; } // Callback is done. - DONE_IF_OK(cb); - return genwrapper_next(self); + RETURN_ADVANCE_GENERATOR(); } - if ( - Py_TYPE(cb->coro)->tp_as_async == NULL || - Py_TYPE(cb->coro)->tp_as_async->am_await == NULL - ) - { - PyErr_Format( - PyExc_TypeError, - "pyawaitable: %R is not awaitable", - cb->coro - ); - DONE(cb); - AW_DONE(); - return NULL; - } - - g->gw_current_await = Py_TYPE(cb->coro)->tp_as_async->am_await( - cb->coro - ); - if (g->gw_current_await == NULL) - { - if ( - genwrapper_fire_err_callback( - (PyObject *)aw, - cb->err_callback - ) < 0 - ) - { - DONE_IF_OK(cb); - AW_DONE(); - return NULL; - } - - DONE_IF_OK(cb); - return genwrapper_next(self); + assert(cb->coro != NULL); + g->gw_current_await = get_awaitable_iterator(cb->coro); + if (g->gw_current_await == NULL) { + FIRE_ERROR_CALLBACK_AND_NEXT(); } - } else - { + } + else { cb = pyawaitable_array_GET_ITEM(&aw->aw_callbacks, aw->aw_state - 1); } @@ -236,91 +280,54 @@ genwrapper_next(PyObject *self) g->gw_current_await )->tp_iternext(g->gw_current_await); - if (result != NULL) - { + if (result != NULL) { // Yield! return result; } + // Rare, but it's possible that the generator cancelled us + CLEAR_CALLBACK_IF_CANCELLED(); + PyObject *occurred = PyErr_Occurred(); - if (!occurred) - { + if (!occurred) { // Coro is done, no result. - if (!cb->callback) - { - // No callback, skip that step. - DONE(cb); - return genwrapper_next(self); + if (cb == NULL || !cb->callback) { + // No callback, skip trying to handle anything + RETURN_ADVANCE_GENERATOR(); } } - // TODO: I wonder if the occurred check is needed here. - if ( - occurred && !PyErr_ExceptionMatches(PyExc_StopIteration) - ) - { - if ( - genwrapper_fire_err_callback( - (PyObject *) aw, - cb->err_callback - ) < 0 - ) - { - DONE(cb); - AW_DONE(); - return NULL; - } - - DONE(cb); - return genwrapper_next(self); + if (occurred && !PyErr_ExceptionMatches(PyExc_StopIteration)) { + // An error occurred! + FIRE_ERROR_CALLBACK_AND_NEXT(); } - if (cb->callback == NULL) - { - // Coroutine is done, but with a result. - // We can disregard the result if theres no callback. - DONE(cb); + /* Coroutine is done, but with a result. */ + if (cb == NULL || cb->callback == NULL) { + // We can disregard the result if there's no callback. PyErr_Clear(); - return genwrapper_next(self); + RETURN_ADVANCE_GENERATOR(); } + assert(cb != NULL); // Deduce the return value of the coroutine - PyObject *value; - if (occurred) - { - value = PyErr_GetRaisedException(); - assert(value != NULL); - assert(PyObject_IsInstance(value, PyExc_StopIteration)); - PyObject *tmp = PyObject_GetAttrString(value, "value"); - if (tmp == NULL) - { - Py_DECREF(value); - DONE(cb); - AW_DONE(); - return NULL; - } - Py_DECREF(value); - value = tmp; - } else - { - value = Py_NewRef(Py_None); + PyObject *value = get_generator_return_value(); + if (value == NULL) { + DONE(cb); + AW_DONE(); + return NULL; } // Preserve the error callback in case we get cancelled - awaitcallback_err err_callback = cb->err_callback; + PyAwaitable_Error err_callback = cb->err_callback; Py_INCREF(aw); int res = cb->callback((PyObject *) aw, value); Py_DECREF(aw); Py_DECREF(value); - // If we recently cancelled, then cb is no longer valid - if (aw->aw_recently_cancelled) - { - cb = NULL; - } + CLEAR_CALLBACK_IF_CANCELLED(); - if (res < -1) - { + if (res < -1) { // -2 or lower denotes that the error should be deferred, // regardless of whether a handler is present. DONE_IF_OK(cb); @@ -328,44 +335,21 @@ genwrapper_next(PyObject *self) return NULL; } - if (res < 0) - { - if (!PyErr_Occurred()) - { - PyErr_SetString( - PyExc_RuntimeError, - "pyawaitable: user callback returned -1 without exception set" - ); - DONE_IF_OK(cb); - AW_DONE(); - return NULL; - } - if ( - genwrapper_fire_err_callback( - (PyObject *) aw, - err_callback - ) < 0 - ) - { - DONE_IF_OK(cb); - AW_DONE(); - return NULL; - } + if (res < 0) { + FIRE_ERROR_CALLBACK_AND_NEXT(); } - DONE_IF_OK(cb); - return genwrapper_next(self); + RETURN_ADVANCE_GENERATOR(); } -PyTypeObject _PyAwaitableGenWrapperType = -{ +_PyAwaitable_INTERNAL_DATA_DEF(PyTypeObject) _PyAwaitableGenWrapperType = { PyVarObject_HEAD_INIT(NULL, 0) - .tp_name = "_genwrapper", + .tp_name = "_PyAwaitableGenWrapperType", .tp_basicsize = sizeof(GenWrapperObject), .tp_dealloc = gen_dealloc, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, .tp_iter = PyObject_SelfIter, - .tp_iternext = genwrapper_next, + .tp_iternext = _PyAwaitableGenWrapper_Next, .tp_clear = genwrapper_clear, .tp_traverse = genwrapper_traverse, .tp_new = gen_new, diff --git a/src/_pyawaitable/init.c b/src/_pyawaitable/init.c new file mode 100644 index 0000000..8bebadc --- /dev/null +++ b/src/_pyawaitable/init.c @@ -0,0 +1,378 @@ +#include +#include +#include +#include + +static int +dict_add_type(PyObject *state, PyTypeObject *obj) +{ + assert(obj != NULL); + assert(state != NULL); + assert(PyDict_Check(state)); + assert(obj->tp_name != NULL); + + Py_INCREF(obj); + if (PyType_Ready(obj) < 0) { + Py_DECREF(obj); + return -1; + } + + if (PyDict_SetItemString(state, obj->tp_name, (PyObject *)obj) < 0) { + Py_DECREF(obj); + return -1; + } + Py_DECREF(obj); + return 0; +} + +static int +init_state(PyObject *state) +{ + assert(state != NULL); + assert(PyDict_Check(state)); + if (dict_add_type(state, &PyAwaitable_Type) < 0) { + return -1; + } + + if (dict_add_type(state, &_PyAwaitableGenWrapperType) < 0) { + return -1; + } + + PyObject *version = PyLong_FromLong(PyAwaitable_MAGIC_NUMBER); + if (version == NULL) { + return -1; + } + + if (PyDict_SetItemString(state, "magic_version", version) < 0) { + Py_DECREF(version); + return -1; + } + + Py_DECREF(version); + return 0; +} + +static PyObject * +create_state(void) +{ + PyObject *state = PyDict_New(); + if (state == NULL) { + return NULL; + } + + if (init_state(state) < 0) { + Py_DECREF(state); + return NULL; + } + + return state; +} + +static PyObject * +interp_get_dict(void) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + assert(interp != NULL); + PyObject *interp_state = PyInterpreterState_GetDict(interp); + if (interp_state == NULL) { + // Would be a memory error or something more exotic, but + // there's nothing we can do. + PyErr_SetString( + PyExc_RuntimeError, + "PyAwaitable: Interpreter failed to provide a state dictionary" + ); + return NULL; + } + + return interp_state; +} + +static inline PyObject * +not_initialized(void) +{ + PyErr_SetString( + PyExc_RuntimeError, + "PyAwaitable hasn't been initialized yet! " + "Did you forget to call PyAwaitable_Init()?" + ); + return NULL; +} + +static inline int +state_corrupted(const char *err, PyObject *found) +{ + assert(err != NULL); + assert(found != NULL); + PyErr_Format( + PyExc_SystemError, + "PyAwaitable corruption! %s: %R", + err, + found + ); + Py_DECREF(found); + return -1; +} + +static PyObject * +get_state_value(PyObject *state, const char *name) +{ + assert(name != NULL); + PyObject *str = PyUnicode_FromString(name); + if (str == NULL) { + return NULL; + } + + PyObject *version = PyDict_GetItemWithError(state, str); + Py_DECREF(str); + return version; +} + +static long +get_state_version(PyObject *state) +{ + assert(state != NULL); + assert(PyDict_Check(state)); + + PyObject *version = get_state_value(state, "magic_version"); + if (version == NULL) { + return -1; + } + + if (!PyLong_CheckExact(version)) { + return state_corrupted("Non-int version number", version); + } + + long version_num = PyLong_AsLong(version); + if (version_num == -1 && PyErr_Occurred()) { + Py_DECREF(version); + return -1; + } + + if (version_num < 0) { + return state_corrupted("Found <0 version somehow", version); + } + + return version_num; +} + +static PyObject * +find_module_for_version(PyObject *interp_dict, long version) +{ + PyObject *list = PyDict_GetItemString(interp_dict, "_pyawaitable_states"); + if (list == NULL) { + return not_initialized(); + } + + if (!PyList_CheckExact(list)) { + state_corrupted("_pyawaitable_states is not a list", list); + return NULL; + } + + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(list); ++i) { + PyObject *mod = PyList_GET_ITEM(list, i); + long got_version = get_state_version(mod); + + if (got_version == -1) { + return NULL; + } + + if (got_version == version) { + return mod; + } + } + + return not_initialized(); +} + +static PyObject * +find_top_level_state(PyObject **interp_dict) +{ + PyObject *dict = interp_get_dict(); + if (dict == NULL) { + return NULL; + } + + PyObject *mod = PyDict_GetItemString(dict, "_pyawaitable_state"); + if (mod == NULL) { + return not_initialized(); + } + + if (interp_dict != NULL) { + *interp_dict = dict; + } + return mod; +} + +static PyAwaitable_thread_local PyObject *pyawaitable_fast_state = NULL; + +_PyAwaitable_INTERNAL(PyObject *) +_PyAwaitable_GetState(void) +{ + if (pyawaitable_fast_state != NULL) { + return pyawaitable_fast_state; + } + + PyObject *interp_dict; + PyObject *state = find_top_level_state(&interp_dict); // Borrowed reference + if (state == NULL) { + return NULL; + } + + long version = get_state_version(state); + if (version == -1) { + return NULL; + } + + if (version != PyAwaitable_MAGIC_NUMBER) { + // Not our module! + state = find_module_for_version(interp_dict, PyAwaitable_MAGIC_NUMBER); + if (state == NULL) { + return NULL; + } + } + + // We want this to be a borrowed reference + pyawaitable_fast_state = state; + return state; +} + +static PyAwaitable_thread_local PyTypeObject *pyawaitable_fast_aw = NULL; +static PyAwaitable_thread_local PyTypeObject *pyawaitable_fast_gw = NULL; + +_PyAwaitable_API(PyTypeObject *) +PyAwaitable_GetType(void) +{ + if (pyawaitable_fast_aw != NULL) { + return pyawaitable_fast_aw; + } + PyObject *state = _PyAwaitable_GetState(); + if (state == NULL) { + return NULL; + } + + PyTypeObject *pyawaitable_type = (PyTypeObject *)get_state_value( + state, + "_PyAwaitableType" + ); + if (pyawaitable_type == NULL) { + return NULL; + } + + // Should be an immortal reference + pyawaitable_fast_aw = pyawaitable_type; + return pyawaitable_type; +} + + +_PyAwaitable_INTERNAL(PyTypeObject *) +_PyAwaitable_GetGenWrapperType(void) +{ + if (pyawaitable_fast_gw != NULL) { + return pyawaitable_fast_gw; + } + PyObject *state = _PyAwaitable_GetState(); + if (state == NULL) { + return NULL; + } + + PyTypeObject *gw_type = (PyTypeObject *)get_state_value( + state, + "_PyAwaitableGenWrapperType" + ); + if (gw_type == NULL) { + return NULL; + } + + pyawaitable_fast_gw = gw_type; + return (PyTypeObject *)gw_type; +} + +static int +add_state_to_list(PyObject *interp_dict, PyObject *state) +{ + assert(interp_dict != NULL); + assert(state != NULL); + assert(PyDict_Check(interp_dict)); + assert(PyDict_Check(state)); + + PyObject *pyawaitable_list = Py_XNewRef( + PyDict_GetItemString( + interp_dict, + "_pyawaitable_states" + ) + ); + if (pyawaitable_list == NULL) { + // No list has been set + pyawaitable_list = PyList_New(1); + if (pyawaitable_list == NULL) { + // Memory error + return -1; + } + + if ( + PyDict_SetItemString( + interp_dict, + "_pyawaitable_states", + pyawaitable_list + ) < 0 + ) { + Py_DECREF(pyawaitable_list); + return -1; + } + } + + if (PyList_Append(pyawaitable_list, state) < 0) { + Py_DECREF(pyawaitable_list); + return -1; + } + + Py_DECREF(pyawaitable_list); + return 0; +} + +_PyAwaitable_API(int) +PyAwaitable_Init(void) +{ + PyObject *dict = interp_get_dict(); + if (dict == NULL) { + return -1; + } + + PyObject *state = create_state(); + if (state == NULL) { + return -1; + } + + PyObject *existing = PyDict_GetItemString(dict, "_pyawaitable_state"); + if (existing != NULL) { + /* Oh no, PyAwaitable has been used twice! */ + long version = get_state_version(existing); + if (version == -1) { + Py_DECREF(state); + return -1; + } + + if (version == PyAwaitable_MAGIC_NUMBER) { + // Yay! It just happens that it's the same version as us. + // Let's just reuse it. + Py_DECREF(state); + return 0; + } + + if (add_state_to_list(dict, state) < 0) { + Py_DECREF(state); + return -1; + } + + Py_DECREF(state); + return 0; + } + + if (PyDict_SetItemString(dict, "_pyawaitable_state", state) < 0) { + Py_DECREF(state); + return -1; + } + + Py_DECREF(state); + return 0; +} diff --git a/src/_pyawaitable/mod.c b/src/_pyawaitable/mod.c deleted file mode 100644 index f5a1c45..0000000 --- a/src/_pyawaitable/mod.c +++ /dev/null @@ -1,88 +0,0 @@ -#include -#include -#include -#include -#include -#include -#define ADD_TYPE(tp) \ - do \ - { \ - Py_INCREF(&tp); \ - if (PyType_Ready(&tp) < 0) { \ - Py_DECREF(&tp); \ - Py_DECREF(m); \ - return NULL; \ - } \ - if (PyModule_AddObject(m, #tp, (PyObject *)&tp) < 0) { \ - Py_DECREF(&tp); \ - Py_DECREF(m); \ - return NULL; \ - } \ - } while (0) - -static PyModuleDef awaitable_module = -{ - PyModuleDef_HEAD_INIT, - "_pyawaitable", - NULL, - -1 -}; - -/* - * This is the ABI definition. - * - * You can only append to this structure. - * Never ever remove, move, or change the size of an existing field. - */ -static PyAwaitableABI _abi_interface = -{ - sizeof(PyAwaitableABI), - pyawaitable_new_impl, - pyawaitable_await_impl, - pyawaitable_cancel_impl, - pyawaitable_set_result_impl, - pyawaitable_save_impl, - pyawaitable_save_arb_impl, - pyawaitable_unpack_impl, - pyawaitable_unpack_arb_impl, - &_PyAwaitableType, - pyawaitable_await_function_impl, - pyawaitable_save_int_impl, - pyawaitable_unpack_int_impl, - pyawaitable_set_impl, - pyawaitable_set_arb_impl, - pyawaitable_set_int_impl, - pyawaitable_get_impl, - pyawaitable_get_arb_impl, - pyawaitable_get_int_impl, - pyawaitable_async_with_impl, - pyawaitable_defer_await_impl -}; - -PyMODINIT_FUNC -PyInit__pyawaitable(void) -{ - PyObject *m = PyModule_Create(&awaitable_module); - ADD_TYPE(_PyAwaitableType); - ADD_TYPE(_PyAwaitableGenWrapperType); - PyObject *capsule = PyCapsule_New( - &_abi_interface, - "_pyawaitable.abi_v1", - NULL - ); - - if (!capsule) - { - Py_DECREF(m); - return NULL; - } - - if (PyModule_AddObject(m, "abi_v1", capsule) < 0) - { - Py_DECREF(m); - Py_DECREF(capsule); - return NULL; - } - - return m; -} diff --git a/src/_pyawaitable/values.c b/src/_pyawaitable/values.c index df95373..50b4f4b 100644 --- a/src/_pyawaitable/values.c +++ b/src/_pyawaitable/values.c @@ -5,6 +5,7 @@ #include #include #include +#include #define NOTHING @@ -31,7 +32,7 @@ if (pyawaitable_array_LENGTH(array) == 0) { \ PyErr_SetString( \ PyExc_RuntimeError, \ - "pyawaitable: object has no stored values" \ + "PyAwaitable: Object has no stored values" \ ); \ return -1; \ } \ @@ -70,20 +71,18 @@ static int check_index(Py_ssize_t index, pyawaitable_array *array) { assert(array != NULL); - if (index < 0) - { + if (PyAwaitable_UNLIKELY(index < 0)) { PyErr_SetString( PyExc_IndexError, - "pyawaitable: cannot set negative index" + "PyAwaitable: Cannot set negative index" ); return -1; } - if (index >= pyawaitable_array_LENGTH(array)) - { + if (PyAwaitable_UNLIKELY(index >= pyawaitable_array_LENGTH(array))) { PyErr_SetString( PyExc_IndexError, - "pyawaitable: cannot set index that is out of bounds" + "PyAwaitable: Cannot set index that is out of bounds" ); return -1; } @@ -91,31 +90,31 @@ check_index(Py_ssize_t index, pyawaitable_array *array) return 0; } -int -pyawaitable_unpack_impl(PyObject *awaitable, ...) +_PyAwaitable_API(int) +PyAwaitable_UnpackValues(PyObject * awaitable, ...) { UNPACK(aw_object_values, PyObject *); } -int -pyawaitable_save_impl(PyObject *awaitable, Py_ssize_t nargs, ...) +_PyAwaitable_API(int) +PyAwaitable_SaveValues(PyObject * awaitable, Py_ssize_t nargs, ...) { SAVE(aw_object_values, PyObject *, Py_INCREF(ptr)); } -int -pyawaitable_set_impl( - PyObject *awaitable, +_PyAwaitable_API(int) +PyAwaitable_SetValue( + PyObject * awaitable, Py_ssize_t index, - PyObject *new_value + PyObject * new_value ) { SET(aw_object_values, Py_NewRef); } -PyObject * -pyawaitable_get_impl( - PyObject *awaitable, +_PyAwaitable_API(PyObject *) +PyAwaitable_GetValue( + PyObject * awaitable, Py_ssize_t index ) { @@ -124,21 +123,21 @@ pyawaitable_get_impl( /* Arbitrary Values */ -int -pyawaitable_unpack_arb_impl(PyObject *awaitable, ...) +_PyAwaitable_API(int) +PyAwaitable_UnpackArbValues(PyObject * awaitable, ...) { UNPACK(aw_arbitrary_values, void *); } -int -pyawaitable_save_arb_impl(PyObject *awaitable, Py_ssize_t nargs, ...) +_PyAwaitable_API(int) +PyAwaitable_SaveArbValues(PyObject * awaitable, Py_ssize_t nargs, ...) { SAVE(aw_arbitrary_values, void *, NOTHING); } -int -pyawaitable_set_arb_impl( - PyObject *awaitable, +_PyAwaitable_API(int) +PyAwaitable_SetArbValue( + PyObject * awaitable, Py_ssize_t index, void *new_value ) @@ -146,9 +145,9 @@ pyawaitable_set_arb_impl( SET(aw_arbitrary_values, void *); } -void * -pyawaitable_get_arb_impl( - PyObject *awaitable, +_PyAwaitable_API(void *) +PyAwaitable_GetArbValue( + PyObject * awaitable, Py_ssize_t index ) { @@ -157,21 +156,21 @@ pyawaitable_get_arb_impl( /* Integer Values */ -int -pyawaitable_unpack_int_impl(PyObject *awaitable, ...) +_PyAwaitable_API(int) +PyAwaitable_UnpackIntValues(PyObject * awaitable, ...) { UNPACK(aw_integer_values, long); } -int -pyawaitable_save_int_impl(PyObject *awaitable, Py_ssize_t nargs, ...) +_PyAwaitable_API(int) +PyAwaitable_SaveIntValues(PyObject * awaitable, Py_ssize_t nargs, ...) { SAVE(aw_integer_values, long, NOTHING); } -int -pyawaitable_set_int_impl( - PyObject *awaitable, +_PyAwaitable_API(int) +PyAwaitable_SetIntValue( + PyObject * awaitable, Py_ssize_t index, long new_value ) @@ -179,9 +178,9 @@ pyawaitable_set_int_impl( SET(aw_integer_values, long); } -long -pyawaitable_get_int_impl( - PyObject *awaitable, +_PyAwaitable_API(long) +PyAwaitable_GetIntValue( + PyObject * awaitable, Py_ssize_t index ) { diff --git a/src/_pyawaitable/with.c b/src/_pyawaitable/with.c index d01c7d6..651d363 100644 --- a/src/_pyawaitable/with.c +++ b/src/_pyawaitable/with.c @@ -6,14 +6,16 @@ static int async_with_inner(PyObject *aw, PyObject *res) { - awaitcallback cb; - awaitcallback_err err; + PyAwaitable_Callback cb; + PyAwaitable_Error err; PyObject *exit; - if (pyawaitable_unpack_arb_impl(aw, &cb, &err) < 0) + if (PyAwaitable_UnpackArbValues(aw, &cb, &err) < 0) { return -1; + } - if (pyawaitable_unpack_impl(aw, &exit) < 0) + if (PyAwaitable_UnpackValues(aw, &exit) < 0) { return -1; + } Py_INCREF(aw); Py_INCREF(res); @@ -21,14 +23,12 @@ async_with_inner(PyObject *aw, PyObject *res) Py_DECREF(res); Py_DECREF(aw); - if (callback_result < 0) - { + if (callback_result < 0) { PyObject *tp, *val, *tb; PyErr_Fetch(&tp, &val, &tb); PyErr_NormalizeException(&tp, &val, &tb); - if (tp == NULL) - { + if (tp == NULL) { PyErr_SetString( PyExc_SystemError, "pyawaitable: async with callback returned -1 without exception set" @@ -49,21 +49,19 @@ async_with_inner(PyObject *aw, PyObject *res) Py_DECREF(tp); Py_DECREF(val); Py_DECREF(tb); - if (coro == NULL) - { + if (coro == NULL) { return -1; } - if (pyawaitable_await_impl(aw, coro, NULL, NULL) < 0) - { + if (PyAwaitable_AddAwait(aw, coro, NULL, NULL) < 0) { Py_DECREF(coro); return -1; } Py_DECREF(coro); return 0; - } else - { + } + else { // OK PyObject *coro = PyObject_Vectorcall( exit, @@ -71,13 +69,11 @@ async_with_inner(PyObject *aw, PyObject *res) 3, NULL ); - if (coro == NULL) - { + if (coro == NULL) { return -1; } - if (pyawaitable_await_impl(aw, coro, NULL, NULL) < 0) - { + if (PyAwaitable_AddAwait(aw, coro, NULL, NULL) < 0) { Py_DECREF(coro); return -1; } @@ -86,17 +82,16 @@ async_with_inner(PyObject *aw, PyObject *res) } } -int -pyawaitable_async_with_impl( - PyObject *aw, - PyObject *ctx, - awaitcallback cb, - awaitcallback_err err +_PyAwaitable_API(int) +PyAwaitable_AsyncWith( + PyObject * aw, + PyObject * ctx, + PyAwaitable_Callback cb, + PyAwaitable_Error err ) { PyObject *with = PyObject_GetAttrString(ctx, "__aenter__"); - if (with == NULL) - { + if (with == NULL) { PyErr_Format( PyExc_TypeError, "pyawaitable: %R is not an async context manager (missing __aenter__)", @@ -105,8 +100,7 @@ pyawaitable_async_with_impl( return -1; } PyObject *exit = PyObject_GetAttrString(ctx, "__aexit__"); - if (exit == NULL) - { + if (exit == NULL) { Py_DECREF(with); PyErr_Format( PyExc_TypeError, @@ -116,25 +110,22 @@ pyawaitable_async_with_impl( return -1; } - PyObject *inner_aw = pyawaitable_new_impl(); + PyObject *inner_aw = PyAwaitable_New(); - if (inner_aw == NULL) - { + if (inner_aw == NULL) { Py_DECREF(with); Py_DECREF(exit); return -1; } - if (pyawaitable_save_arb_impl(inner_aw, 2, cb, err) < 0) - { + if (PyAwaitable_SaveArbValues(inner_aw, 2, cb, err) < 0) { Py_DECREF(inner_aw); Py_DECREF(with); Py_DECREF(exit); return -1; } - if (pyawaitable_save_impl(inner_aw, 1, exit) < 0) - { + if (PyAwaitable_SaveValues(inner_aw, 1, exit) < 0) { Py_DECREF(inner_aw); Py_DECREF(exit); Py_DECREF(with); @@ -146,22 +137,20 @@ pyawaitable_async_with_impl( PyObject *coro = PyObject_CallNoArgs(with); Py_DECREF(with); - if (coro == NULL) - { + if (coro == NULL) { Py_DECREF(inner_aw); return -1; } // Note: Errors in __aenter__ are not sent to __aexit__ if ( - pyawaitable_await_impl( + PyAwaitable_AddAwait( inner_aw, coro, async_with_inner, NULL ) < 0 - ) - { + ) { Py_DECREF(inner_aw); Py_DECREF(coro); return -1; @@ -169,8 +158,7 @@ pyawaitable_async_with_impl( Py_DECREF(coro); - if (pyawaitable_await_impl(aw, inner_aw, NULL, err) < 0) - { + if (PyAwaitable_AddAwait(aw, inner_aw, NULL, err) < 0) { Py_DECREF(inner_aw); return -1; } diff --git a/src/pyawaitable/__init__.py b/src/pyawaitable/__init__.py index c0416e6..2967dd5 100644 --- a/src/pyawaitable/__init__.py +++ b/src/pyawaitable/__init__.py @@ -1,27 +1,29 @@ """ PyAwaitable - Call asynchronous code from an extension module. +It's unlikely that you want to import this module from Python, other than +for use in setuptools. + Docs: https://awaitable.zintensity.dev/ Source: https://github.com/ZeroIntensity/pyawaitable """ -from typing import Type - -from _pyawaitable import _PyAwaitableType # type: ignore - -from . import abi - -__all__ = "PyAwaitable", "include", "abi" -__version__ = "1.4.0" +__all__ = ("include",) +__version__ = "2.0.0-dev0" __author__ = "Peter Bierma" -PyAwaitable: Type = _PyAwaitableType - -def include() -> str: +def include(*, suppress_error: bool = False) -> str: """ Get the directory containing the `pyawaitable.h` file. """ import os + from pathlib import Path + + directory = Path(__file__).parent + if "pyawaitable.h" not in os.listdir(directory) and not suppress_error: + raise RuntimeError( + "pyawaitable.h wasn't found! Are you sure your installation is correct?" + ) - return os.path.dirname(__file__) + return str(directory.absolute()) diff --git a/src/pyawaitable/abi.py b/src/pyawaitable/abi.py deleted file mode 100644 index 1864910..0000000 --- a/src/pyawaitable/abi.py +++ /dev/null @@ -1,5 +0,0 @@ -from _pyawaitable import abi_v1 - -__all__ = ("v1",) - -v1 = abi_v1 diff --git a/src/pyawaitable/bindings.py b/src/pyawaitable/bindings.py deleted file mode 100644 index 0169b90..0000000 --- a/src/pyawaitable/bindings.py +++ /dev/null @@ -1,181 +0,0 @@ -import ctypes -from ctypes import pythonapi -from typing import Any - -from typing_extensions import Self - -from . import abi - -__all__ = ["abi", "add_await", "awaitcallback", "awaitcallback_err", "defer_callback", "defer_await"] - -get_pointer = pythonapi.PyCapsule_GetPointer -get_pointer.argtypes = (ctypes.py_object, ctypes.c_void_p) -get_pointer.restype = ctypes.c_void_p - -get_name = pythonapi.PyCapsule_GetName -get_name.argtypes = (ctypes.py_object,) -get_name.restype = ctypes.c_char_p - - -class PyABI(ctypes.Structure): - @classmethod - def from_capsule(cls, capsule: Any) -> Self: - # Assume that argtypes and restype have been properly set - capsule_name = get_name(capsule) - abi = ctypes.cast( - get_pointer(capsule, capsule_name), ctypes.POINTER(cls) - ) - - return abi.contents - - def __init_subclass__(cls, **kwargs) -> None: - super().__init_subclass__(**kwargs) - # Assume that _fields_ is a list - cls._fields_.insert(0, ("size", ctypes.c_ssize_t)) # type: ignore - - def __getattribute__(self, name: str) -> Any: - size = super().__getattribute__("size") - offset = getattr(type(self), name).offset - - if size <= offset: - raise ValueError(f"{name!r} is not available on this ABI version") - - attr = super().__getattribute__(name) - return attr - - -awaitcallback = ctypes.PYFUNCTYPE( - ctypes.c_int, ctypes.py_object, ctypes.py_object -) -awaitcallback_err = awaitcallback -defer_callback = ctypes.PYFUNCTYPE( - ctypes.c_int, ctypes.py_object) - - -class AwaitableABI(PyABI): - _fields_ = [ - ("new", ctypes.PYFUNCTYPE(ctypes.py_object)), - ( - "await", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.py_object, - awaitcallback, - awaitcallback_err, - ), - ), - ("cancel", ctypes.PYFUNCTYPE(None, ctypes.py_object)), - ( - "set_result", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.py_object, - ), - ), - ( - "save", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.c_ssize_t, - ), - ), - ( - "save_arb", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.c_ssize_t, - ), - ), - ("unpack", ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.py_object)), - ("unpack_arb", ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.py_object)), - ("PyAwaitableType", ctypes.py_object), - ( - "await_function", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.py_object, - ctypes.c_char_p, - awaitcallback, - awaitcallback_err, - ), - ), - ( - "save_int", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.c_ssize_t, - ), - ), - ("unpack_int", ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.py_object)), - ( - "set", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.c_ssize_t, - ctypes.py_object, - ), - ), - ( - "set_arb", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.c_ssize_t, - ctypes.c_void_p, - ), - ), - ( - "set_int", - ctypes.PYFUNCTYPE( - ctypes.c_int, ctypes.py_object, ctypes.c_ssize_t, ctypes.c_long - ), - ), - ( - "get", - ctypes.PYFUNCTYPE( - ctypes.py_object, ctypes.py_object, ctypes.c_ssize_t - ), - ), - ( - "get_arb", - ctypes.PYFUNCTYPE( - ctypes.c_void_p, ctypes.py_object, ctypes.c_ssize_t - ), - ), - ( - "get_int", - ctypes.PYFUNCTYPE( - ctypes.c_long, ctypes.py_object, ctypes.c_ssize_t - ), - ), - ( - "async_with", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - ctypes.py_object, - awaitcallback, - awaitcallback_err, - ), - ), - ( - "defer_await", - ctypes.PYFUNCTYPE( - ctypes.c_int, - ctypes.py_object, - defer_callback - ), - ), - ] - - -abi = AwaitableABI.from_capsule(abi.v1) -add_await = getattr(abi, "await") -defer_await = getattr(abi, "defer_await") diff --git a/src/pyawaitable/pyawaitable.h b/src/pyawaitable/pyawaitable.h deleted file mode 100644 index e3c4134..0000000 --- a/src/pyawaitable/pyawaitable.h +++ /dev/null @@ -1,165 +0,0 @@ -#ifndef PYAWAITABLE_H -#define PYAWAITABLE_H -#include - -#define PYAWAITABLE_MAJOR_VERSION 1 -#define PYAWAITABLE_MINOR_VERSION 4 -#define PYAWAITABLE_MICRO_VERSION 0 -/* Per CPython Conventions: 0xA for alpha, 0xB for beta, 0xC for release candidate or 0xF for final. */ -#define PYAWAITABLE_RELEASE_LEVEL 0xF - -typedef int (*awaitcallback)(PyObject *, PyObject *); -typedef int (*awaitcallback_err)(PyObject *, PyObject *); -typedef int (*defer_callback)(PyObject *); - -typedef struct _PyAwaitableObject PyAwaitableObject; - -typedef struct _pyawaitable_abi -{ - Py_ssize_t size; - PyObject *(*new)(void); - int (*await)( - PyObject *, - PyObject *, - awaitcallback, - awaitcallback_err - ); - void (*cancel)(PyObject *); - int (*set_result)(PyObject *, PyObject *); - int (*save)(PyObject *, Py_ssize_t, ...); - int (*save_arb)(PyObject *, Py_ssize_t, ...); - int (*unpack)(PyObject *, ...); - int (*unpack_arb)(PyObject *, ...); - PyTypeObject *PyAwaitableType; - int (*await_function)( - PyObject *, - PyObject *, - const char *fmt, - awaitcallback, - awaitcallback_err, - ... - ); - int (*save_int)(PyObject *, Py_ssize_t nargs, ...); - int (*unpack_int)(PyObject *, ...); - int (*set)(PyObject *, Py_ssize_t, PyObject *); - int (*set_arb)(PyObject *, Py_ssize_t, void *); - int (*set_int)(PyObject *, Py_ssize_t, long); - PyObject *(*get)(PyObject *, Py_ssize_t); - void *(*get_arb)(PyObject *, Py_ssize_t); - long (*get_int)(PyObject *, Py_ssize_t); - int (*async_with)( - PyObject *aw, - PyObject *ctx, - awaitcallback cb, - awaitcallback_err err - ); - int (*defer_await)( - PyObject *aw, - defer_callback cb - ); -} PyAwaitableABI; - -#ifdef PYAWAITABLE_THIS_FILE_INIT -PyAwaitableABI *pyawaitable_abi = NULL; -#else -extern PyAwaitableABI *pyawaitable_abi; -#endif - -#define pyawaitable_new pyawaitable_abi->new -#define pyawaitable_cancel pyawaitable_abi->cancel -#define pyawaitable_set_result pyawaitable_abi->set_result - -#define pyawaitable_await pyawaitable_abi->await -#define pyawaitable_await_function pyawaitable_abi->await_function -#define pyawaitable_async_with pyawaitable_abi->async_with -#define pyawaitable_defer_await pyawaitable_abi->defer_await - -#define pyawaitable_save pyawaitable_abi->save -#define pyawaitable_save_arb pyawaitable_abi->save_arb -#define pyawaitable_save_int pyawaitable_abi->save_int - -#define pyawaitable_unpack pyawaitable_abi->unpack -#define pyawaitable_unpack_arb pyawaitable_abi->unpack_arb -#define pyawaitable_unpack_int pyawaitable_abi->unpack_int - -#define pyawaitable_set pyawaitable_abi->set -#define pyawaitable_set_arb pyawaitable_abi->set_arb -#define pyawaitable_set_int pyawaitable_abi->set_int - -#define pyawaitable_get pyawaitable_abi->get -#define pyawaitable_get_arb pyawaitable_abi->get_arb -#define pyawaitable_get_int pyawaitable_abi->get_int - -#define PyAwaitableType pyawaitable_abi->PyAwaitableType - - -#ifdef PYAWAITABLE_THIS_FILE_INIT -static int -pyawaitable_init() -{ - if (pyawaitable_abi != NULL) - return 0; - - PyAwaitableABI *capsule = PyCapsule_Import("_pyawaitable.abi_v1", 0); - if (capsule == NULL) - return -1; - - pyawaitable_abi = capsule; - return 0; -} - -#else -static int -pyawaitable_init() -{ - PyErr_SetString( - PyExc_RuntimeError, - "pyawaitable_init() can only be called in a file with a PYAWAITABLE_THIS_FILE_INIT #define" - ); - return -1; -} - -#endif - -#ifdef PYAWAITABLE_PYAPI -#define PyAwaitable_Init pyawaitable_init -#define PyAwaitable_ABI pyawaitable_abi -#define PyAwaitable_Type PyAwaitableType - -#define PyAwaitable_New pyawaitable_new -#define PyAwaitable_Cancel pyawaitable_cancel -#define PyAwaitable_SetResult pyawaitable_set_result - -#define PyAwaitable_AddAwait pyawaitable_await -#define PyAwaitable_AwaitFunction pyawaitable_await_function -#define PyAwaitable_AsyncWith pyawaitable_async_with -#define PyAwaitable_DeferAwait pyawaitable_defer_await - -#define PyAwaitable_SaveValues pyawaitable_save -#define PyAwaitable_SaveArbValues pyawaitable_save_arb -#define PyAwaitable_SaveIntValues pyawaitable_save_int - -#define PyAwaitable_UnpackValues pyawaitable_unpack -#define PyAwaitable_UnpackArbValues pyawaitable_unpack_arb -#define PyAwaitable_UnpackIntValues pyawaitable_unpack_int - -#define PyAwaitable_SetValue pyawaitable_set -#define PyAwaitable_SetArbValue pyawaitable_set_arb -#define PyAwaitable_SetIntValue pyawaitable_set_int - -#define PyAwaitable_GetValue pyawaitable_set -#define PyAwaitable_GetArbValue pyawaitable_get_arb -#define PyAwaitable_GetIntValue pyawaitable_get_int -#endif - -static int -pyawaitable_vendor_init(PyObject *mod) -{ - PyErr_SetString( - PyExc_SystemError, - "cannot use pyawaitable_vendor_init from an installed version, use pyawaitable_init instead!" - ); - return -1; -} - -#endif diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index f10bbc6..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,58 +0,0 @@ -import ctypes -import functools -import inspect -import platform -import sys -from typing import Any, Callable - -import pytest - -from pyawaitable.bindings import awaitcallback, awaitcallback_err - -try: - import _pyawaitable_test -except ImportError: - pytest.exit( - "PyAwaitable testing package has not been build! (Hint: pip install tests/extension --no-build-isolation)", # noqa - returncode=-1, - ) - -ITERATIONS: int = 1000 -LEAK_LIMIT: str = "100 KB" - -raising_callback = ctypes.cast( - _pyawaitable_test.raising_callback, awaitcallback -) -raising_err_callback = ctypes.cast( - _pyawaitable_test.raising_err_callback, awaitcallback_err -) - - -def pytest_addoption(parser: Any) -> None: - parser.addoption("--enable-leak-tracking", action="store_true") - - -def limit_leaks(func: Callable): - if "--enable-leak-tracking" not in sys.argv: - return func - - if platform.system() != "Windows": - if not inspect.iscoroutinefunction(func): - - @functools.wraps(func) - def wrapper(*args, **kwargs): # type: ignore - for _ in range(ITERATIONS): - func(*args, **kwargs) - - else: - - @functools.wraps(func) - async def wrapper(*args, **kwargs): - for _ in range(ITERATIONS): - await func(*args, **kwargs) - - wrapper = pytest.mark.asyncio(wrapper) - - return pytest.mark.limit_leaks(LEAK_LIMIT)(wrapper) - else: - return func diff --git a/tests/extension/a.c b/tests/extension/a.c deleted file mode 100644 index 078c4b7..0000000 --- a/tests/extension/a.c +++ /dev/null @@ -1,8 +0,0 @@ -#include -#include - -PyObject * -test2(PyObject *self, PyObject *args) -{ - return pyawaitable_new(); -} diff --git a/tests/extension/pyproject.toml b/tests/extension/pyproject.toml deleted file mode 100644 index 087ee59..0000000 --- a/tests/extension/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build-system] -requires = ["setuptools", "pip"] -build-backend = "setuptools.build_meta" diff --git a/tests/extension/setup.py b/tests/extension/setup.py deleted file mode 100644 index c9b8efb..0000000 --- a/tests/extension/setup.py +++ /dev/null @@ -1,15 +0,0 @@ -from setuptools import Extension, setup -import pyawaitable - -if __name__ == "__main__": - setup( - name="pyawaitable_test", - ext_modules=[ - Extension( - "_pyawaitable_test", - sources=["./test.c", "./a.c"], - include_dirs=[pyawaitable.include()], - ) - ], - license="MIT", - ) diff --git a/tests/extension/test.c b/tests/extension/test.c deleted file mode 100644 index fc8abd8..0000000 --- a/tests/extension/test.c +++ /dev/null @@ -1,84 +0,0 @@ -#define PYAWAITABLE_THIS_FILE_INIT -#include -#include -#include - -#define ADD_ADDRESS(ptr) \ - do { PyObject *py_int = PyLong_FromVoidPtr((void *) ptr); \ - if (!py_int) { \ - Py_DECREF(m); \ - return NULL; \ - } \ - if (PyModule_AddObject(m, #ptr, py_int) < 0) { \ - Py_DECREF(m); \ - Py_DECREF(py_int); \ - return NULL; \ - } \ - } while (0); - -PyObject * -test2(PyObject *self, PyObject *args); - -static int -callback(PyObject *awaitable, PyObject *result) -{ - return pyawaitable_set_result(awaitable, result); -} - -static PyObject * -test(PyObject *self, PyObject *coro) -{ - PyObject *awaitable = pyawaitable_new(); - if (pyawaitable_await(awaitable, coro, callback, NULL) < 0) - { - Py_DECREF(awaitable); - return NULL; - } - - return awaitable; -} - -int -raising_callback(PyObject *awaitable, PyObject *result) -{ - PyErr_SetString(PyExc_RuntimeError, "test"); - return -1; -} - -int -raising_err_callback(PyObject *awaitable, PyObject *result) -{ - PyErr_SetString(PyExc_ZeroDivisionError, "test"); - return -2; -} - -static PyMethodDef methods[] = -{ - {"test", test, METH_O, NULL}, - {"test2", test2, METH_NOARGS, NULL}, - {NULL, NULL, 0, NULL} -}; - -static PyModuleDef module = -{ - PyModuleDef_HEAD_INIT, - "_pyawaitable_test", - NULL, - -1, - .m_methods = methods -}; - -PyMODINIT_FUNC -PyInit__pyawaitable_test() -{ - if (pyawaitable_init() < 0) - return NULL; - - PyObject *m = PyModule_Create(&module); - if (!m) - return NULL; - - ADD_ADDRESS(raising_callback); - ADD_ADDRESS(raising_err_callback); - return m; -} diff --git a/tests/main.py b/tests/main.py new file mode 100644 index 0000000..9df8a5d --- /dev/null +++ b/tests/main.py @@ -0,0 +1,45 @@ +import unittest +import asyncio +from typing import Any, Callable +from collections.abc import Awaitable, Coroutine +import inspect + +NOT_FOUND = """ +The PyAwaitable test package wasn't built! +Please install it with `pip install ./tests` +""" +try: + import _pyawaitable_test +except ImportError as err: + raise RuntimeError(NOT_FOUND) from err + +class PyAwaitableTests(unittest.TestCase): + def test_awaitable_semantics(self): + awaitable = _pyawaitable_test.generic_awaitable(None) + self.assertIsInstance(awaitable, Awaitable) + self.assertIsInstance(awaitable, Coroutine) + # It's not a *native* coroutine + self.assertFalse(inspect.iscoroutine(awaitable)) + + with self.assertWarns(ResourceWarning): + del awaitable + +def coro_wrap_call(method: Callable[[Awaitable[Any]], Any]) -> Callable[[], None]: + def wrapper(*_: Any): + async def dummy(): + await asyncio.sleep(0) + + method(dummy()) + + return wrapper + +for method in dir(_pyawaitable_test): + if method.startswith("test_"): + test_case = getattr(_pyawaitable_test, method) + if method.endswith("needs_coro"): + setattr(PyAwaitableTests, method.rstrip("_needs_coro"), coro_wrap_call(test_case)) + else: + setattr(PyAwaitableTests, method, test_case) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/module.c b/tests/module.c new file mode 100644 index 0000000..1e281a4 --- /dev/null +++ b/tests/module.c @@ -0,0 +1,35 @@ +#include +#include +#include "pyawaitable_test.h" + +static int +_pyawaitable_test_exec(PyObject *mod) +{ +#define ADD_TESTS(name) \ + do { \ + if (PyModule_AddFunctions(mod, _pyawaitable_test_ ## name) < 0) { \ + return -1; \ + } \ + } while (0) + + ADD_TESTS(awaitable); +#undef ADD_TESTS + return PyAwaitable_Init(); +} + +static PyModuleDef_Slot _pyawaitable_test_slots[] = { + {Py_mod_exec, _pyawaitable_test_exec}, + {0, NULL} +}; + +static PyModuleDef _pyawaitable_test_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_size = 0, + .m_slots = _pyawaitable_test_slots, +}; + +PyMODINIT_FUNC +PyInit__pyawaitable_test() +{ + return PyModuleDef_Init(&_pyawaitable_test_module); +} diff --git a/tests/pyawaitable_test.h b/tests/pyawaitable_test.h new file mode 100644 index 0000000..7e2df08 --- /dev/null +++ b/tests/pyawaitable_test.h @@ -0,0 +1,9 @@ +#ifndef PYAWAITABLE_TEST_H +#define PYAWAITABLE_TEST_H + +#include +#include "test_util.h" + +extern TESTS(awaitable); + +#endif diff --git a/tests/pyproject.toml b/tests/pyproject.toml new file mode 100644 index 0000000..8b394ee --- /dev/null +++ b/tests/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyawaitable_test" +version = "0.0.0" diff --git a/tests/setup.py b/tests/setup.py new file mode 100644 index 0000000..70fc42c --- /dev/null +++ b/tests/setup.py @@ -0,0 +1,30 @@ +from setuptools import setup, Extension +from pathlib import Path +from glob import glob + +NOT_FOUND = """pyawaitable.h wasn't found! It probably wasn't built. + +To build a working copy, you can either install the package +locally (via `pip install .`), or by executing the `hatch_build.py` file. +""" + +def find_local_pyawaitable() -> str: + """Find the directory containing the local copy of pyawaitable.h""" + top_level = Path(__file__).parent.parent + source = top_level / "src" / "pyawaitable" + if not (source / "pyawaitable.h").exists(): + raise RuntimeError(NOT_FOUND) + + return str(source.absolute()) + +if __name__ == "__main__": + setup( + ext_modules=[ + Extension( + "_pyawaitable_test", + glob("*.c"), + include_dirs=[find_local_pyawaitable()], + extra_compile_args=["-O0", "-g3"] + ) + ] + ) diff --git a/tests/test_awaitable.c b/tests/test_awaitable.c new file mode 100644 index 0000000..c44be9c --- /dev/null +++ b/tests/test_awaitable.c @@ -0,0 +1,159 @@ +#include +#include +#include "pyawaitable_test.h" + +static PyObject * +generic_awaitable(PyObject *self, PyObject *coro) +{ + PyObject *awaitable = PyAwaitable_New(); + if (awaitable == NULL) { + return NULL; + } + + if (coro != Py_None) { + if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { + Py_DECREF(awaitable); + return NULL; + } + } + + return awaitable; +} + +static PyObject * +test_awaitable_new(PyObject *self, PyObject *nothing) +{ + PyObject *awaitable = PyAwaitable_New(); + if (awaitable == NULL) { + return NULL; + } + + TEST_ASSERT(Py_IS_TYPE(awaitable, PyAwaitable_GetType())); + PyAwaitable_Cancel(awaitable); // Prevent warning + Py_DECREF(awaitable); + + Test_SetNoMemory(); + PyObject *fail_alloc = PyAwaitable_New(); + Test_UnSetNoMemory(); + EXPECT_ERROR(PyExc_MemoryError); + TEST_ASSERT(fail_alloc == NULL); + Py_RETURN_NONE; +} + +static PyObject * +test_set_result(PyObject *self, PyObject *nothing) +{ + PyObject *awaitable = PyAwaitable_New(); + if (awaitable == NULL) { + return NULL; + } + + PyObject *value = PyLong_FromLong(42); + if (value == NULL) { + Py_DECREF(awaitable); + return NULL; + } + + if (PyAwaitable_SetResult(awaitable, value) < 0) { + Py_DECREF(value); + Py_DECREF(awaitable); + return NULL; + } + + TEST_ASSERT(Py_REFCNT(value) > 1); + Py_DECREF(value); + return Test_RunAndCheck(awaitable, value); +} + +static PyObject * +test_add_await(PyObject *self, PyObject *coro) +{ + PyObject *awaitable = PyAwaitable_New(); + if (awaitable == NULL) { + return NULL; + } + + TEST_ASSERT(PyAwaitable_AddAwait(awaitable, self, NULL, NULL) < 0); + EXPECT_ERROR(PyExc_TypeError); + + TEST_ASSERT(PyAwaitable_AddAwait(awaitable, NULL, NULL, NULL) < 0); + EXPECT_ERROR(PyExc_ValueError); + + TEST_ASSERT(PyAwaitable_AddAwait(awaitable, awaitable, NULL, NULL) < 0); + EXPECT_ERROR(PyExc_ValueError); + + if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { + Py_DECREF(awaitable); + return NULL; + } + + return Test_RunAwaitable(awaitable); +} + +static PyObject * +test_add_await_no_memory(PyObject *self, PyObject *coro) +{ + PyObject *awaitable = PyAwaitable_New(); + if (awaitable == NULL) { + return NULL; + } + + // Exhaust any preallocated buffers + for (int i = 0; i < 16; ++i) { + PyObject *dummy = PyAwaitable_New(); + if (dummy == NULL) { + return NULL; + } + + if (PyAwaitable_AddAwait(awaitable, dummy, NULL, NULL) < 0) { + Py_DECREF(awaitable); + Py_DECREF(dummy); + return NULL; + } + + TEST_ASSERT(Py_REFCNT(dummy) > 1); + Py_DECREF(dummy); + } +#if PY_MINOR_VERSION < 11 +/* Apparently, it's not a MemoryError for exceptions on <3.11 */ +#define EXPECT_ERROR_NOMEM(exc) EXPECT_ERROR(exc) +#else +#define EXPECT_ERROR_NOMEM(exc) EXPECT_ERROR(PyExc_MemoryError) +#endif + + int res; + Test_SetNoMemory(); + res = PyAwaitable_AddAwait(awaitable, NULL, NULL, NULL); + Test_UnSetNoMemory(); + EXPECT_ERROR_NOMEM(PyExc_ValueError); + TEST_ASSERT(res < 0); + + Test_SetNoMemory(); + res = PyAwaitable_AddAwait(awaitable, awaitable, NULL, NULL); + Test_UnSetNoMemory(); + EXPECT_ERROR_NOMEM(PyExc_ValueError); + TEST_ASSERT(res < 0); + + Test_SetNoMemory(); + res = PyAwaitable_AddAwait(awaitable, coro, NULL, NULL); + Test_UnSetNoMemory(); + EXPECT_ERROR_NOMEM(PyExc_TypeError); + TEST_ASSERT(res < 0); + + // Actually await it to prevent the warning + if (PyAwaitable_AddAwait(awaitable, coro, NULL, NULL) < 0) { + Py_DECREF(awaitable); + return NULL; + } + + return Test_RunAwaitable(awaitable); +} + +TESTS(awaitable) = { + TEST_UTIL(generic_awaitable), + TEST(test_awaitable_new), + TEST(test_set_result), + TEST_CORO(test_add_await), + TEST_CORO(test_add_await_no_memory), + {NULL} +}; diff --git a/tests/test_awaitable.py b/tests/test_awaitable.py deleted file mode 100644 index c6a27ef..0000000 --- a/tests/test_awaitable.py +++ /dev/null @@ -1,218 +0,0 @@ -import asyncio -from collections.abc import Coroutine - -import _pyawaitable_test -import pytest -from conftest import limit_leaks - -import pyawaitable -from pyawaitable.bindings import (abi, add_await, awaitcallback, - awaitcallback_err) - - -@limit_leaks -@pytest.mark.asyncio -async def test_new(): - awaitable = abi.new() - assert isinstance(awaitable, pyawaitable.PyAwaitable) - assert isinstance(awaitable, Coroutine) - await awaitable - await asyncio.create_task(abi.new()) - await abi.new() - - -@limit_leaks -@pytest.mark.asyncio -async def test_await(): - called = False - - async def coro(): - for _ in range(10000): - await asyncio.sleep(0) - nonlocal called - called = True - - awaitable = abi.new() - add_await(awaitable, coro(), awaitcallback(0), awaitcallback_err(0)) - await awaitable - assert called is True - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_order(): - data = [] - - awaitable = abi.new() - - async def echo(value: int) -> int: - await asyncio.sleep(0) - return value - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - data.append(result) - return 0 - - for i in (1, 2, 3): - add_await(awaitable, echo(i), cb, awaitcallback_err(0)) - - await awaitable - assert data == [1, 2, 3] - - -@limit_leaks -@pytest.mark.asyncio -@pytest.mark.filterwarnings( - "ignore::RuntimeWarning" -) # Second and third iteration of echo() are skipped, resulting in a warning -async def test_await_cancel(): - data = [] - - awaitable = abi.new() - - async def echo(value: int) -> int: - await asyncio.sleep(0) - return value - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - assert result == 1 - abi.cancel(awaitable_inner) - data.append(result) - return 0 - - for i in (1, 2, 3): - add_await(awaitable, echo(i), cb, awaitcallback_err(0)) - - await awaitable - assert data == [1] - - -@limit_leaks -@pytest.mark.asyncio -async def test_awaitable_chaining(): - data = [] - - awaitable = abi.new() - - async def echo(value: int) -> int: - await asyncio.sleep(0) - return value - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - abi.cancel(awaitable_inner) - data.append(result) - return 0 - - awaitable2 = abi.new() - add_await(awaitable2, echo(1), cb, awaitcallback_err(0)) - add_await(awaitable, awaitable2, awaitcallback(0), awaitcallback_err(0)) - - await awaitable - assert data == [1] - - -@limit_leaks -@pytest.mark.asyncio -async def test_coro_raise(): - awaitable = abi.new() - - async def coro() -> None: - await asyncio.sleep(0) - raise ZeroDivisionError("test") - - add_await(awaitable, coro(), awaitcallback(0), awaitcallback_err(0)) - - with pytest.raises(ZeroDivisionError): - await awaitable - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_no_cb_raise(): - awaitable = abi.new() - - add_await(awaitable, 42, awaitcallback(0), awaitcallback_err(0)) - - with pytest.raises(TypeError): - await awaitable - - -@limit_leaks -@pytest.mark.asyncio -async def test_set_results(): - awaitable = abi.new() - - async def coro(): - await asyncio.sleep(0) - return "abc" - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: str): - abi.set_result(awaitable_inner, 42) - return 0 - - add_await(awaitable, coro(), cb, awaitcallback(0)) - assert (await awaitable) == 42 - - -@limit_leaks -@pytest.mark.asyncio -async def test_set_results_tracked_type(): - awaitable = abi.new() - - class TestObject: - def __init__(self) -> None: - self.value = [1, 2, 3] - - async def coro(): - pass - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: str): - obj = TestObject() - obj.value.append(4) - abi.set_result(awaitable_inner, obj) - return 0 - - add_await(awaitable, coro(), cb, awaitcallback(0)) - value = await awaitable - assert isinstance(value, TestObject) - assert value.value == [1, 2, 3, 4] - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_function(): - awaitable = abi.new() - called: bool = False - - async def coro(value: int, suffix: str) -> str: - await asyncio.sleep(0) - return str(value * 2) + suffix - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: str): - nonlocal called - called = True - assert result == "42hello" - return 0 - - abi.await_function( - awaitable, coro, b"is", cb, awaitcallback_err(0), 21, b"hello" - ) - await awaitable - assert called is True - - -@limit_leaks -@pytest.mark.asyncio -async def test_c_built_extension(): - async def hello(): - await asyncio.sleep(0) - return 42 - - assert (await _pyawaitable_test.test(hello())) == 42 - assert (await _pyawaitable_test.test2()) is None diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py deleted file mode 100644 index 9f04508..0000000 --- a/tests/test_callbacks.py +++ /dev/null @@ -1,204 +0,0 @@ -import asyncio - -import pytest -from conftest import limit_leaks, raising_callback, raising_err_callback - -import pyawaitable -from pyawaitable.bindings import (abi, add_await, awaitcallback, - awaitcallback_err, defer_callback, - defer_await) - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_cb(): - awaitable = abi.new() - - async def coro(value: int): - await asyncio.sleep(0) - return value * 2 - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - assert awaitable_inner is awaitable - assert result == 42 - return 0 - - add_await(awaitable, coro(21), cb, awaitcallback_err(0)) - await awaitable - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_cb_err(): - awaitable = abi.new() - - async def coro_raise() -> float: - await asyncio.sleep(0) - return 0 / 0 - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: float) -> int: - raise RuntimeError(b"no good!") - - @awaitcallback_err - def cb_err( - awaitable_inner: pyawaitable.PyAwaitable, - err: Exception, - ) -> int: - assert isinstance(err, ZeroDivisionError) - return 0 - - add_await(awaitable, coro_raise(), cb, cb_err) - await awaitable - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_cb_err_cb(): - awaitable = abi.new() - - async def coro() -> int: - await asyncio.sleep(0) - return 42 - - @awaitcallback_err - def cb_err( - awaitable_inner: pyawaitable.PyAwaitable, - err: Exception, - ) -> int: - assert isinstance(err, RuntimeError) - assert str(err) == "test" - return 0 - - add_await( - awaitable, - coro(), - raising_callback, - cb_err, - ) - await awaitable - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_cb_noerr(): - awaitable = abi.new() - - async def coro() -> int: - await asyncio.sleep(0) - return 42 - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - return -1 - - add_await(awaitable, coro(), cb, awaitcallback_err(0)) - - with pytest.raises(RuntimeError): - await awaitable - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_cb_err_restore(): - awaitable = abi.new() - called = False - - async def coro() -> int: - await asyncio.sleep(0) - return 42 - - @awaitcallback_err - def cb_err( - awaitable_inner: pyawaitable.PyAwaitable, - err: Exception, - ) -> int: - assert str(err) == "test" - nonlocal called - called = True - return -1 - - add_await(awaitable, coro(), raising_callback, cb_err) - - with pytest.raises(RuntimeError): - await awaitable - - assert called is True - - -@limit_leaks -@pytest.mark.asyncio -async def test_await_cb_err_norestore(): - awaitable = abi.new() - called = False - - async def coro() -> int: - nonlocal called - called = True - await asyncio.sleep(0) - return 42 - - add_await(awaitable, coro(), raising_callback, raising_err_callback) - - with pytest.raises(ZeroDivisionError): - await awaitable - - assert called is True - -@limit_leaks -@pytest.mark.asyncio -async def test_a_lot_of_coroutines(): - awaitable = abi.new() - amount = 500 - - awaited = 0 - called = 0 - - async def coro(): - await asyncio.sleep(0) - nonlocal awaited - awaited += 1 - - @awaitcallback - def callback(awaitable: pyawaitable.PyAwaitable, result: None) -> int: - assert result is None - nonlocal called - called += 1 - return 0 - - for _ in range(amount): - add_await(awaitable, coro(), callback, awaitcallback_err(0)) - - await awaitable - assert called == amount - assert awaited == amount - -@limit_leaks -@pytest.mark.asyncio -async def test_deferred_await(): - called = 0 - awaitable = abi.new() - - async def coro(value: int): - await asyncio.sleep(0) - return value * 2 - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - assert awaitable_inner is awaitable - assert result == 42 - nonlocal called - called += 1 - return 0 - - @defer_callback - def defer_cb(awaitable: pyawaitable.PyAwaitable): - nonlocal called - called += 1 - add_await(awaitable, coro(21), cb, awaitcallback_err(0)) - return 0 - - defer_await(awaitable, defer_cb) - await awaitable - assert called == 2 diff --git a/tests/test_util.h b/tests/test_util.h new file mode 100644 index 0000000..3c8d10f --- /dev/null +++ b/tests/test_util.h @@ -0,0 +1,66 @@ +#ifndef PYAWAITABLE_TEST_UTIL_H +#define PYAWAITABLE_TEST_UTIL_H + +#include + +#define TEST(name) {#name, name, METH_NOARGS, NULL} +#define TEST_UTIL(name) {#name, name, METH_O, NULL} +#define TEST_CORO(name) {#name "_needs_coro", name, METH_O, NULL} +#define TEST_ERROR(msg) \ + PyErr_Format( \ + PyExc_AssertionError, \ + "%s (" __FILE__ ":%d): " msg, \ + __func__, \ + __LINE__ \ + ); +#define TEST_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + PyErr_Format( \ + PyExc_AssertionError, \ + "assertion failed in %s (" __FILE__ ":%d): " #cond, \ + __func__, \ + __LINE__ \ + ); \ + return NULL; \ + } \ + } while (0) +#define TESTS(name) PyMethodDef _pyawaitable_test_ ## name [] +#define EXPECT_ERROR(tp) \ + do { \ + if (!PyErr_Occurred()) { \ + TEST_ERROR( \ + "expected " #tp " to be raised, but nothing happened" \ + ); \ + return NULL; \ + } \ + if (!PyErr_ExceptionMatches((PyObject *)tp)) { \ + /* Let the unexpected error fall through */ \ + return NULL; \ + } \ + PyErr_Clear(); \ + } while (0) + +void Test_SetNoMemory(void); +void Test_UnSetNoMemory(void); +PyObject *Test_RunAwaitable(PyObject *awaitable); + +PyObject * +_Test_RunAndCheck( + PyObject *awaitable, + PyObject *expected, + const char *func, + const char *file, + int line +); + +#define Test_RunAndCheck(aw, ex) \ + _Test_RunAndCheck( \ + aw, \ + ex, \ + __func__, \ + __FILE__, \ + __LINE__ \ + ); + +#endif diff --git a/tests/test_values.py b/tests/test_values.py deleted file mode 100644 index b88bddb..0000000 --- a/tests/test_values.py +++ /dev/null @@ -1,270 +0,0 @@ -import asyncio -import ctypes - -import pytest -from conftest import limit_leaks - -import pyawaitable -from pyawaitable.bindings import (abi, add_await, awaitcallback, - awaitcallback_err) - - -@limit_leaks -@pytest.mark.asyncio -async def test_store_values(): - awaitable = abi.new() - - async def echo(value: int) -> int: - await asyncio.sleep(0) - return value - - data = ctypes.py_object([1, 2, 3]) - some_val = ctypes.py_object("test") - - abi.save(awaitable, 2, data, some_val) - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - data_inner = ctypes.py_object() - some_val_inner = ctypes.py_object() - abi.unpack( - awaitable_inner, - ctypes.byref(data_inner), - ctypes.byref(some_val_inner), - ) - assert data.value == data_inner.value - assert some_val.value == some_val_inner.value - assert abi.get(awaitable, 0) == data.value - data.value.append(4) - - with pytest.raises(IndexError): - abi.get(awaitable, 2) - - with pytest.raises(IndexError): - abi.get(awaitable, 200) - - with pytest.raises(IndexError): - abi.get(awaitable, -2) - - with pytest.raises(IndexError): - abi.set(awaitable, 2, "test") - - with pytest.raises(IndexError): - abi.set(awaitable, -42, "test") - - abi.set(awaitable, 1, "hello") - assert abi.get(awaitable, 1) == "hello" - foo = ctypes.py_object("foo") - bar = ctypes.py_object("bar") - - abi.save(awaitable, 2, foo, bar) - - foo_inner = ctypes.py_object() - bar_inner = ctypes.py_object() - - abi.unpack( - awaitable, - None, - None, - ctypes.byref(foo_inner), - ctypes.byref(bar_inner), - ) - assert foo_inner.value == "foo" - assert bar_inner.value == "bar" - - assert abi.get(awaitable, 2) == "foo" - assert abi.get(awaitable, 3) == "bar" - - return 0 - - add_await(awaitable, echo(42), cb, awaitcallback_err(0)) - await awaitable - assert data.value == [1, 2, 3, 4] - - -@limit_leaks -@pytest.mark.asyncio -async def test_store_arb_values(): - awaitable = abi.new() - - async def echo(value: int) -> int: - await asyncio.sleep(0) - return value - - buffer = ctypes.create_string_buffer(b"test") - abi.save_arb(awaitable, 1, ctypes.byref(buffer)) - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - buffer_inner = ctypes.c_char_p() - abi.unpack_arb(awaitable_inner, ctypes.byref(buffer_inner)) - assert buffer_inner.value == b"test" - assert ( - ctypes.cast(abi.get_arb(awaitable, 0), ctypes.c_char_p).value - == b"test" - ) - unicode = ctypes.create_unicode_buffer("hello") - - abi.save_arb( - awaitable, - 1, - ctypes.byref(unicode), - ) - unicode_inner = ctypes.c_wchar_p() - abi.unpack_arb( - awaitable_inner, - None, - ctypes.byref(unicode_inner), - ) - assert unicode_inner.value == "hello" - - assert ( - ctypes.cast( - abi.get_arb(awaitable_inner, 1), - ctypes.c_wchar_p, - ).value - == "hello" - ) - - with pytest.raises(IndexError): - abi.get_arb(awaitable_inner, 2) - - with pytest.raises(IndexError): - abi.get_arb(awaitable_inner, 300) - - with pytest.raises(IndexError): - abi.get_arb(awaitable_inner, -10) - - assert ( - ctypes.cast(abi.get_arb(awaitable_inner, 0), ctypes.c_char_p).value - == b"test" - ) - - with pytest.raises(IndexError): - abi.set_arb(awaitable_inner, 10, None) - - abi.set_arb(awaitable_inner, 0, ctypes.c_char_p(b"hello")) - assert ( - ctypes.cast(abi.get_arb(awaitable_inner, 0), ctypes.c_char_p).value - == b"hello" - ) - return 0 - - add_await(awaitable, echo(42), cb, awaitcallback_err(0)) - await awaitable - - -@limit_leaks -@pytest.mark.asyncio -async def test_null_save_arb(): - awaitable = abi.new() - - async def echo(value: int) -> int: - await asyncio.sleep(0) - return value - - buffer = ctypes.create_string_buffer(b"test") - buffer2 = ctypes.create_string_buffer(b"hello") - abi.save_arb(awaitable, 3, ctypes.byref(buffer), None, buffer2) - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - buffer_inner = ctypes.c_char_p() - null = ctypes.c_void_p() - buffer2_inner = ctypes.c_char_p() - abi.unpack_arb( - awaitable_inner, - ctypes.byref(buffer_inner), - ctypes.byref(null), - ctypes.byref(buffer2_inner), - ) - assert buffer_inner.value == b"test" - assert buffer2_inner.value == b"hello" - return 0 - - add_await(awaitable, echo(42), cb, awaitcallback_err(0)) - await awaitable - - -@limit_leaks -@pytest.mark.asyncio -async def test_int_values(): - awaitable = abi.new() - - abi.save_int( - awaitable, - 3, - ctypes.c_long(42), - ctypes.c_long(3000), - ctypes.c_long(-10), - ) - - @awaitcallback - def cb(awaitable_inner: pyawaitable.PyAwaitable, result: int) -> int: - first = ctypes.c_long() - second = ctypes.c_long() - third = ctypes.c_long() - abi.unpack_int( - awaitable_inner, - ctypes.byref(first), - ctypes.byref(second), - ctypes.byref(third), - ) - assert first.value == 42 - assert second.value == 3000 - assert third.value == -10 - assert abi.get_int(awaitable_inner, 0) == 42 - - with pytest.raises(IndexError): - abi.set_int(awaitable_inner, 10, ctypes.c_long(4)) - - with pytest.raises(IndexError): - abi.set_int(awaitable_inner, 3, ctypes.c_long(4)) - - abi.set_int(awaitable_inner, 2, ctypes.c_long(4)) - abi.unpack_int( - awaitable_inner, - ctypes.byref(first), - ctypes.byref(second), - ctypes.byref(third), - ) - - assert first.value == 42 - assert second.value == 3000 - assert third.value == 4 - - abi.set_int(awaitable_inner, 0, ctypes.c_long(-400)) - assert abi.get_int(awaitable_inner, 0) == -400 - assert abi.get_int(awaitable_inner, 2) == 4 - - with pytest.raises(IndexError): - abi.get_int(awaitable_inner, 3) - - with pytest.raises(IndexError): - abi.get_int(awaitable_inner, 100) - - with pytest.raises(IndexError): - abi.get_int(awaitable_inner, -10) - - abi.save_int( - awaitable, - 3, - ctypes.c_long(1), - ctypes.c_long(2), - ctypes.c_long(3), - ) - - assert abi.get_int(awaitable_inner, 5) == 3 - assert abi.get_int(awaitable_inner, 3) == 1 - abi.set_int(awaitable_inner, 5, ctypes.c_long(1000)) - assert abi.get_int(awaitable_inner, 5) == 1000 - - with pytest.raises(IndexError): - abi.get_int(awaitable_inner, 10) - - return 0 - - async def coro(): ... - - add_await(awaitable, coro(), cb, awaitcallback_err(0)) - await awaitable diff --git a/tests/test_with.py b/tests/test_with.py deleted file mode 100644 index d6525e0..0000000 --- a/tests/test_with.py +++ /dev/null @@ -1,188 +0,0 @@ -from contextlib import asynccontextmanager - -import pytest -from conftest import limit_leaks, raising_callback - -import pyawaitable -from pyawaitable.bindings import abi, awaitcallback, awaitcallback_err - - -@limit_leaks -@pytest.mark.asyncio -async def test_async_with(): - called = False - - @asynccontextmanager - async def my_context(): - try: - yield 1 - finally: - nonlocal called - called = True - - @awaitcallback - def inner(inner: pyawaitable.PyAwaitable, val: int) -> int: - assert val == 1 - return 0 - - aw = abi.new() - abi.async_with(aw, my_context(), inner, awaitcallback_err(0)) - - class Half: - def __aenter__(self): ... - - class OtherHalf: - def __aexit__(self, *args): ... - - with pytest.raises(TypeError): - abi.async_with(aw, 1, inner, awaitcallback_err(0)) - - with pytest.raises(TypeError): - abi.async_with(aw, Half(), inner, awaitcallback_err(0)) - - with pytest.raises(TypeError): - abi.async_with(aw, OtherHalf(), inner, awaitcallback_err(0)) - - await aw - assert called is True - - -@limit_leaks -@pytest.mark.asyncio -async def test_async_with_no_callback(): - called = False - - @asynccontextmanager - async def my_context(): - try: - yield 1 - finally: - nonlocal called - called = True - - aw = abi.new() - abi.async_with(aw, my_context(), awaitcallback(0), awaitcallback_err(0)) - await aw - assert called is True - - -@limit_leaks -@pytest.mark.asyncio -async def test_async_with_no_callback_error(): - @asynccontextmanager - async def my_context(): - yield 1 - raise ZeroDivisionError - - aw = abi.new() - abi.async_with(aw, my_context(), awaitcallback(0), awaitcallback_err(0)) - - with pytest.raises(ZeroDivisionError): - await aw - - -@limit_leaks -@pytest.mark.asyncio -async def test_async_with_no_callback_exit_error(): - @asynccontextmanager - async def my_context(): - try: - yield 1 - finally: - raise ZeroDivisionError - - aw = abi.new() - abi.async_with(aw, my_context(), awaitcallback(0), awaitcallback_err(0)) - - with pytest.raises(ZeroDivisionError): - await aw - - -@limit_leaks -@pytest.mark.asyncio -async def test_async_with_cb_error(): - called = False - - @asynccontextmanager - async def my_context(): - try: - yield 1 - finally: - nonlocal called - called = True - - aw = abi.new() - abi.async_with(aw, my_context(), raising_callback, awaitcallback_err(0)) - await aw - assert called is True - - -@limit_leaks -@pytest.mark.asyncio -async def test_async_with_exit_error(): - @asynccontextmanager - async def my_context(): - try: - yield 1 - finally: - raise ZeroDivisionError - - called = False - - @awaitcallback_err - def cb(inner: pyawaitable.PyAwaitable, err: BaseException) -> int: - nonlocal called - called = True - assert isinstance(err, ZeroDivisionError) - return 0 - - aw = abi.new() - abi.async_with(aw, my_context(), awaitcallback(0), cb) - await aw - assert called is True - - -@limit_leaks -@pytest.mark.asyncio -async def test_async_with_enter_error(): - called = False - - @asynccontextmanager - async def my_context(): - try: - yield 1 - raise ZeroDivisionError - finally: - nonlocal called - called = True - - aw = abi.new() - abi.async_with(aw, my_context(), awaitcallback(0), awaitcallback_err(0)) - with pytest.raises(ZeroDivisionError): - await aw - assert called is True - - -@limit_leaks -@pytest.mark.asyncio -async def test_async_with_exit_error_cb(): - called = False - - @asynccontextmanager - async def my_context(): - try: - yield 1 - finally: - raise ZeroDivisionError - - @awaitcallback_err - def cb(inner: pyawaitable.PyAwaitable, err: BaseException) -> int: - assert isinstance(err, ZeroDivisionError) - nonlocal called - called = True - return 0 - - aw = abi.new() - abi.async_with(aw, my_context(), awaitcallback(0), cb) - await aw - assert called is True diff --git a/tests/util.c b/tests/util.c new file mode 100644 index 0000000..598345e --- /dev/null +++ b/tests/util.c @@ -0,0 +1,139 @@ +#include +#include + +typedef struct { + PyMemAllocatorEx raw; + PyMemAllocatorEx mem; + PyMemAllocatorEx obj; +} AllocHook; + +// TODO: Make this thread-safe for concurrent tests +AllocHook hook; + +static void * +malloc_fail(void *ctx, size_t size) +{ + return NULL; +} + +static void * +calloc_fail(void *ctx, size_t nitems, size_t size) +{ + return NULL; +} + +static void * +realloc_fail(void *ctx, void *ptr, size_t newsize) +{ + return NULL; +} + +static void +wrapped_free(void *ctx, void *ptr) +{ + PyMemAllocatorEx *alloc = (PyMemAllocatorEx *)ctx; + alloc->free(alloc->ctx, ptr); +} + +void +Test_SetNoMemory(void) +{ + PyMemAllocatorEx alloc; + alloc.malloc = malloc_fail; + alloc.calloc = calloc_fail; + alloc.realloc = realloc_fail; + alloc.free = wrapped_free; + + PyMem_GetAllocator(PYMEM_DOMAIN_RAW, &hook.raw); + PyMem_GetAllocator(PYMEM_DOMAIN_MEM, &hook.mem); + PyMem_GetAllocator(PYMEM_DOMAIN_OBJ, &hook.obj); + + alloc.ctx = &hook.raw; + PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc); + + alloc.ctx = &hook.raw; + PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc); + + alloc.ctx = &hook.raw; + PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &alloc); +} + +void +Test_UnSetNoMemory(void) +{ + PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &hook.raw); + PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &hook.mem); + PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &hook.obj); +} + +PyObject * +Test_RunAwaitable(PyObject *awaitable) +{ + PyObject *asyncio = PyImport_ImportModule("asyncio"); + if (asyncio == NULL) { + return NULL; + } + + PyObject *loop = PyObject_CallMethod(asyncio, "new_event_loop", ""); + Py_DECREF(asyncio); + if (loop == NULL) { + return NULL; + } + + PyObject *res = PyObject_CallMethod( + loop, + "run_until_complete", + "O", + awaitable + ); + // Temporarily remove the error so we can close the loop + PyObject *err = PyErr_GetRaisedException(); + PyObject *close_res = PyObject_CallMethod(loop, "close", ""); + Py_DECREF(loop); + + if (res == NULL) { + assert(err != NULL); + PyErr_SetRaisedException(err); + return NULL; + } + + if (close_res == NULL) { + Py_DECREF(res); + return NULL; + } + + Py_DECREF(close_res); + return res; +} + +PyObject * +_Test_RunAndCheck( + PyObject *awaitable, + PyObject *expected, + const char *func, + const char *file, + int line +) +{ + PyObject *res = Test_RunAwaitable(awaitable); + if (res == NULL) { + Py_DECREF(awaitable); + return NULL; + } + Py_DECREF(awaitable); + if (res != expected) { + PyErr_Format( + PyExc_AssertionError, + "test %s at %s:%d expected awaitable to return %R, got %R", + func, + file, + line, + expected, + res + ); + Py_DECREF(res); + return NULL; + } + Py_DECREF(res); + Py_RETURN_NONE; +} diff --git a/uncrustify.cfg b/uncrustify.cfg index 6331597..6fe5f9a 100644 --- a/uncrustify.cfg +++ b/uncrustify.cfg @@ -10,7 +10,7 @@ indent_with_tabs = 0 # Ugly Newlines nl_struct_brace = remove nl_else_brace = remove -nl_brace_else = remove +nl_brace_else = force nl_if_brace = remove nl_else_if = remove nl_for_brace = remove @@ -59,7 +59,6 @@ sp_paren_comma = remove # Braces sp_inside_braces_empty = remove sp_sparen_brace = force -nl_before_brace_open = true nl_assign_brace = remove sp_brace_else = force sp_else_brace = force diff --git a/vendor.py b/vendor.py deleted file mode 100644 index 76b75ad..0000000 --- a/vendor.py +++ /dev/null @@ -1,79 +0,0 @@ -import os -import shutil -from pathlib import Path -from typing import TextIO - -from pyawaitable import __version__ - - -def _write(fp: TextIO, value: str) -> None: - fp.write(value) - print(f"Wrote {len(value.encode('utf-8'))} bytes to {fp.name}") - - -def _header_file(text: str) -> str: - lines = text.replace("_impl", "").split("\n") - - # Remove header guards and trailing newlines - for i in (0, 0, -1, -1): - lines.pop(i) - - return "\n".join([i for i in lines if not i.startswith("#include")]) - - -def _source_file(fp: TextIO, file: Path) -> None: - text: str = file.read_text(encoding="utf-8").replace("_impl", "") - lines = text.split("\n") - lines.insert(0, f"/* Vendor of {file} */") - _write(fp, "\n".join([i for i in lines if not i.startswith("#include")])) - - -def main(): - dist = Path("./pyawaitable-vendor") - if dist.exists(): - print("./pyawaitable-vendor already exists, removing it...") - shutil.rmtree(dist) - print("Creating vendored copy of pyawaitable in ./pyawaitable-vendor...") - os.mkdir(dist) - - with open(dist / "pyawaitable.h", "w", encoding="utf-8") as f: - _write( - f, - """#ifndef PYAWAITABLE_VENDOR_H -#define PYAWAITABLE_VENDOR_H - -#include -#include -#include -""", - ) - for path in [i for i in Path("./include/pyawaitable").iterdir()] + [ - Path("./include/vendor.h") - ]: - _write(f, _header_file(path.read_text(encoding="utf-8"))) - - _write(f, "\n#endif") - - with open(dist / "pyawaitable.c", "w", encoding="utf-8") as f: - f.write( - f"""/* - * PyAwaitable - Vendored copy of version {__version__} - * - * Docs: https://awaitable.zintensity.dev - * Source: https://github.com/ZeroIntensity/pyawaitable - */ - -#include "pyawaitable.h" - -PyTypeObject _PyAwaitableGenWrapperType; // Forward declaration -""" - ) - for source_file in Path("./src/_pyawaitable/").iterdir(): - if source_file.name == "mod.c": - continue - - _source_file(f, source_file) - - -if __name__ == "__main__": - main()