diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37b23da..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 @@ -83,7 +85,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)?)?;