Skip to content

Commit 9648d59

Browse files
authored
implement PartialEq<str> for Bound<'py, PyString> (#4245)
* implement `PartialEq<str>` for `Bound<'py, PyString>` * fixup conditional code * document equality semantics for `Bound<'_, PyString>` * fix doc example
1 parent 0b2f19b commit 9648d59

File tree

7 files changed

+194
-30
lines changed

7 files changed

+194
-30
lines changed

newsfragments/4245.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement `PartialEq<str>` for `Bound<'py, PyString>`.

pyo3-ffi/src/unicodeobject.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,15 @@ extern "C" {
328328
pub fn PyUnicode_Compare(left: *mut PyObject, right: *mut PyObject) -> c_int;
329329
#[cfg_attr(PyPy, link_name = "PyPyUnicode_CompareWithASCIIString")]
330330
pub fn PyUnicode_CompareWithASCIIString(left: *mut PyObject, right: *const c_char) -> c_int;
331+
#[cfg(Py_3_13)]
332+
pub fn PyUnicode_EqualToUTF8(unicode: *mut PyObject, string: *const c_char) -> c_int;
333+
#[cfg(Py_3_13)]
334+
pub fn PyUnicode_EqualToUTF8AndSize(
335+
unicode: *mut PyObject,
336+
string: *const c_char,
337+
size: Py_ssize_t,
338+
) -> c_int;
339+
331340
pub fn PyUnicode_RichCompare(
332341
left: *mut PyObject,
333342
right: *mut PyObject,

src/instance.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,9 +2010,7 @@ impl PyObject {
20102010
#[cfg(test)]
20112011
mod tests {
20122012
use super::{Bound, Py, PyObject};
2013-
use crate::types::any::PyAnyMethods;
2014-
use crate::types::{dict::IntoPyDict, PyDict, PyString};
2015-
use crate::types::{PyCapsule, PyStringMethods};
2013+
use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString};
20162014
use crate::{ffi, Borrowed, PyAny, PyResult, Python, ToPyObject};
20172015

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

20232021
let assert_repr = |obj: &Bound<'_, PyAny>, expected: &str| {
2024-
assert_eq!(obj.repr().unwrap().to_cow().unwrap(), expected);
2022+
assert_eq!(obj.repr().unwrap(), expected);
20252023
};
20262024

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

2224-
assert_eq!(obj.to_cow().unwrap(), "hello world");
2222+
assert_eq!(obj, "hello world");
22252223
});
22262224
}
22272225

