diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index c206db9..7be0419 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -21,10 +21,11 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest, macos-15-intel] arch: [native] - python: ["{cp310,pp311}"] + python: ["{cp310,cp314t,cp315t,pp311}"] # Split aarch64 across jobs because it uses emulation (slow) include: # DNS fails inside i686 containers on ubuntu-26.04 runners + # cffi has no free-threaded i686 wheels - os: ubuntu-24.04 arch: i686 python: "{cp310,pp311}" @@ -34,6 +35,9 @@ jobs: - os: ubuntu-latest arch: aarch64 python: "pp311" + - os: ubuntu-latest + arch: aarch64 + python: "{cp314t,cp315t}" steps: - uses: actions/checkout@v7 with: diff --git a/NEWS.rst b/NEWS.rst index 4f957ca..b0da1d2 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -4,6 +4,12 @@ Version 0.1.8 -- unreleased * Fix possible crashes when a stream is garbage-collected while it is running, or when an action is garbage-collected after being cancelled * Update PortAudio submodule to the current development version + * Add wheels for free-threaded CPython 3.14t, and limited API wheels + (``abi3.abi3t``) for CPython 3.15 and 3.15t and newer + * Fix building from source on free-threaded CPython, which produced a + wheel wrongly tagged ``abi3`` that could not be installed + * Fix possible crashes when actions are started or waited for from + several threads at the same time Version 0.1.7 -- 2024-07-02 -- PyPI__ -- diff__ * Restore the ``x86_64`` Linux wheels that were missing from 0.1.6 diff --git a/doc/api.rst b/doc/api.rst index 202fd38..c29bd9a 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -36,6 +36,13 @@ Common parameters that are shared by most commands: All commands return a corresponding "action", which can be compared against the active `actions`, and can be used as input for `cancel()` and `wait()`. +The commands, `actions` and `wait()` can be used from several threads at +once. The methods inherited from `sounddevice.Stream` (e.g. +:meth:`~sounddevice.Stream.start` and :meth:`~sounddevice.Stream.close`) +cannot, so they must be called from one thread or synchronized by the user, +as required by PortAudio. Likewise, a `RingBuffer` that is used for playback +(or recording) must only be written to (or read from) by one thread at a time. + The fields of action objects are defined in C but can be accessed with Python (e.g. ``my_action.stats.min_blocksize``) *after* the command is finished: diff --git a/setup.py b/setup.py index 990cb5a..80eaff4 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ -from setuptools import setup, Extension -from wheel.bdist_wheel import bdist_wheel +import sys +import sysconfig + +from setuptools import setup __version__ = 'unknown' @@ -12,16 +14,15 @@ # Adapted from # https://github.com/joerick/python-abi3-package-sample/blob/main/setup.py -class bdist_wheel_abi3(bdist_wheel): - def get_tag(self): - python, abi, plat = super().get_tag() - - if python.startswith("cp"): - # on CPython, our wheels are abi3 and compatible back to 3.2, - # but let's set it to our min version anyway - return "cp310", "abi3", plat - - return python, abi, plat +# On CPython our wheels are abi3. Free-threaded builds support the limited API +# (as abi3t, PEP 803) only from 3.15 on, so 3.14t gets a version-specific +# wheel. +options = {} +if sys.implementation.name == 'cpython': + if not sysconfig.get_config_var('Py_GIL_DISABLED'): + options['bdist_wheel'] = {'py_limited_api': 'cp310'} + elif sys.version_info >= (3, 15): + options['bdist_wheel'] = {'py_limited_api': 'cp315'} setup( @@ -29,5 +30,5 @@ def get_tag(self): package_dir={'': 'src'}, py_modules=['rtmixer'], cffi_modules=['rtmixer_build.py:ffibuilder'], # sets Py_LIMITED_API for us - cmdclass={"bdist_wheel": bdist_wheel_abi3}, + options=options, ) diff --git a/src/rtmixer.py b/src/rtmixer.py index d481614..ba9dfaf 100644 --- a/src/rtmixer.py +++ b/src/rtmixer.py @@ -5,6 +5,8 @@ """ __version__ = '0.1.7' +import threading as _threading + import sounddevice as _sd from pa_ringbuffer import init as _init_ringbuffer from _rtmixer import ffi as _ffi, lib as _lib @@ -40,6 +42,8 @@ def __init__(self, kind, qsize=16, **kwargs): self._actions = {} self._temp_action_ptr = _ffi.new('struct action**') + # The queues allow only one reader and one writer at a time: + self._lock = _threading.Lock() def __del__(self): # The C callback uses memory owned by this object, so the stream @@ -53,8 +57,9 @@ def __del__(self): @property def actions(self): """The set of active "actions".""" - self._drain_result_q() - return self._actions.keys() + with self._lock: + self._drain_result_q() + return set(self._actions) @property def stats(self): @@ -135,17 +140,22 @@ def _check_channels(self, channels, kind): return channels, mapping def _enqueue(self, action, keep_alive=None): - self._drain_result_q() - self._temp_action_ptr[0] = action - ret = self._action_q.write(self._temp_action_ptr) - if ret != 1: - raise RuntimeError( - 'Action queue is full, consider increasing qsize') - assert action not in self._actions - self._actions[action] = keep_alive + with self._lock: + self._drain_result_q() + self._temp_action_ptr[0] = action + ret = self._action_q.write(self._temp_action_ptr) + if ret != 1: + raise RuntimeError( + 'Action queue is full, consider increasing qsize') + assert action not in self._actions + self._actions[action] = keep_alive def _drain_result_q(self): - """Get actions from the result queue and discard them.""" + """Get actions from the result queue and discard them. + + Must be called with ``self._lock`` held. + + """ while self._result_q.readinto(self._temp_action_ptr): # Each item is a linked list of actions action = self._temp_action_ptr[0] diff --git a/tools/cibw_repair_wheel_command.sh b/tools/cibw_repair_wheel_command.sh index fb26641..8896759 100755 --- a/tools/cibw_repair_wheel_command.sh +++ b/tools/cibw_repair_wheel_command.sh @@ -12,7 +12,3 @@ elif [[ "$RUNNER_OS" == "macOS" ]]; then else cp $WHEEL $DEST_DIR fi - -if [[ "$(python -c 'import platform; print(platform.python_implementation())')" == "CPython" ]]; then - pipx run abi3audit --strict --report $WHEEL -fi