From 5e8f92cf37bbddbf0376834cb3e7c855ebe38261 Mon Sep 17 00:00:00 2001 From: Jan-Paul Bultmann <74891396+somethingelseentirely@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:05:05 +0200 Subject: [PATCH] test(pyo3): validate PyAnyBytes memoryview --- CHANGELOG.md | 5 ++++- Cargo.toml | 2 +- README.md | 6 +++--- examples/{pybytes.rs => pyanybytes.rs} | 4 ++-- src/lib.rs | 4 ++-- src/{pybytes.rs => pyanybytes.rs} | 6 +++--- src/tests.rs | 30 ++++++++++++++++++++++++++ 7 files changed, 45 insertions(+), 12 deletions(-) rename examples/{pybytes.rs => pyanybytes.rs} (81%) rename src/{pybytes.rs => pyanybytes.rs} (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ab1a5..e1505e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,7 +57,7 @@ in a dedicated AGENTS section - add tests for weak reference upgrade/downgrade and Kani proofs for view helpers - add Kani proofs covering `Bytes::try_unwrap_owner` and `WeakBytes` upgrade semantics -- add examples for quick start and PyBytes usage +- add examples for quick start and PyAnyBytes usage - add example showing how to wrap Python `bytes` into `Bytes` - summarize built-in `ByteSource`s and show how to extend them - added tests verifying `WeakView` upgrade and drop semantics @@ -97,6 +97,9 @@ - implemented `bytes::Buf` for `Bytes` and `From` for `bytes::Bytes` for seamless integration with Tokio and other libraries - implemented `ExactSizeIterator` and `FusedIterator` for `BytesIterOffsets` +- added test exposing `PyAnyBytes` as a read-only `memoryview` +- renamed `PyBytes` wrapper to `PyAnyBytes` to avoid confusion +- renamed `py_anybytes` module to `pyanybytes` for consistency ## 0.19.3 - 2025-05-30 - implemented `Error` for `ViewError` diff --git a/Cargo.toml b/Cargo.toml index 551d889..67798be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ name = "from_python" required-features = ["pyo3"] [[example]] -name = "pybytes" +name = "pyanybytes" required-features = ["pyo3"] [[example]] diff --git a/README.md b/README.md index 16492d7..7227975 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Other optional features provide additional integrations: - `ownedbytes` – adds compatibility with [`ownedbytes`](https://crates.io/crates/ownedbytes) and implements its `StableDeref` trait. - `mmap` – enables memory-mapped file handling via the `memmap2` crate. - `zerocopy` – exposes the [`view`](src/view.rs) module for typed zero-copy access and allows using `zerocopy` types as sources. -- `pyo3` – builds the [`pybytes`](src/pybytes.rs) module to provide Python bindings for `Bytes`. +- `pyo3` – builds the [`pyanybytes`](src/pyanybytes.rs) module to provide Python bindings for `Bytes`. - `winnow` – implements the [`Stream`](https://docs.rs/winnow/) traits for `Bytes` and offers parsers (`view`, `view_elems(count)`) that return typed `View`s. Enabling the `pyo3` feature requires the Python development headers and libraries @@ -145,7 +145,7 @@ needs these libraries installed; otherwise disable the feature during testing. - [`examples/quick_start.rs`](examples/quick_start.rs) – the quick start shown above - [`examples/try_unwrap_owner.rs`](examples/try_unwrap_owner.rs) – reclaim the owner when uniquely referenced -- [`examples/pybytes.rs`](examples/pybytes.rs) – demonstrates the `pyo3` feature using `PyBytes` +- [`examples/pyanybytes.rs`](examples/pyanybytes.rs) – demonstrates the `pyo3` feature using `PyAnyBytes` - [`examples/from_python.rs`](examples/from_python.rs) – wrap a Python `bytes` object into `Bytes` - [`examples/python_winnow.rs`](examples/python_winnow.rs) – parse Python bytes with winnow - [`examples/python_winnow_view.rs`](examples/python_winnow_view.rs) – parse structured data from Python bytes using winnow's `view` @@ -179,7 +179,7 @@ development iterations. - [`ByteSource`](src/bytes.rs) – trait for objects that can provide bytes. - [`ByteOwner`](src/bytes.rs) – keeps backing storage alive. - [`view` module](src/view.rs) – typed zero-copy access to bytes. -- [`pybytes` module](src/pybytes.rs) – Python bindings. +- [`pyanybytes` module](src/pyanybytes.rs) – Python bindings. ## Acknowledgements This library started as a fork of the minibyte library in facebooks [sapling scm](https://github.com/facebook/sapling). diff --git a/examples/pybytes.rs b/examples/pyanybytes.rs similarity index 81% rename from examples/pybytes.rs rename to examples/pyanybytes.rs index 8cac38a..f078b87 100644 --- a/examples/pybytes.rs +++ b/examples/pyanybytes.rs @@ -1,10 +1,10 @@ -use anybytes::{Bytes, PyBytes}; +use anybytes::{Bytes, PyAnyBytes}; use pyo3::prelude::*; fn main() -> PyResult<()> { Python::with_gil(|py| { let bytes = Bytes::from(vec![1u8, 2, 3, 4]); - let wrapped = Py::new(py, PyBytes::new(bytes))?; + let wrapped = Py::new(py, PyAnyBytes::new(bytes))?; let builtins = PyModule::import(py, "builtins")?; let memoryview = builtins.getattr("memoryview")?.call1((wrapped.bind(py),))?; diff --git a/src/lib.rs b/src/lib.rs index 0f8ca29..b1a4a70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ pub mod view; #[cfg(feature = "pyo3")] /// Python bindings for [`Bytes`]. -pub mod pybytes; +pub mod pyanybytes; #[cfg(feature = "winnow")] /// Integration with the `winnow` parser library. @@ -37,7 +37,7 @@ pub use crate::bytes::ByteSource; pub use crate::bytes::Bytes; pub use crate::bytes::WeakBytes; #[cfg(feature = "pyo3")] -pub use crate::pybytes::PyBytes; +pub use crate::pyanybytes::PyAnyBytes; #[cfg(feature = "zerocopy")] pub use crate::view::View; diff --git a/src/pybytes.rs b/src/pyanybytes.rs similarity index 95% rename from src/pybytes.rs rename to src/pyanybytes.rs index f8c8187..cd8a08e 100644 --- a/src/pybytes.rs +++ b/src/pyanybytes.rs @@ -13,12 +13,12 @@ use crate::Bytes; /// Python wrapper around [`Bytes`]. #[pyclass(name = "Bytes")] -pub struct PyBytes { +pub struct PyAnyBytes { bytes: Bytes, } #[pymethods] -impl PyBytes { +impl PyAnyBytes { /// Exposes the bytes to Python's buffer protocol. /// /// # Safety @@ -45,7 +45,7 @@ impl PyBytes { } } -impl PyBytes { +impl PyAnyBytes { /// Wrap a [`Bytes`] instance for Python exposure. pub fn new(bytes: Bytes) -> Self { Self { bytes } diff --git a/src/tests.rs b/src/tests.rs index c4faa10..7009d4f 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -560,3 +560,33 @@ fn test_area_alignment_padding() { expected.extend_from_slice(&0x0506u16.to_ne_bytes()); assert_eq!(all.as_ref(), expected.as_slice()); } + +#[cfg(feature = "pyo3")] +#[test] +fn test_pyanybytes_memoryview() { + use crate::{Bytes, PyAnyBytes}; + use pyo3::types::{PyAnyMethods, PyMemoryView}; + use pyo3::{Py, Python}; + + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let data = b"memoryview"; + let bytes = PyAnyBytes::new(Bytes::from(data.to_vec())); + let py_obj = Py::new(py, bytes).expect("PyAnyBytes"); + let view = PyMemoryView::from(py_obj.bind(py).as_any()).expect("memoryview"); + + let mv_bytes: Vec = view + .call_method0("tobytes") + .expect("tobytes") + .extract() + .expect("extract bytes"); + assert_eq!(mv_bytes.as_slice(), data); + + let readonly: bool = view + .getattr("readonly") + .expect("readonly attr") + .extract() + .expect("extract bool"); + assert!(readonly); + }); +}