src/types/bytearray.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -515,12 +515,8 @@ impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyByteArray> {
515515

516516
#[cfg(test)]
517517
mod tests {
518-
use crate::types::any::PyAnyMethods;
519-
use crate::types::bytearray::PyByteArrayMethods;
520-
use crate::types::string::PyStringMethods;
521-
use crate::types::PyByteArray;
522-
use crate::{exceptions, Bound, PyAny};
523-
use crate::{PyObject, Python};
518+
use crate::types::{PyAnyMethods, PyByteArray, PyByteArrayMethods};
519+
use crate::{exceptions, Bound, PyAny, PyObject, Python};
524520

525521
#[test]
526522
fn test_len() {
@@ -555,10 +551,7 @@ mod tests {
555551

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

558-
assert_eq!(
559-
bytearray.str().unwrap().to_cow().unwrap(),
560-
"bytearray(b'Hi... Python')"
561-
);
554+
assert_eq!(bytearray.str().unwrap(), "bytearray(b'Hi... Python')");
562555
});
563556
}
564557

src/types/module.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl PyModule {
3737
/// Python::with_gil(|py| -> PyResult<()> {
3838
/// let module = PyModule::new_bound(py, "my_module")?;
3939
///
40-
/// assert_eq!(module.name()?.to_cow()?, "my_module");
40+
/// assert_eq!(module.name()?, "my_module");
4141
/// Ok(())
4242
/// })?;
4343
/// # Ok(())}
@@ -728,23 +728,21 @@ fn __name__(py: Python<'_>) -> &Bound<'_, PyString> {
728728
#[cfg(test)]
729729
mod tests {
730730
use crate::{
731-
types::{module::PyModuleMethods, string::PyStringMethods, PyModule},
731+
types::{module::PyModuleMethods, PyModule},
732732
Python,
733733
};
734734

735735
#[test]
736736
fn module_import_and_name() {
737737
Python::with_gil(|py| {
738738
let builtins = PyModule::import_bound(py, "builtins").unwrap();
739-
assert_eq!(
740-
builtins.name().unwrap().to_cow().unwrap().as_ref(),
741-
"builtins"
742-
);
739+
assert_eq!(builtins.name().unwrap(), "builtins");
743740
})
744741
}
745742

746743
#[test]
747744
fn module_filename() {
745+
use crate::types::string::PyStringMethods;
748746
Python::with_gil(|py| {
749747
let site = PyModule::import_bound(py, "site").unwrap();
750748
assert!(site

src/types/string.rs

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,33 @@ impl<'a> PyStringData<'a> {
123123

124124
/// Represents a Python `string` (a Unicode string object).
125125
///
126-
/// This type is immutable.
126+
/// This type is only seen inside PyO3's smart pointers as [`Py<PyString>`], [`Bound<'py, PyString>`],
127+
/// and [`Borrowed<'a, 'py, PyString>`].
128+
///
129+
/// All functionality on this type is implemented through the [`PyStringMethods`] trait.
130+
///
131+
/// # Equality
132+
///
133+
/// For convenience, [`Bound<'py, PyString>`] implements [`PartialEq<str>`] to allow comparing the
134+
/// data in the Python string to a Rust UTF-8 string slice.
135+
///
136+
/// This is not always the most appropriate way to compare Python strings, as Python string subclasses
137+
/// may have different equality semantics. In situations where subclasses overriding equality might be
138+
/// relevant, use [`PyAnyMethods::eq`], at cost of the additional overhead of a Python method call.
139+
///
140+
/// ```rust
141+
/// # use pyo3::prelude::*;
142+
/// use pyo3::types::PyString;
143+
///
144+
/// # Python::with_gil(|py| {
145+
/// let py_string = PyString::new_bound(py, "foo");
146+
/// // via PartialEq<str>
147+
/// assert_eq!(py_string, "foo");
148+
///
149+
/// // via Python equality
150+
/// assert!(py_string.as_any().eq("foo").unwrap());
151+
/// # });
152+
/// ```
127153
#[repr(transparent)]
128154
pub struct PyString(PyAny);
129155

@@ -490,6 +516,118 @@ impl IntoPy<Py<PyString>> for &'_ Py<PyString> {
490516
}
491517
}
492518

519+
/// Compares whether the data in the Python string is equal to the given UTF8.
520+
///
521+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
522+
impl PartialEq<str> for Bound<'_, PyString> {
523+
#[inline]
524+
fn eq(&self, other: &str) -> bool {
525+
self.as_borrowed() == *other
526+
}
527+
}
528+
529+
/// Compares whether the data in the Python string is equal to the given UTF8.
530+
///
531+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
532+
impl PartialEq<&'_ str> for Bound<'_, PyString> {
533+
#[inline]
534+
fn eq(&self, other: &&str) -> bool {
535+
self.as_borrowed() == **other
536+
}
537+
}
538+
539+
/// Compares whether the data in the Python string is equal to the given UTF8.
540+
///
541+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
542+
impl PartialEq<Bound<'_, PyString>> for str {
543+
#[inline]
544+
fn eq(&self, other: &Bound<'_, PyString>) -> bool {
545+
*self == other.as_borrowed()
546+
}
547+
}
548+
549+
/// Compares whether the data in the Python string is equal to the given UTF8.
550+
///
551+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
552+
impl PartialEq<&'_ Bound<'_, PyString>> for str {
553+
#[inline]
554+
fn eq(&self, other: &&Bound<'_, PyString>) -> bool {
555+
*self == other.as_borrowed()
556+
}
557+
}
558+
559+
/// Compares whether the data in the Python string is equal to the given UTF8.
560+
///
561+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
562+
impl PartialEq<Bound<'_, PyString>> for &'_ str {
563+
#[inline]
564+
fn eq(&self, other: &Bound<'_, PyString>) -> bool {
565+
**self == other.as_borrowed()
566+
}
567+
}
568+
569+
/// Compares whether the data in the Python string is equal to the given UTF8.
570+
///
571+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
572+
impl PartialEq<str> for &'_ Bound<'_, PyString> {
573+
#[inline]
574+
fn eq(&self, other: &str) -> bool {
575+
self.as_borrowed() == other
576+
}
577+
}
578+
579+
/// Compares whether the data in the Python string is equal to the given UTF8.
580+
///
581+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
582+
impl PartialEq<str> for Borrowed<'_, '_, PyString> {
583+
#[inline]
584+
fn eq(&self, other: &str) -> bool {
585+
#[cfg(not(Py_3_13))]
586+
{
587+
self.to_cow().map_or(false, |s| s == other)
588+
}
589+
590+
#[cfg(Py_3_13)]
591+
unsafe {
592+
ffi::PyUnicode_EqualToUTF8AndSize(
593+
self.as_ptr(),
594+
other.as_ptr().cast(),
595+
other.len() as _,
596+
) == 1
597+
}
598+
}
599+
}
600+
601+
/// Compares whether the data in the Python string is equal to the given UTF8.
602+
///
603+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
604+
impl PartialEq<&str> for Borrowed<'_, '_, PyString> {
605+
#[inline]
606+
fn eq(&self, other: &&str) -> bool {
607+
*self == **other
608+
}
609+
}
610+
611+
/// Compares whether the data in the Python string is equal to the given UTF8.
612+
///
613+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
614+
impl PartialEq<Borrowed<'_, '_, PyString>> for str {
615+
#[inline]
616+
fn eq(&self, other: &Borrowed<'_, '_, PyString>) -> bool {
617+
other == self
618+
}
619+
}
620+
621+
/// Compares whether the data in the Python string is equal to the given UTF8.
622+
///
623+
/// In some cases Python equality might be more appropriate; see the note on [`PyString`].
624+
impl PartialEq<Borrowed<'_, '_, PyString>> for &'_ str {
625+
#[inline]
626+
fn eq(&self, other: &Borrowed<'_, '_, PyString>) -> bool {
627+
other == self
628+
}
629+
}
630+
493631
#[cfg(test)]
494632
mod tests {
495633
use super::*;
@@ -708,15 +846,15 @@ mod tests {
708846
fn test_intern_string() {
709847
Python::with_gil(|py| {
710848
let py_string1 = PyString::intern_bound(py, "foo");
711-
assert_eq!(py_string1.to_cow().unwrap(), "foo");
849+
assert_eq!(py_string1, "foo");
712850

713851
let py_string2 = PyString::intern_bound(py, "foo");
714-
assert_eq!(py_string2.to_cow().unwrap(), "foo");
852+
assert_eq!(py_string2, "foo");
715853

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

718856
let py_string3 = PyString::intern_bound(py, "bar");
719-
assert_eq!(py_string3.to_cow().unwrap(), "bar");
857+
assert_eq!(py_string3, "bar");
720858

721859
assert_ne!(py_string1.as_ptr(), py_string3.as_ptr());
722860
});
@@ -762,4 +900,34 @@ mod tests {
762900
assert_eq!(py_string.to_string_lossy(py), "🐈 Hello ���World");
763901
})
764902
}
903+
904+
#[test]
905+
fn test_comparisons() {
906+
Python::with_gil(|py| {
907+
let s = "hello, world";
908+
let py_string = PyString::new_bound(py, s);
909+
910+
assert_eq!(py_string, "hello, world");
911+
912+
assert_eq!(py_string, s);
913+
assert_eq!(&py_string, s);
914+
assert_eq!(s, py_string);
915+
assert_eq!(s, &py_string);
916+
917+
assert_eq!(py_string, *s);
918+
assert_eq!(&py_string, *s);
919+
assert_eq!(*s, py_string);
920+
assert_eq!(*s, &py_string);
921+
922+
let py_string = py_string.as_borrowed();
923+
924+
assert_eq!(py_string, s);
925+
assert_eq!(&py_string, s);
926+
assert_eq!(s, py_string);
927+
assert_eq!(s, &py_string);
928+
929+
assert_eq!(py_string, *s);
930+
assert_eq!(*s, py_string);
931+
})
932+
}
765933
}

tests/test_proto_methods.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,15 @@ fn test_delattr() {
131131
fn test_str() {
132132
Python::with_gil(|py| {
133133
let example_py = make_example(py);
134-
assert_eq!(example_py.str().unwrap().to_cow().unwrap(), "5");
134+
assert_eq!(example_py.str().unwrap(), "5");
135135
})
136136
}
137137

138138
#[test]
139139
fn test_repr() {
140140
Python::with_gil(|py| {
141141
let example_py = make_example(py);
142-
assert_eq!(
143-
example_py.repr().unwrap().to_cow().unwrap(),
144-
"ExampleClass(value=5)"
145-
);
142+
assert_eq!(example_py.repr().unwrap(), "ExampleClass(value=5)");
146143
})
147144
}
148145

0 commit comments

Comments
 (0)