diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index 42a5c7ac..c2d50f68 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0"] + python-version: ["3.10", "3.11", "3.12", "3.13.0", "3.14"] fail-fast: false steps: diff --git a/.github/workflows/python-build-test.yml b/.github/workflows/python-build-test.yml index 54a1e0c9..f433a06a 100644 --- a/.github/workflows/python-build-test.yml +++ b/.github/workflows/python-build-test.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] fail-fast: false steps: @@ -48,7 +48,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + # Rust runtime doesn't support 3.14 yet. + python-version: ["3.10", "3.11", "3.12", "3.13"] rust-version: [ "1.80.0", "stable" ] fail-fast: false steps: diff --git a/apps/auracast.py b/apps/auracast.py index 1e7413c6..70e290bb 100644 --- a/apps/auracast.py +++ b/apps/auracast.py @@ -742,10 +742,9 @@ async def run_receive( ] packet_stats = [0, 0] - audio_output = await audio_io.create_audio_output(output) - # This try should be replaced with contextlib.aclosing() when python 3.9 is no - # longer needed. - try: + async with contextlib.AsyncExitStack() as stack: + audio_output = await audio_io.create_audio_output(output) + stack.push_async_callback(audio_output.aclose) await audio_output.open( audio_io.PcmFormat( audio_io.PcmFormat.Endianness.LITTLE, @@ -793,8 +792,6 @@ async def run_receive( terminated = asyncio.Event() big_sync.on(big_sync.Event.TERMINATION, lambda _: terminated.set()) await terminated.wait() - finally: - await audio_output.aclose() async def run_transmit( @@ -891,11 +888,10 @@ async def run_transmit( print('Start Periodic Advertising') await advertising_set.start_periodic() - audio_input = await audio_io.create_audio_input(input, input_format) - pcm_format = await audio_input.open() - # This try should be replaced with contextlib.aclosing() when python 3.9 is no - # longer needed. - try: + async with contextlib.AsyncExitStack() as stack: + audio_input = await audio_io.create_audio_input(input, input_format) + pcm_format = await audio_input.open() + stack.push_async_callback(audio_input.aclose) if pcm_format.channels != 2: print("Only 2 channels PCM configurations are supported") return @@ -967,8 +963,6 @@ async def run_transmit( await iso_queues[1].write(lc3_frame[mid:]) frame_count += 1 - finally: - await audio_input.aclose() def run_async(async_command: Coroutine) -> None: diff --git a/bumble/audio/io.py b/bumble/audio/io.py index 37189305..98bc1a0f 100644 --- a/bumble/audio/io.py +++ b/bumble/audio/io.py @@ -19,13 +19,13 @@ from __future__ import annotations import abc import asyncio +import concurrent.futures import dataclasses import enum import logging import pathlib import sys import wave -from concurrent.futures import ThreadPoolExecutor from typing import TYPE_CHECKING, AsyncGenerator, BinaryIO from bumble.colors import color @@ -176,7 +176,7 @@ class ThreadedAudioOutput(AudioOutput): """ def __init__(self) -> None: - self._thread_pool = ThreadPoolExecutor(1) + self._thread_pool = concurrent.futures.ThreadPoolExecutor(1) self._pcm_samples: asyncio.Queue[bytes] = asyncio.Queue() self._write_task = asyncio.create_task(self._write_loop()) @@ -405,7 +405,7 @@ class ThreadedAudioInput(AudioInput): """Base class for AudioInput implementation where reading samples may block.""" def __init__(self) -> None: - self._thread_pool = ThreadPoolExecutor(1) + self._thread_pool = concurrent.futures.ThreadPoolExecutor(1) self._pcm_samples: asyncio.Queue[bytes] = asyncio.Queue() @abc.abstractmethod diff --git a/bumble/device.py b/bumble/device.py index 75765d60..1f63ba2a 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -2370,11 +2370,7 @@ class Device(utils.CompositeEventEmitter): hci.Address.ANY: [] } # Futures, by BD address OR [Futures] for hci.Address.ANY - # In Python <= 3.9 + Rust Runtime, asyncio.Lock cannot be properly initiated. - if sys.version_info >= (3, 10): - self._cis_lock = asyncio.Lock() - else: - self._cis_lock = AsyncExitStack() + self._cis_lock = asyncio.Lock() # Own address type cache self.connect_own_address_type = None diff --git a/bumble/utils.py b/bumble/utils.py index 4b9f0748..fcb7429b 100644 --- a/bumble/utils.py +++ b/bumble/utils.py @@ -241,11 +241,7 @@ def cancel_on_event( return msg = f'abort: {event} event occurred.' if isinstance(future, asyncio.Task): - # python < 3.9 does not support passing a message on `Task.cancel` - if sys.version_info < (3, 9, 0): - future.cancel() - else: - future.cancel(msg) + future.cancel(msg) else: future.set_exception(asyncio.CancelledError(msg)) diff --git a/docs/mkdocs/src/getting_started.md b/docs/mkdocs/src/getting_started.md index 9570849f..960e37f7 100644 --- a/docs/mkdocs/src/getting_started.md +++ b/docs/mkdocs/src/getting_started.md @@ -3,7 +3,7 @@ GETTING STARTED WITH BUMBLE # Prerequisites -You need Python 3.9 or above. +You need Python 3.10 or above. Visit the [Python site](https://www.python.org/) for instructions on how to install Python for your platform. Throughout the documentation, when shell commands are shown, it is assumed that you can diff --git a/docs/mkdocs/src/index.md b/docs/mkdocs/src/index.md index 12a3cf3b..a5b69292 100644 --- a/docs/mkdocs/src/index.md +++ b/docs/mkdocs/src/index.md @@ -31,7 +31,7 @@ Some of the configurations that may be useful: See the [use cases page](use_cases/index.md) for more use cases. -The project is implemented in Python (Python >= 3.9 is required). A number of APIs for functionality that is inherently I/O bound is implemented in terms of python coroutines with async IO. This means that all of the concurrent tasks run in the same thread, which makes everything much simpler and more predictable. +The project is implemented in Python (Python >= 3.10 is required). A number of APIs for functionality that is inherently I/O bound is implemented in terms of python coroutines with async IO. This means that all of the concurrent tasks run in the same thread, which makes everything much simpler and more predictable. ![layers](images/bumble_layers.svg) diff --git a/docs/mkdocs/src/platforms/index.md b/docs/mkdocs/src/platforms/index.md index 9bf7c29e..d07aaa96 100644 --- a/docs/mkdocs/src/platforms/index.md +++ b/docs/mkdocs/src/platforms/index.md @@ -1,7 +1,7 @@ PLATFORMS ========= -Most of the code included in the project should run on any platform that supports Python >= 3.9. Not all features are supported on all platforms (for example, USB dongle support is only available on platforms where the python USB library is functional). +Most of the code included in the project should run on any platform that supports Python >= 3.10. Not all features are supported on all platforms (for example, USB dongle support is only available on platforms where the python USB library is functional). For platform-specific information, see the following pages: diff --git a/environment.yml b/environment.yml deleted file mode 100644 index bb657221..00000000 --- a/environment.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: bumble -channels: - - defaults - - conda-forge -dependencies: - - pip=23 - - python=3.9 - - pip: - - --editable .[development,documentation,test] diff --git a/pyproject.toml b/pyproject.toml index 37f450cb..0ef717ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" license = "Apache-2.0" license-files = ["LICENSE"] authors = [{ name = "Google", email = "bumble-dev@google.com" }] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "aiohttp ~= 3.8; platform_system!='Emscripten'", "appdirs >= 1.4; platform_system!='Emscripten'",