Added `ToPyObject` and `IntoPy<PyObject>` impls for `PyBackedStr`. (#4205)

* Added `ToPyObject` and `Into<PyObject>` impls for `PyBackedStr`.

* Create 4205.added.md

* Added attributes limiting the `ToPyObject` and `IntoPy<PyObject>` impls to the case where `cfg(any(Py_3_10, not(Py_LIMITED_API)))`. When this cfg does not apply, the conversion is less trivial since the `storage` is actually `PyBytes`, not `PyString`.

* Fixed imports format.

* Updated the `ToPyObject` and `IntoPy<PyObject>` impls to support the `cfg(not(any(Py_3_10, not(Py_LIMITED_API))))` case by converting the `PyBytes` back to `PyString`.

* Added `ToPyObject` and `IntoPy<PyObject>` impls for `PyBackedBytes`.

* Added tests for the `PyBackedBytes` conversion impls.

* Updated newsfragment entry to include the `PyBackedBytes` impls.

* Changed the `IntoPy` and `ToPyObject` impls for `PyBackedBytes` to produce `PyBytes` regardless of the backing variant. Updated tests to demonstrate this.

* retrigger checks

* Updated `PyBackedStr` conversion tests to extract the result as a `PyBackedStr` instead of `&str` since the latter is not supported under some `cfg`'s.

* Fixed `IntoPy<PyObject> for PyBackedBytes` impl to create `bytes` for both storage types as intended. Updated test to properly catch the error.

---------

Co-authored-by: jrudolph <jrudolph@anl.gov>
This commit is contained in:
JRRudy1 2024-06-01 16:09:14 -05:00 committed by GitHub
parent a7a5c10b8a
commit 5d47c4ae4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 94 additions and 1 deletions

View File

@ -0,0 +1 @@
Added `ToPyObject` and `IntoPy<PyObject>` impls for `PyBackedStr` and `PyBackedBytes`.

View File

@ -7,7 +7,7 @@ use crate::{
any::PyAnyMethods, bytearray::PyByteArrayMethods, bytes::PyBytesMethods,
string::PyStringMethods, PyByteArray, PyBytes, PyString,
},
Bound, DowncastError, FromPyObject, Py, PyAny, PyErr, PyResult,
Bound, DowncastError, FromPyObject, IntoPy, Py, PyAny, PyErr, PyResult, Python, ToPyObject,
};
/// A wrapper around `str` where the storage is owned by a Python `bytes` or `str` object.
@ -85,6 +85,28 @@ impl FromPyObject<'_> for PyBackedStr {
}
}
impl ToPyObject for PyBackedStr {
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
fn to_object(&self, py: Python<'_>) -> Py<PyAny> {
self.storage.clone_ref(py)
}
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
fn to_object(&self, py: Python<'_>) -> Py<PyAny> {
PyString::new_bound(py, self).into_any().unbind()
}
}
impl IntoPy<Py<PyAny>> for PyBackedStr {
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
fn into_py(self, _py: Python<'_>) -> Py<PyAny> {
self.storage
}
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
fn into_py(self, py: Python<'_>) -> Py<PyAny> {
PyString::new_bound(py, &self).into_any().unbind()
}
}
/// A wrapper around `[u8]` where the storage is either owned by a Python `bytes` object, or a Rust `Box<[u8]>`.
///
/// This type gives access to the underlying data via a `Deref` implementation.
@ -181,6 +203,24 @@ impl FromPyObject<'_> for PyBackedBytes {
}
}
impl ToPyObject for PyBackedBytes {
fn to_object(&self, py: Python<'_>) -> Py<PyAny> {
match &self.storage {
PyBackedBytesStorage::Python(bytes) => bytes.to_object(py),
PyBackedBytesStorage::Rust(bytes) => PyBytes::new_bound(py, bytes).into_any().unbind(),
}
}
}
impl IntoPy<Py<PyAny>> for PyBackedBytes {
fn into_py(self, py: Python<'_>) -> Py<PyAny> {
match self.storage {
PyBackedBytesStorage::Python(bytes) => bytes.into_any(),
PyBackedBytesStorage::Rust(bytes) => PyBytes::new_bound(py, &bytes).into_any().unbind(),
}
}
}
macro_rules! impl_traits {
($slf:ty, $equiv:ty) => {
impl std::fmt::Debug for $slf {
@ -288,6 +328,30 @@ mod test {
});
}
#[test]
fn py_backed_str_to_object() {
Python::with_gil(|py| {
let orig_str = PyString::new_bound(py, "hello");
let py_backed_str = orig_str.extract::<PyBackedStr>().unwrap();
let new_str = py_backed_str.to_object(py);
assert_eq!(new_str.extract::<PyBackedStr>(py).unwrap(), "hello");
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
assert!(new_str.is(&orig_str));
});
}
#[test]
fn py_backed_str_into_py() {
Python::with_gil(|py| {
let orig_str = PyString::new_bound(py, "hello");
let py_backed_str = orig_str.extract::<PyBackedStr>().unwrap();
let new_str = py_backed_str.into_py(py);
assert_eq!(new_str.extract::<PyBackedStr>(py).unwrap(), "hello");
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
assert!(new_str.is(&orig_str));
});
}
#[test]
fn py_backed_bytes_empty() {
Python::with_gil(|py| {
@ -324,6 +388,34 @@ mod test {
});
}
#[test]
fn py_backed_bytes_into_py() {
Python::with_gil(|py| {
let orig_bytes = PyBytes::new_bound(py, b"abcde");
let py_backed_bytes = PyBackedBytes::from(orig_bytes.clone());
assert!(py_backed_bytes.to_object(py).is(&orig_bytes));
assert!(py_backed_bytes.into_py(py).is(&orig_bytes));
});
}
#[test]
fn rust_backed_bytes_into_py() {
Python::with_gil(|py| {
let orig_bytes = PyByteArray::new_bound(py, b"abcde");
let rust_backed_bytes = PyBackedBytes::from(orig_bytes);
assert!(matches!(
rust_backed_bytes.storage,
PyBackedBytesStorage::Rust(_)
));
let to_object = rust_backed_bytes.to_object(py).into_bound(py);
assert!(&to_object.is_exact_instance_of::<PyBytes>());
assert_eq!(&to_object.extract::<PyBackedBytes>().unwrap(), b"abcde");
let into_py = rust_backed_bytes.into_py(py).into_bound(py);
assert!(&into_py.is_exact_instance_of::<PyBytes>());
assert_eq!(&into_py.extract::<PyBackedBytes>().unwrap(), b"abcde");
});
}
#[test]
fn test_backed_types_send_sync() {
fn is_send<T: Send>() {}