From 63d4012bb91175de94f069acf0aad34c3b48ef9b Mon Sep 17 00:00:00 2001 From: Mika Uthmann Date: Wed, 18 Mar 2026 11:08:13 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat(python):=20=E2=9C=A8=20support=20CPyth?= =?UTF-8?q?on=203.9-3.13=20with=20per-version=20wheels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop EOL Python 3.8, add Python 3.13 support. Upgrade to PyO3 0.23 and numpy crate 0.23 with Bound API migration. Release workflow now builds wheels for every supported interpreter on each OS. --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 15 ++++---- README.md | 4 ++- ...python-version-support-and-wheel-builds.md | 31 +++++++++++++++++ docs/adr/README.md | 1 + docs/features/README.md | 1 + docs/features/python-version-support.md | 34 +++++++++++++++++++ evt3-python/Cargo.toml | 4 +-- evt3-python/README.md | 4 ++- evt3-python/pyproject.toml | 4 +-- evt3-python/src/lib.rs | 26 +++++++------- 11 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 docs/adr/0002-python-version-support-and-wheel-builds.md create mode 100644 docs/features/python-version-support.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37b23da..9ad80d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2158799..c8e1eea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,11 +68,12 @@ jobs: path: ${{ matrix.name }} build-wheels: - name: Build Python wheels - ${{ matrix.os }} + name: Build Python wheels - ${{ matrix.os }} - py${{ matrix.python-version }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -82,19 +83,19 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' - - - name: Install maturin - run: uv tool install maturin + python-version: ${{ matrix.python-version }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable - name: Build wheels working-directory: evt3-python - run: maturin build --release --out dist + run: uv tool run maturin build --release --out dist --interpreter python - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-${{ matrix.os }} + name: wheels-${{ matrix.os }}-py${{ matrix.python-version }} path: evt3-python/dist/*.whl release: diff --git a/README.md b/README.md index ead52e9..f4f7fc8 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ Or with pip: pip install evt3 ``` +Published wheels target CPython 3.9 through 3.13. + > **Note:** The pip package supports `.raw` files only. HDF5 (`.h5`/`.hdf5`) > requires building from source — see [HDF5 Inputs](#hdf5-inputs) below. @@ -87,7 +89,7 @@ HDF5_DIR="$(brew --prefix hdf5)" cargo build --release -p evt3-cli --features hd # Optional: Install to PATH cp target/release/evt3 ~/.local/bin/ -# Build Python package (requires uv + Rust) +# Build Python package (requires Python 3.9+, uv + Rust) cd evt3-python uv venv uv pip install maturin diff --git a/docs/adr/0002-python-version-support-and-wheel-builds.md b/docs/adr/0002-python-version-support-and-wheel-builds.md new file mode 100644 index 0000000..5fbeadd --- /dev/null +++ b/docs/adr/0002-python-version-support-and-wheel-builds.md @@ -0,0 +1,31 @@ +# 0002: Support CPython 3.9-3.13 With Per-Interpreter Wheels + +## Status + +Accepted + +## Context + +The package metadata still advertised Python 3.8 support even though Python 3.8 +reached end of life in October 2024. At the same time, the release workflow +only built Python wheels with a single Python 3.11 interpreter on each OS, so +PyPI users on other supported versions had to build from source. Adding Python +3.13 support also requires newer PyO3 and `numpy` crate releases than the +project was using. + +## Decision + +- Set the supported Python range to CPython 3.9 through 3.13. +- Upgrade the Python bindings to `pyo3 = 0.23` and `numpy = 0.23`. +- Migrate the bindings to the current PyO3 Bound API required by that upgrade. +- Run Python CI coverage on 3.9, 3.10, 3.11, 3.12, and 3.13. +- Build release wheels per operating system and interpreter with + `maturin build --interpreter python`. + +## Consequences + +- Python 3.8 users must stay on an older release or build from an older branch. +- Release CI now runs more wheel jobs because each OS builds one wheel per + supported interpreter. +- PyPI users on supported CPython versions get prebuilt wheels instead of a + single 3.11-only release artifact set. diff --git a/docs/adr/README.md b/docs/adr/README.md index bab6546..ad928eb 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -1,3 +1,4 @@ # ADR Index - [0001: HDF5 File Support Through `decode_file`](./0001-hdf5-file-support.md) +- [0002: Support CPython 3.9-3.13 With Per-Interpreter Wheels](./0002-python-version-support-and-wheel-builds.md) diff --git a/docs/features/README.md b/docs/features/README.md index 8e2ff79..2b7cd10 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -2,3 +2,4 @@ - [Byte-Stream Decoding](./byte-stream-decoding.md): Incremental decoding of raw EVT3 byte streams via `Evt3Decoder::decode_bytes` and `finish_stream`. - [HDF5 File Support](./hdf5-file-support.md): Optional `.h5`/`.hdf5` decoding through the existing `Evt3Decoder::decode_file` API. +- [Python Version Support And Wheel Builds](./python-version-support.md): CPython 3.9-3.13 support with per-interpreter wheels for release builds. diff --git a/docs/features/python-version-support.md b/docs/features/python-version-support.md new file mode 100644 index 0000000..5a8c4b4 --- /dev/null +++ b/docs/features/python-version-support.md @@ -0,0 +1,34 @@ +# Python Version Support And Wheel Builds + +## Summary + +The Python package now targets CPython 3.9 through 3.13. The bindings were +updated to PyO3 and `numpy` crate releases that support Python 3.13, and the +release workflow now builds wheels for every supported interpreter on Linux, +macOS, and Windows instead of only producing Python 3.11 wheels. + +## User Impact + +- `pip install evt3` now has published wheel coverage for CPython 3.9, 3.10, + 3.11, 3.12, and 3.13. +- Python 3.8 is no longer supported; package metadata now requires Python 3.9+. +- Local source builds continue to use `maturin develop` / `maturin build` and + still require Rust. +- HDF5 support remains source-only because it depends on the optional native + HDF5 toolchain and runtime plugin setup. + +## Implementation Notes + +- `evt3-python/Cargo.toml` now uses `pyo3 = 0.23` and `numpy = 0.23`. +- `evt3-python/src/lib.rs` was migrated to the current PyO3 Bound API used by + those releases. +- `.github/workflows/ci.yml` runs the Python synthetic test suite on Python 3.9 + through 3.13. +- `.github/workflows/release.yml` builds one wheel per supported interpreter and + operating system with `maturin build --interpreter python`. + +## Verification + +- `cargo check -p evt3-python` +- `cargo fmt --all` +- Python build and test verification commands are recorded in `tasks/todo.md` diff --git a/evt3-python/Cargo.toml b/evt3-python/Cargo.toml index 3a83f83..52262f7 100644 --- a/evt3-python/Cargo.toml +++ b/evt3-python/Cargo.toml @@ -17,5 +17,5 @@ hdf5 = ["evt3-core/hdf5"] [dependencies] evt3-core = { path = "../evt3-core" } -pyo3 = { version = "0.20", features = ["extension-module"] } -numpy = "0.20" +pyo3 = { version = "0.23", features = ["extension-module"] } +numpy = "0.23" diff --git a/evt3-python/README.md b/evt3-python/README.md index b1a16d6..d15bb94 100644 --- a/evt3-python/README.md +++ b/evt3-python/README.md @@ -4,6 +4,8 @@ High-performance EVT 3.0 decoder for Prophesee event cameras with zero-copy nump ## Installation +Supported Python versions: CPython 3.9 through 3.13. + ```bash # From source (requires Rust toolchain) cd evt3-python @@ -11,7 +13,7 @@ pip install maturin maturin develop # Or build a wheel -maturin build --release +maturin build --release --interpreter python pip install target/wheels/evt3-*.whl ``` diff --git a/evt3-python/pyproject.toml b/evt3-python/pyproject.toml index a32e8a9..f80a44d 100644 --- a/evt3-python/pyproject.toml +++ b/evt3-python/pyproject.toml @@ -9,7 +9,7 @@ description = "High-performance EVT 3.0 decoder for Prophesee event cameras" readme = "README.md" license = { text = "MIT" } authors = [{ name = "Uthmann", email = "mika.uthmann@uni-bielefeld.de" }] -requires-python = ">=3.8" +requires-python = ">=3.9" keywords = ["event-camera", "evt3", "prophesee", "decoder", "neuromorphic", "dvs"] classifiers = [ "Development Status :: 4 - Beta", @@ -18,11 +18,11 @@ classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Image Processing", "Topic :: Multimedia :: Video", diff --git a/evt3-python/src/lib.rs b/evt3-python/src/lib.rs index 7ea460c..7963d23 100644 --- a/evt3-python/src/lib.rs +++ b/evt3-python/src/lib.rs @@ -7,7 +7,7 @@ use evt3_core::{CdEvent, Evt3Decoder, TriggerEvent}; use numpy::{IntoPyArray, PyArray1}; use pyo3::exceptions::PyIOError; use pyo3::prelude::*; -use pyo3::types::PyDict; +use pyo3::types::{PyDict, PyModule}; use std::path::PathBuf; /// Container for decoded CD events with zero-copy numpy access. @@ -52,13 +52,13 @@ impl Events { /// This creates a view into the Rust-allocated memory without copying. /// The array is valid as long as this Events object is alive. #[getter] - fn x<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn x<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.x.clone().into_pyarray(py) } /// Returns the Y coordinates as a numpy array. #[getter] - fn y<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn y<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.y.clone().into_pyarray(py) } @@ -66,25 +66,25 @@ impl Events { /// /// Values: 0 = OFF (decrease in brightness), 1 = ON (increase) #[getter] - fn polarity<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn polarity<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.polarity.clone().into_pyarray(py) } /// Alias for polarity (shorter name). #[getter] - fn p<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn p<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.polarity.clone().into_pyarray(py) } /// Returns the timestamps as a numpy array (in microseconds). #[getter] - fn timestamp<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn timestamp<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.timestamp.clone().into_pyarray(py) } /// Alias for timestamp (shorter name). #[getter] - fn t<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn t<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.timestamp.clone().into_pyarray(py) } @@ -109,13 +109,13 @@ impl Events { /// Returns all arrays as a dictionary. /// /// This is useful for creating a pandas DataFrame or structured array. - fn to_dict<'py>(&self, py: Python<'py>) -> PyResult { + fn to_dict<'py>(&self, py: Python<'py>) -> PyResult> { let dict = PyDict::new(py); dict.set_item("x", self.x.clone().into_pyarray(py))?; dict.set_item("y", self.y.clone().into_pyarray(py))?; dict.set_item("polarity", self.polarity.clone().into_pyarray(py))?; dict.set_item("timestamp", self.timestamp.clone().into_pyarray(py))?; - Ok(dict.into()) + Ok(dict.unbind()) } } @@ -166,19 +166,19 @@ impl TriggerEvents { /// Returns the trigger values as a numpy array. #[getter] - fn value<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn value<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.value.clone().into_pyarray(py) } /// Returns the trigger channel IDs as a numpy array. #[getter] - fn id<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn id<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.id.clone().into_pyarray(py) } /// Returns the timestamps as a numpy array. #[getter] - fn timestamp<'py>(&self, py: Python<'py>) -> &'py PyArray1 { + fn timestamp<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { self.timestamp.clone().into_pyarray(py) } } @@ -319,7 +319,7 @@ fn decode_bytes( /// EVT 3.0 decoder module for Python. #[pymodule] -fn _evt3(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn _evt3(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(decode_file, m)?)?; m.add_function(wrap_pyfunction!(decode_file_with_triggers, m)?)?; m.add_function(wrap_pyfunction!(decode_bytes, m)?)?; From af352ebc8776ed823810b9a2e0065e11b9e4abeb Mon Sep 17 00:00:00 2001 From: Mika Uthmann Date: Wed, 18 Mar 2026 11:12:29 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix(ci):=20=F0=9F=94=A7=20add=20apt-get=20u?= =?UTF-8?q?pdate=20before=20HDF5=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub runner package index can be stale, causing 404 errors when fetching transitive dependencies like libcurl4-openssl-dev. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ad80d7..e1b081c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,9 @@ jobs: key: ubuntu-cargo-hdf5-${{ hashFiles('**/Cargo.lock') }} - name: Install HDF5 - run: sudo apt-get install -y libhdf5-dev + run: | + sudo apt-get update + sudo apt-get install -y libhdf5-dev - name: Clippy (hdf5 feature) run: cargo clippy -p evt3-core -p evt3-cli --features hdf5 -- -D warnings