Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement PartialEq<str> for Bound<'py, PyString> #4245

Merged
merged 4 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/4245.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement `PartialEq<str>` for `Bound<'py, PyString>`.
9 changes: 9 additions & 0 deletions pyo3-ffi/src/unicodeobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,15 @@ extern "C" {
pub fn PyUnicode_Compare(left: *mut PyObject, right: *mut PyObject) -> c_int;
#[cfg_attr(PyPy, link_name = "PyPyUnicode_CompareWithASCIIString")]
pub fn PyUnicode_CompareWithASCIIString(left: *mut PyObject, right: *const c_char) -> c_int;
#[cfg(Py_3_13)]
pub fn PyUnicode_EqualToUTF8(unicode: *mut PyObject, string: *const c_char) -> c_int;
#[cfg(Py_3_13)]
pub fn PyUnicode_EqualToUTF8AndSize(
unicode: *mut PyObject,
string: *const c_char,
size: Py_ssize_t,
) -> c_int;

pub fn PyUnicode_RichCompare(
left: *mut PyObject,
right: *mut PyObject,
Expand Down
8 changes: 3 additions & 5 deletions src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2010,9 +2010,7 @@ impl PyObject {
#[cfg(test)]
mod tests {
use super::{Bound, Py, PyObject};
use crate::types::any::PyAnyMethods;
use crate::types::{dict::IntoPyDict, PyDict, PyString};
use crate::types::{PyCapsule, PyStringMethods};
use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString};
use crate::{ffi, Borrowed, PyAny, PyResult, Python, ToPyObject};

#[test]
Expand All @@ -2021,7 +2019,7 @@ mod tests {
let obj = py.get_type_bound::<PyDict>().to_object(py);

let assert_repr = |obj: &Bound<'_, PyAny>, expected: &str| {
assert_eq!(obj.repr().unwrap().to_cow().unwrap(), expected);
assert_eq!(obj.repr().unwrap(), expected);
};

assert_repr(obj.call0(py).unwrap().bind(py), "{}");
Expand Down Expand Up @@ -2221,7 +2219,7 @@ a = A()
let obj_unbound: Py<PyString> = obj.unbind();
let obj: Bound<'_, PyString> = obj_unbound.into_bound(py);

assert_eq!(obj.to_cow().unwrap(), "hello world");
assert_eq!(obj, "hello world");
});
}

