From ad7645f027d27705f798f22766b90eb9a817c70a Mon Sep 17 00:00:00 2001 From: zydo Date: Mon, 12 Oct 2026 02:55:13 -0700 Subject: [PATCH 1/6] ci: stop running setup.py inside conda-build load_setup_py_data() executes setup.py in the conda-build process. setup.py prepends "." to sys.path, so the source tree's unbuilt zstandard package later shadows the zstandard bindings that conda-package-handling imports for .conda support. conda-build then fails with "Didn't recognize extension for file '...conda'". Read the version from c-ext/python-zstandard.h with load_file_regex() instead. --- ci/conda/meta.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ci/conda/meta.yaml b/ci/conda/meta.yaml index cbea594a..2311ac3b 100644 --- a/ci/conda/meta.yaml +++ b/ci/conda/meta.yaml @@ -1,8 +1,12 @@ -{% set data = load_setup_py_data() %} +# Don't use load_setup_py_data(): it runs setup.py inside the conda-build +# process, and setup.py's sys.path.insert(0, ".") then makes the unbuilt +# zstandard package in the source tree shadow the zstandard bindings that +# conda-package-handling needs for .conda support. +{% set version_match = load_file_regex(load_file="../../c-ext/python-zstandard.h", regex_pattern='PYTHON_ZSTANDARD_VERSION "([^"]+)"', from_recipe_dir=True) %} package: name: zstandard - version: "{{ data.get('version') }}" + version: "{{ version_match.group(1) }}" source: path: ../../ From 914af5fde730bd1c9cc138d96901be09d7b43935 Mon Sep 17 00:00:00 2001 From: zydo Date: Mon, 12 Oct 2026 02:55:26 -0700 Subject: [PATCH 2/6] ci: install only test dependencies when testing wheels cibuildwheel installs the test-groups packages unlocked, so it picked up the latest mypy, which now depends on ast-serialize. ast-serialize has no wheels for musllinux_1_1 or musllinux s390x, and building it from source needs a Rust toolchain the containers don't have, failing those wheel jobs before any test ran. Move the test suite's dependencies into a new "test" dependency group, have "dev" include it plus mypy, and point cibuildwheel at "test". --- pyproject.toml | 12 +++++++++--- uv.lock | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b358a7f5..cbd2d07b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,14 +42,20 @@ requires = [ build-backend = "setuptools.build_meta" [dependency-groups] -dev = [ +# Dependencies needed to run the test suite. cibuildwheel installs this group +# unlocked inside its build containers, so keep it free of packages (like mypy) +# without wheels for every platform we build. +test = [ "cffi~=1.17; platform_python_implementation != 'PyPy' and python_version < '3.14'", "cffi>=2.0.0b; platform_python_implementation != 'PyPy' and python_version >= '3.14'", "hypothesis==6.111.0", - "mypy>=1.17.1", "pytest>=8.4.1", "pytest-xdist>=3.8.0", ] +dev = [ + { include-group = "test" }, + "mypy>=1.17.1", +] [tool.pytest.ini_options] testpaths = ["tests"] @@ -71,7 +77,7 @@ test-command = [ "pytest -v --numprocesses=auto", "PYTHON_ZSTANDARD_IMPORT_POLICY=cffi pytest -v --numprocesses=auto", ] -test-groups = ["dev"] +test-groups = ["test"] test-sources = ["tests"] [tool.cibuildwheel.linux] diff --git a/uv.lock b/uv.lock index 89fdd450..717aa37f 100644 --- a/uv.lock +++ b/uv.lock @@ -420,6 +420,13 @@ dev = [ { name = "pytest" }, { name = "pytest-xdist" }, ] +test = [ + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' and platform_python_implementation != 'PyPy'" }, + { name = "cffi", version = "2.0.0b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'" }, + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-xdist" }, +] [package.metadata] requires-dist = [ @@ -437,3 +444,10 @@ dev = [ { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, ] +test = [ + { name = "cffi", marker = "python_full_version < '3.14' and platform_python_implementation != 'PyPy'", specifier = "~=1.17" }, + { name = "cffi", marker = "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'", specifier = ">=2.0.0b0" }, + { name = "hypothesis", specifier = "==6.111.0" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, +] From a5adf085271435205c0a717c6b1995aab6ea9576 Mon Sep 17 00:00:00 2001 From: zydo Date: Mon, 12 Oct 2026 02:55:49 -0700 Subject: [PATCH 3/6] ci: find Homebrew zstd in the macOS external zstd job On Apple Silicon runners Homebrew installs into /opt/homebrew, which isn't on the compiler's default search paths, so the --system-zstd build failed with "'zdict.h' file not found". Pass Homebrew's include and lib directories explicitly, and build for arm64 only since Homebrew's libzstd is not universal2. --- .github/workflows/external-zstd.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/external-zstd.yml b/.github/workflows/external-zstd.yml index 2e0f3b69..42bf25e6 100644 --- a/.github/workflows/external-zstd.yml +++ b/.github/workflows/external-zstd.yml @@ -61,4 +61,11 @@ jobs: - name: Build run: | + # Homebrew's prefix (/opt/homebrew on Apple Silicon) isn't on the + # default compiler search paths, and its libzstd is arm64 only, so + # don't build the universal2 binary the Python framework asks for. + ZSTD_PREFIX="$(brew --prefix zstd)" + export CFLAGS="-I${ZSTD_PREFIX}/include" + export LDFLAGS="-L${ZSTD_PREFIX}/lib" + export ARCHFLAGS="-arch arm64" python -m pip install --config-settings=--global-option=--system-zstd . From 1f2111ca62a123e31040febf334ba86bb06cf668 Mon Sep 17 00:00:00 2001 From: zydo Date: Mon, 12 Oct 2026 02:55:53 -0700 Subject: [PATCH 4/6] tests: disable hypothesis deadlines in the ci profile test_stream_source_readinto intermittently fails on macos-26-intel runners with DeadlineExceeded: a single example took 31s against the 20s deadline, then 18s on hypothesis's rerun, which hypothesis reports as a FlakyFailure. The timing depends on runner load, not on the code under test, so don't enforce a deadline in the ci profile, as the expensive profile already does. --- tests/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 84e71215..dcba0693 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,9 @@ def pytest_configure(config): default_settings = hypothesis.settings(deadline=10000) hypothesis.settings.register_profile("default", default_settings) - ci_settings = hypothesis.settings(deadline=20000, max_examples=1000) + # Shared CI runners (notably the Intel macOS ones) are slow and noisy + # enough that per-example deadlines only produce flaky failures. + ci_settings = hypothesis.settings(deadline=None, max_examples=1000) hypothesis.settings.register_profile("ci", ci_settings) expensive_settings = hypothesis.settings(deadline=None, max_examples=10000) From 3a3e4fd0705d689b7c632e9c929697a081f13243 Mon Sep 17 00:00:00 2001 From: zydo Date: Mon, 12 Oct 2026 02:56:24 -0700 Subject: [PATCH 5/6] rust: upgrade PyO3 from 0.25 to 0.29 PyO3 0.29 is the first release that supports Python 3.15. Port the Rust backend to the APIs renamed or removed since 0.25: PyObject becomes Py, downcast*() becomes cast*(), allow_threads() becomes detach() and assume_gil_acquired() becomes assume_attached(). Py::from_borrowed_ptr() is deprecated, so build the parent reference in ZstdBufferWithSegments.segments() with a safe conversion instead of a raw pointer cast. --- Cargo.lock | 52 ++++++--------------------- Cargo.toml | 2 +- docs/news.rst | 5 +++ rust-ext/src/buffers.rs | 37 ++++++++++--------- rust-ext/src/compression_chunker.rs | 4 +-- rust-ext/src/compression_dict.rs | 4 +-- rust-ext/src/compression_reader.rs | 6 ++-- rust-ext/src/compression_writer.rs | 4 +-- rust-ext/src/compressionobj.rs | 4 +-- rust-ext/src/compressor.rs | 6 ++-- rust-ext/src/compressor_iterator.rs | 4 +-- rust-ext/src/compressor_multi.rs | 6 ++-- rust-ext/src/decompression_writer.rs | 12 +++---- rust-ext/src/decompressor.rs | 2 +- rust-ext/src/decompressor_iterator.rs | 4 +-- rust-ext/src/decompressor_multi.rs | 6 ++-- rust-ext/src/stream.rs | 14 ++++---- 17 files changed, 72 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c78c052c..a59cc5d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "bindgen" version = "0.72.1" @@ -146,12 +140,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "itertools" version = "0.13.0" @@ -193,15 +181,6 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -257,36 +236,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.25.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655" dependencies = [ - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.25.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.25.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330" dependencies = [ "libc", "pyo3-build-config", @@ -294,9 +269,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.25.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -306,13 +281,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] @@ -428,12 +402,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "wasi" version = "0.14.5+wasi-0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 157822f0..9da71a77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,5 +26,5 @@ version = "2.0.16" features = ["experimental", "legacy", "zstdmt"] [dependencies.pyo3] -version = "0.25.1" +version = "0.29.3" features = ["extension-module"] diff --git a/docs/news.rst b/docs/news.rst index 83641a75..66975356 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -17,6 +17,11 @@ Backwards Compatibility Notes * cibuildwheel upgrade required us to drop generating the CPython 3.13 free-threaded wheels. +Changes +------- + +* PyO3 Rust crate upgraded from 0.25 to 0.29. + 0.25.0 (released 2025-09-14) ============================ diff --git a/rust-ext/src/buffers.rs b/rust-ext/src/buffers.rs index 9ad609b1..289d1a48 100644 --- a/rust-ext/src/buffers.rs +++ b/rust-ext/src/buffers.rs @@ -26,7 +26,7 @@ pub(crate) struct BufferSegment { #[pyclass(module = "zstandard.backend_rust", name = "BufferSegment")] pub struct ZstdBufferSegment { /// The object backing storage. For reference counting. - _parent: PyObject, + _parent: Py, /// PyBuffer into parent object. buffer: PyBuffer, /// Offset of segment within data. @@ -53,7 +53,7 @@ impl ZstdBufferSegment { } // PyBufferProtocol. - fn bf_getbuffer(slf: PyRefMut, view: PyObject, flags: i32) -> PyResult<()> { + fn bf_getbuffer(slf: PyRefMut, view: Py, flags: i32) -> PyResult<()> { let slice = slf.as_slice(); if unsafe { @@ -74,7 +74,7 @@ impl ZstdBufferSegment { } #[allow(unused_variables)] - fn bf_releasebuffer(slf: PyRefMut, view: PyObject) {} + fn bf_releasebuffer(slf: PyRefMut, view: Py) {} // Our methods. @@ -90,7 +90,7 @@ impl ZstdBufferSegment { #[pyclass(module = "zstandard.backend_rust", name = "BufferSegments")] pub struct ZstdBufferSegments { - parent: PyObject, + parent: Py, } unsafe impl Sync for ZstdBufferSegments {} @@ -98,10 +98,10 @@ unsafe impl Sync for ZstdBufferSegments {} #[pymethods] impl ZstdBufferSegments { // PyBufferProtocol. - fn bf_getbuffer(slf: PyRefMut, view: PyObject, flags: i32) -> PyResult<()> { + fn bf_getbuffer(slf: PyRefMut, view: Py, flags: i32) -> PyResult<()> { let py = slf.py(); - let parent = slf.parent.downcast_bound::(py)?; + let parent = slf.parent.cast_bound::(py)?; if unsafe { pyo3::ffi::PyBuffer_FillInfo( @@ -121,12 +121,12 @@ impl ZstdBufferSegments { } #[allow(unused_variables)] - fn bf_releasebuffer(slf: PyRefMut, view: PyObject) {} + fn bf_releasebuffer(slf: PyRefMut, view: Py) {} } #[pyclass(module = "zstandard.backend_rust", name = "BufferWithSegments")] pub struct ZstdBufferWithSegments { - source: PyObject, + source: Py, pub(crate) buffer: PyBuffer, pub(crate) segments: Vec, } @@ -161,7 +161,7 @@ impl ZstdBufferWithSegments { } fn __getitem__(&self, key: isize) -> PyResult { - let py = unsafe { Python::assume_gil_acquired() }; + let py = unsafe { Python::assume_attached() }; if key < 0 { return Err(PyIndexError::new_err("offset must be non-negative")); @@ -180,14 +180,14 @@ impl ZstdBufferWithSegments { Ok(ZstdBufferSegment { _parent: self.source.clone_ref(py), - buffer: PyBuffer::get(self.source.downcast_bound(py)?)?, + buffer: PyBuffer::get(self.source.cast_bound(py)?)?, offset: segment.offset as _, len: segment.length as _, }) } // PyBufferProtocol. - fn bf_getbuffer(slf: PyRefMut, view: PyObject, flags: i32) -> PyResult<()> { + fn bf_getbuffer(slf: PyRefMut, view: Py, flags: i32) -> PyResult<()> { if unsafe { pyo3::ffi::PyBuffer_FillInfo( view.as_ptr() as *mut Py_buffer, @@ -206,7 +206,7 @@ impl ZstdBufferWithSegments { } #[allow(unused_variables)] - fn bf_releasebuffer(slf: PyRefMut, view: PyObject) {} + fn bf_releasebuffer(slf: PyRefMut, view: Py) {} // Our methods. @@ -256,8 +256,7 @@ impl ZstdBufferWithSegments { fn segments(slf: PyRef, py: Python) -> PyResult { Ok(ZstdBufferSegments { - // TODO surely there is a better way to cast self to PyObject? - parent: unsafe { Py::from_borrowed_ptr(py, slf.as_ptr()) }, + parent: slf.into_pyobject(py)?.into_any().unbind(), }) } @@ -272,7 +271,7 @@ impl ZstdBufferWithSegments { )] pub struct ZstdBufferWithSegmentsCollection { // Py. - pub(crate) buffers: Vec, + pub(crate) buffers: Vec>, first_elements: Vec, } @@ -287,7 +286,7 @@ impl ZstdBufferWithSegmentsCollection { } fn __getitem__(&self, key: isize) -> PyResult { - let py = unsafe { Python::assume_gil_acquired() }; + let py = unsafe { Python::assume_attached() }; if key < 0 { return Err(PyIndexError::new_err("offset must be non-negative")); @@ -309,7 +308,7 @@ impl ZstdBufferWithSegmentsCollection { offset = self.first_elements[buffer_index - 1]; } - let item = segment.downcast_bound::(py)?; + let item = segment.cast_bound::(py)?; return item.borrow().__getitem__((key - offset) as isize); } @@ -332,7 +331,7 @@ impl ZstdBufferWithSegmentsCollection { let mut offset = 0; for item in py_args { - let item = item.downcast::().map_err(|_| { + let item = item.cast::().map_err(|_| { PyTypeError::new_err("arguments must be BufferWithSegments instances") })?; let segment = item.borrow(); @@ -359,7 +358,7 @@ impl ZstdBufferWithSegmentsCollection { let mut size = 0; for buffer in &self.buffers { - let item = buffer.downcast_bound::(py)?; + let item = buffer.cast_bound::(py)?; for segment in &item.borrow().segments { size += segment.length as usize; diff --git a/rust-ext/src/compression_chunker.rs b/rust-ext/src/compression_chunker.rs index 5ce66fd2..e4323e1e 100644 --- a/rust-ext/src/compression_chunker.rs +++ b/rust-ext/src/compression_chunker.rs @@ -196,12 +196,12 @@ impl ZstdCompressionChunkerIterator { slf } - fn __next__(mut slf: PyRefMut) -> PyResult> { + fn __next__(mut slf: PyRefMut) -> PyResult>> { if slf.finished { return Ok(None); } - let py = unsafe { Python::assume_gil_acquired() }; + let py = unsafe { Python::assume_attached() }; // Consume any data left in the input. while let Some(mut in_buffer) = slf.source.input_buffer(py)? { diff --git a/rust-ext/src/compression_dict.rs b/rust-ext/src/compression_dict.rs index 89b3d743..1338f59d 100644 --- a/rust-ext/src/compression_dict.rs +++ b/rust-ext/src/compression_dict.rs @@ -257,7 +257,7 @@ fn train_dictionary( // validated to be PyBytes. for sample in samples.iter() { let bytes = sample - .downcast::() + .cast::() .or_else(|_| Err(PyValueError::new_err("samples must be bytes")))?; samples_len += bytes.as_bytes().len(); @@ -267,7 +267,7 @@ fn train_dictionary( let mut sample_sizes: Vec = Vec::with_capacity(samples.len()); for sample in samples.iter() { - let bytes = sample.downcast::()?; + let bytes = sample.cast::()?; let data = bytes.as_bytes(); sample_sizes.push(data.len()); samples_buffer.extend_from_slice(data); diff --git a/rust-ext/src/compression_reader.rs b/rust-ext/src/compression_reader.rs index 47626e34..54fd9829 100644 --- a/rust-ext/src/compression_reader.rs +++ b/rust-ext/src/compression_reader.rs @@ -145,9 +145,9 @@ impl ZstdCompressionReader { fn __exit__<'p>( mut slf: PyRefMut<'p, Self>, py: Python<'p>, - _exc_type: PyObject, - _exc_value: PyObject, - _exc_tb: PyObject, + _exc_type: Py, + _exc_value: Py, + _exc_tb: Py, ) -> PyResult { slf.entered = false; slf.close(py)?; diff --git a/rust-ext/src/compression_writer.rs b/rust-ext/src/compression_writer.rs index 06fe57be..60dc2ed6 100644 --- a/rust-ext/src/compression_writer.rs +++ b/rust-ext/src/compression_writer.rs @@ -22,7 +22,7 @@ const FLUSH_FRAME: usize = 1; #[pyclass(module = "zstandard.backend_rust")] pub struct ZstdCompressionWriter { cctx: Arc>, - writer: PyObject, + writer: Py, write_return_read: bool, closefd: bool, entered: bool, @@ -111,7 +111,7 @@ impl ZstdCompressionWriter { self.cctx.memory_size() } - fn fileno(&self, py: Python) -> PyResult { + fn fileno(&self, py: Python) -> PyResult> { if let Ok(fileno) = self.writer.getattr(py, "fileno") { fileno.call0(py) } else { diff --git a/rust-ext/src/compressionobj.rs b/rust-ext/src/compressionobj.rs index 63f98ca8..e55248f0 100644 --- a/rust-ext/src/compressionobj.rs +++ b/rust-ext/src/compressionobj.rs @@ -52,7 +52,7 @@ impl ZstdCompressionObj { let cctx = &self.cctx; while !source.is_empty() { let result = py - .allow_threads(|| { + .detach(|| { cctx.compress_chunk( source, zstd_sys::ZSTD_EndDirective::ZSTD_e_continue, @@ -100,7 +100,7 @@ impl ZstdCompressionObj { loop { let (chunk, _, call_again) = py - .allow_threads(|| cctx.compress_chunk(&[], flush_mode, write_size)) + .detach(|| cctx.compress_chunk(&[], flush_mode, write_size)) .or_else(|msg| { Err(ZstdError::new_err(format!( "error ending compression stream: {}", diff --git a/rust-ext/src/compressor.rs b/rust-ext/src/compressor.rs index fac6d259..eb3e7797 100644 --- a/rust-ext/src/compressor.rs +++ b/rust-ext/src/compressor.rs @@ -170,7 +170,7 @@ impl ZstdCompressor { // TODO implement 0 copy via Py_SIZE(). let data = py - .allow_threads(|| cctx.compress(source)) + .detach(|| cctx.compress(source)) .or_else(|msg| Err(ZstdError::new_err(format!("cannot compress: {}", msg))))?; Ok(PyBytes::new(py, &data)) @@ -264,7 +264,7 @@ impl ZstdCompressor { // Try to read from source stream. let read_object = ifh.call_method("read", (read_size,), None)?; - let read_bytes = read_object.downcast::()?; + let read_bytes = read_object.cast::()?; let read_data = read_bytes.as_bytes(); // If no data was read we are at EOF. @@ -281,7 +281,7 @@ impl ZstdCompressor { while !source.is_empty() { let result = py - .allow_threads(|| { + .detach(|| { cctx.compress_chunk( source, zstd_sys::ZSTD_EndDirective::ZSTD_e_continue, diff --git a/rust-ext/src/compressor_iterator.rs b/rust-ext/src/compressor_iterator.rs index 86bf25d2..7cb1a07a 100644 --- a/rust-ext/src/compressor_iterator.rs +++ b/rust-ext/src/compressor_iterator.rs @@ -32,12 +32,12 @@ impl ZstdCompressorIterator { slf } - fn __next__(mut slf: PyRefMut) -> PyResult> { + fn __next__(mut slf: PyRefMut) -> PyResult>> { if slf.finished_output { return Ok(None); } - let py = unsafe { Python::assume_gil_acquired() }; + let py = unsafe { Python::assume_attached() }; let mut dest_buffer: Vec = Vec::with_capacity(slf.write_size); diff --git a/rust-ext/src/compressor_multi.rs b/rust-ext/src/compressor_multi.rs index 5ebb9e12..964419c7 100644 --- a/rust-ext/src/compressor_multi.rs +++ b/rust-ext/src/compressor_multi.rs @@ -43,7 +43,7 @@ pub fn multi_compress_to_buffer( let mut sources = vec![]; let mut total_source_size = 0; - if let Ok(buffer) = data.downcast::() { + if let Ok(buffer) = data.cast::() { sources.reserve_exact(buffer.borrow().segments.len()); let borrow = buffer.borrow(); @@ -54,11 +54,11 @@ pub fn multi_compress_to_buffer( sources.push(DataSource { data: slice }); total_source_size += slice.len(); } - } else if let Ok(collection) = data.downcast::() { + } else if let Ok(collection) = data.cast::() { sources.reserve_exact(collection.len()?); for buffer_obj in &collection.borrow().buffers { - let buffer = buffer_obj.downcast_bound::(py)?; + let buffer = buffer_obj.cast_bound::(py)?; let borrow = buffer.borrow(); for i in 0..borrow.segments.len() { diff --git a/rust-ext/src/decompression_writer.rs b/rust-ext/src/decompression_writer.rs index 02c3861b..55694d7b 100644 --- a/rust-ext/src/decompression_writer.rs +++ b/rust-ext/src/decompression_writer.rs @@ -19,7 +19,7 @@ use { #[pyclass(module = "zstandard.backend_rust")] pub struct ZstdDecompressionWriter { dctx: Arc>, - writer: PyObject, + writer: Py, write_size: usize, write_return_read: bool, closefd: bool, @@ -87,9 +87,9 @@ impl ZstdDecompressionWriter { fn __exit__<'p>( mut slf: PyRefMut<'p, Self>, py: Python<'p>, - exc_type: PyObject, - exc_value: PyObject, - exc_tb: PyObject, + exc_type: Py, + exc_value: Py, + exc_tb: Py, ) -> PyResult { slf.entered = false; slf.close(py)?; @@ -129,7 +129,7 @@ impl ZstdDecompressionWriter { self.closed } - fn fileno(&self, py: Python) -> PyResult { + fn fileno(&self, py: Python) -> PyResult> { if let Ok(fileno) = self.writer.getattr(py, "fileno") { fileno.call0(py) } else { @@ -139,7 +139,7 @@ impl ZstdDecompressionWriter { } } - fn flush(&self, py: Python) -> PyResult { + fn flush(&self, py: Python) -> PyResult> { if self.closed { return Err(PyValueError::new_err("stream is closed")); } diff --git a/rust-ext/src/decompressor.rs b/rust-ext/src/decompressor.rs index 00a1acfd..ba8746b0 100644 --- a/rust-ext/src/decompressor.rs +++ b/rust-ext/src/decompressor.rs @@ -127,7 +127,7 @@ impl ZstdDecompressor { // Read all available input. loop { let read_object = ifh.call_method1("read", (read_size,))?; - let read_bytes = read_object.downcast::()?; + let read_bytes = read_object.cast::()?; let read_data = read_bytes.as_bytes(); if read_data.len() == 0 { diff --git a/rust-ext/src/decompressor_iterator.rs b/rust-ext/src/decompressor_iterator.rs index ac6f255c..dd3583c9 100644 --- a/rust-ext/src/decompressor_iterator.rs +++ b/rust-ext/src/decompressor_iterator.rs @@ -32,12 +32,12 @@ impl ZstdDecompressorIterator { slf } - fn __next__(mut slf: PyRefMut) -> PyResult> { + fn __next__(mut slf: PyRefMut) -> PyResult>> { if slf.finished_output { return Ok(None); } - let py = unsafe { Python::assume_gil_acquired() }; + let py = unsafe { Python::assume_attached() }; let mut dest_buffer: Vec = Vec::with_capacity(slf.write_size); diff --git a/rust-ext/src/decompressor_multi.rs b/rust-ext/src/decompressor_multi.rs index 787a63ac..49cefd68 100644 --- a/rust-ext/src/decompressor_multi.rs +++ b/rust-ext/src/decompressor_multi.rs @@ -49,7 +49,7 @@ pub fn multi_decompress_to_buffer( let mut sources = vec![]; - if let Ok(buffer) = frames.downcast::() { + if let Ok(buffer) = frames.cast::() { if decompressed_sizes.is_some() && frame_sizes.len() != buffer.len()? { return Err(PyValueError::new_err(format!( "decompressed_sizes size mismatch; expected {}, got {}", @@ -70,7 +70,7 @@ pub fn multi_decompress_to_buffer( decompressed_size: *frame_sizes.get(i).unwrap_or(&0) as _, }); } - } else if let Ok(collection) = frames.downcast::() { + } else if let Ok(collection) = frames.cast::() { let frames_count = collection.len()?; if decompressed_sizes.is_some() && frame_sizes.len() != frames_count { @@ -85,7 +85,7 @@ pub fn multi_decompress_to_buffer( let mut offset = 0; for buffer_obj in &collection.borrow().buffers { - let buffer = buffer_obj.downcast_bound::(py)?; + let buffer = buffer_obj.cast_bound::(py)?; let borrow = buffer.borrow(); for i in 0..borrow.segments.len() { diff --git a/rust-ext/src/stream.rs b/rust-ext/src/stream.rs index 72dbbe4e..4e121f8b 100644 --- a/rust-ext/src/stream.rs +++ b/rust-ext/src/stream.rs @@ -11,8 +11,8 @@ use { /// Describes a type that can be resolved to a `zstd_sys::ZSTD_inBuffer`. pub trait InBufferSource { - /// Obtain the PyObject this instance is reading from. - fn source_object(&self) -> &PyObject; + /// Obtain the Py this instance is reading from. + fn source_object(&self) -> &Py; /// The size of the input object, if available. fn source_size(&self) -> Option; @@ -29,7 +29,7 @@ pub trait InBufferSource { /// A data source where data is obtaine by calling `read()`. struct ReadSource { - source: PyObject, + source: Py, buffer: Option>, read_size: usize, finished: bool, @@ -37,7 +37,7 @@ struct ReadSource { } impl InBufferSource for ReadSource { - fn source_object(&self) -> &PyObject { + fn source_object(&self) -> &Py { &self.source } @@ -94,16 +94,16 @@ impl InBufferSource for ReadSource { } } -/// A data source where data is obtained from a `PyObject` +/// A data source where data is obtained from a `Py` /// conforming to the buffer protocol. struct BufferSource { - source: PyObject, + source: Py, buffer: PyBuffer, offset: usize, } impl InBufferSource for BufferSource { - fn source_object(&self) -> &PyObject { + fn source_object(&self) -> &Py { &self.source } From cd2a5656ff549331b11456ccbacc8948c1b7a5f9 Mon Sep 17 00:00:00 2001 From: zydo Date: Mon, 12 Oct 2026 02:56:30 -0700 Subject: [PATCH 6/6] Add Python 3.15 support Build cp315 and cp315t wheels and add 3.15 and 3.15t to the test and typing CI matrices. Like 3.14, Linux wheels for 3.15 are built with manylinux_2_28 and musllinux_1_2 only. Add the Python 3.15 trove classifier and list 3.15 as supported in the docs. --- .github/workflows/test.yml | 2 ++ .github/workflows/typing.yml | 2 ++ .github/workflows/wheel.yml | 14 ++++++++++++++ docs/installing.rst | 8 ++++---- docs/news.rst | 6 +++++- pyproject.toml | 1 + 6 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4019757..162f3fbe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,8 @@ jobs: - '3.13t' - '3.14' - '3.14t' + - '3.15' + - '3.15t' arch: - 'arm64' - 'x86' diff --git a/.github/workflows/typing.yml b/.github/workflows/typing.yml index 35786201..f94dde4e 100644 --- a/.github/workflows/typing.yml +++ b/.github/workflows/typing.yml @@ -22,6 +22,8 @@ jobs: - '3.13t' - '3.14' - '3.14t' + - '3.15' + - '3.15t' runs-on: 'ubuntu-24.04' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index f9e8f2e5..5fc9da6e 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -22,6 +22,8 @@ jobs: - 'cp313' - 'cp314' - 'cp314t' + - 'cp315' + - 'cp315t' arch: - 'aarch64' - 'i686' @@ -66,11 +68,19 @@ jobs: image: 'manylinux2014' - py: 'cp314t' image: 'manylinux2014' + - py: 'cp315' + image: 'manylinux2014' + - py: 'cp315t' + image: 'manylinux2014' # Drop support for musllinux_1_1 on 3.14+ - py: 'cp314' image: 'musllinux_1_1' - py: 'cp314t' image: 'musllinux_1_1' + - py: 'cp315' + image: 'musllinux_1_1' + - py: 'cp315t' + image: 'musllinux_1_1' runs-on: ${{ matrix.arch == 'aarch64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} env: CIBW_ARCHS: ${{ matrix.arch }} @@ -124,6 +134,8 @@ jobs: - 'cp313' - 'cp314' - 'cp314t' + - 'cp315' + - 'cp315t' arch: - 'arm64' - 'x86_64' @@ -164,6 +176,8 @@ jobs: - 'cp313' - 'cp314' - 'cp314t' + - 'cp315' + - 'cp315t' arch: - 'win32' - 'win_amd64' diff --git a/docs/installing.rst b/docs/installing.rst index c2f0e72b..8391f899 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -22,10 +22,10 @@ this package with ``conda``. Requirements ============ -This package is designed to run with Python 3.10, 3.11, 3.12, 3.13, and 3.14 -on common platforms (Linux, Windows, and OS X). On PyPy (both PyPy2 and PyPy3) -we support version 6.0.0 and above. x86 and x86_64 are well-tested on Windows. -Only x86_64 is well-tested on Linux and macOS. +This package is designed to run with Python 3.10, 3.11, 3.12, 3.13, 3.14, +and 3.15 on common platforms (Linux, Windows, and OS X). On PyPy (both PyPy2 +and PyPy3) we support version 6.0.0 and above. x86 and x86_64 are well-tested +on Windows. Only x86_64 is well-tested on Linux and macOS. CFFI Backend ============ diff --git a/docs/news.rst b/docs/news.rst index 66975356..7df71020 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -20,7 +20,11 @@ Backwards Compatibility Notes Changes ------- -* PyO3 Rust crate upgraded from 0.25 to 0.29. +* Python 3.15 support. We build wheels (including free-threaded ``cp315t`` + wheels) and have CI test coverage. Like 3.14, Linux wheels are built with + ``manylinux_2_28`` and ``musllinux_1_2``. +* PyO3 Rust crate upgraded from 0.25 to 0.29, which is required for + Python 3.15 support in the Rust backend. 0.25.0 (released 2025-09-14) ============================ diff --git a/pyproject.toml b/pyproject.toml index cbd2d07b..b2b74145 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Free Threading :: 1 - Unstable", ] keywords = ["zstandard", "zstd", "compression"]