Expand Down
13 changes: 3 additions & 10 deletions src/types/bytearray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,12 +515,8 @@ impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyByteArray> {

#[cfg(test)]
mod tests {
use crate::types::any::PyAnyMethods;
use crate::types::bytearray::PyByteArrayMethods;
use crate::types::string::PyStringMethods;
use crate::types::PyByteArray;
use crate::{exceptions, Bound, PyAny};
use crate::{PyObject, Python};
use crate::types::{PyAnyMethods, PyByteArray, PyByteArrayMethods};
use crate::{exceptions, Bound, PyAny, PyObject, Python};

#[test]
fn test_len() {
Expand Down Expand Up @@ -555,10 +551,7 @@ mod tests {

slice[0..5].copy_from_slice(b"Hi...");

assert_eq!(
bytearray.str().unwrap().to_cow().unwrap(),
"bytearray(b'Hi... Python')"
);
assert_eq!(bytearray.str().unwrap(), "bytearray(b'Hi... Python')");
});
}

Expand Down
10 changes: 4 additions & 6 deletions src/types/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl PyModule {
/// Python::with_gil(|py| -> PyResult<()> {
/// let module = PyModule::new_bound(py, "my_module")?;
///
/// assert_eq!(module.name()?.to_cow()?, "my_module");
/// assert_eq!(module.name()?, "my_module");
/// Ok(())
/// })?;
/// # Ok(())}
Expand Down Expand Up @@ -721,24 +721,22 @@ fn __name__(py: Python<'_>) -> &Bound<'_, PyString> {
#[cfg(test)]
mod tests {
use crate::{
types::{module::PyModuleMethods, string::PyStringMethods, PyModule},
types::{module::PyModuleMethods, PyModule},
Python,
};

#[test]
fn module_import_and_name() {
Python::with_gil(|py| {
let builtins = PyModule::import_bound(py, "builtins").unwrap();
assert_eq!(
builtins.name().unwrap().to_cow().unwrap().as_ref(),
"builtins"
);
assert_eq!(builtins.name().unwrap(), "builtins");
})
}

#[test]
#[cfg(not(PyPy))]
fn module_filename() {
use crate::types::string::PyStringMethods;
Python::with_gil(|py| {
let site = PyModule::import_bound(py, "site").unwrap();
assert!(site
Expand Down
176 changes: 172 additions & 4 deletions src/types/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,33 @@ impl<'a> PyStringData<'a> {

/// Represents a Python `string` (a Unicode string object).
///
/// This type is immutable.
/// This type is only seen inside PyO3's smart pointers as [`Py<PyString>`], [`Bound<'py, PyString>`],
/// and [`Borrowed<'a, 'py, PyString>`].
///
/// All functionality on this type is implemented through the [`PyStringMethods`] trait.
///
/// # Equality
///
/// For convenience, [`Bound<'py, PyString>`] implements [`PartialEq<str>`] to allow comparing the
/// data in the Python string to a Rust UTF-8 string slice.
///
/// This is not always the most appropriate way to compare Python strings, as Python string subclasses
/// may have different equality semantics. In situations where subclasses overriding equality might be
/// relevant, use [`PyAnyMethods::eq`], at cost of the additional overhead of a Python method call.
///
/// ```rust
/// # use pyo3::prelude::*;
/// use pyo3::types::PyString;
///
/// # Python::with_gil(|py| {
/// let py_string = PyString::new_bound(py, "foo");
/// // via PartialEq<str>
/// assert_eq!(py_string, "foo");
///
/// // via Python equality
/// assert!(py_string.as_any().eq("foo").unwrap());
/// # });
/// ```
#[repr(transparent)]
pub struct PyString(PyAny);

Expand Down Expand Up @@ -490,6 +516,118 @@ impl IntoPy<Py<PyString>> for &'_ Py<PyString> {
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<str> for Bound<'_, PyString> {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_borrowed() == *other
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<&'_ str> for Bound<'_, PyString> {
#[inline]
fn eq(&self, other: &&str) -> bool {
self.as_borrowed() == **other
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<Bound<'_, PyString>> for str {
#[inline]
fn eq(&self, other: &Bound<'_, PyString>) -> bool {
*self == other.as_borrowed()
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<&'_ Bound<'_, PyString>> for str {
#[inline]
fn eq(&self, other: &&Bound<'_, PyString>) -> bool {
*self == other.as_borrowed()
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<Bound<'_, PyString>> for &'_ str {
#[inline]
fn eq(&self, other: &Bound<'_, PyString>) -> bool {
**self == other.as_borrowed()
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<str> for &'_ Bound<'_, PyString> {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_borrowed() == other
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<str> for Borrowed<'_, '_, PyString> {
#[inline]
fn eq(&self, other: &str) -> bool {
#[cfg(not(Py_3_13))]
{
self.to_cow().map_or(false, |s| s == other)
}

#[cfg(Py_3_13)]
unsafe {
ffi::PyUnicode_EqualToUTF8AndSize(
self.as_ptr(),
other.as_ptr().cast(),
other.len() as _,
) == 1
}
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<&str> for Borrowed<'_, '_, PyString> {
#[inline]
fn eq(&self, other: &&str) -> bool {
*self == **other
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<Borrowed<'_, '_, PyString>> for str {
#[inline]
fn eq(&self, other: &Borrowed<'_, '_, PyString>) -> bool {
other == self
}
}

/// Compares whether the data in the Python string is equal to the given UTF8.
///
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
impl PartialEq<Borrowed<'_, '_, PyString>> for &'_ str {
#[inline]
fn eq(&self, other: &Borrowed<'_, '_, PyString>) -> bool {
other == self
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -708,15 +846,15 @@ mod tests {
fn test_intern_string() {
Python::with_gil(|py| {
let py_string1 = PyString::intern_bound(py, "foo");
assert_eq!(py_string1.to_cow().unwrap(), "foo");
assert_eq!(py_string1, "foo");

let py_string2 = PyString::intern_bound(py, "foo");
assert_eq!(py_string2.to_cow().unwrap(), "foo");
assert_eq!(py_string2, "foo");

assert_eq!(py_string1.as_ptr(), py_string2.as_ptr());

let py_string3 = PyString::intern_bound(py, "bar");
assert_eq!(py_string3.to_cow().unwrap(), "bar");
assert_eq!(py_string3, "bar");

assert_ne!(py_string1.as_ptr(), py_string3.as_ptr());
});
Expand Down Expand Up @@ -762,4 +900,34 @@ mod tests {
assert_eq!(py_string.to_string_lossy(py), "🐈 Hello ���World");
})
}

#[test]
fn test_comparisons() {
Python::with_gil(|py| {
let s = "hello, world";
let py_string = PyString::new_bound(py, s);

assert_eq!(py_string, "hello, world");

assert_eq!(py_string, s);
assert_eq!(&py_string, s);
assert_eq!(s, py_string);
assert_eq!(s, &py_string);
davidhewitt marked this conversation as resolved.
Show resolved Hide resolved

assert_eq!(py_string, *s);
assert_eq!(&py_string, *s);
assert_eq!(*s, py_string);
assert_eq!(*s, &py_string);

let py_string = py_string.as_borrowed();

assert_eq!(py_string, s);
assert_eq!(&py_string, s);
assert_eq!(s, py_string);
assert_eq!(s, &py_string);

assert_eq!(py_string, *s);
assert_eq!(*s, py_string);
})
}
}
7 changes: 2 additions & 5 deletions tests/test_proto_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,18 +131,15 @@ fn test_delattr() {
fn test_str() {
Python::with_gil(|py| {
let example_py = make_example(py);
assert_eq!(example_py.str().unwrap().to_cow().unwrap(), "5");
assert_eq!(example_py.str().unwrap(), "5");
})
}

#[test]
fn test_repr() {
Python::with_gil(|py| {
let example_py = make_example(py);
assert_eq!(
example_py.repr().unwrap().to_cow().unwrap(),
"ExampleClass(value=5)"
);
assert_eq!(example_py.repr().unwrap(), "ExampleClass(value=5)");
})
}

Expand Down
Loading