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

Char support #1282

Merged
merged 5 commits into from
Nov 20, 2020
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Add FFI definitions for PEP 587 "Python Initialization Configuration". [#1247](https://github.com/PyO3/pyo3/pull/1247)
- Add `PyEval_SetProfile` and `PyEval_SetTrace` to FFI. [#1255](https://github.com/PyO3/pyo3/pull/1255)
- Add context.h functions (`PyContext_New`, etc) to FFI. [#1259](https://github.com/PyO3/pyo3/pull/1259)
- Add support for conversion between `char` and `PyString`. [#1282](https://github.com/PyO3/pyo3/pull/1282)

### Changed
- Change return type `PyType::name()` from `Cow<str>` to `PyResult<&str>`. [#1152](https://github.com/PyO3/pyo3/pull/1152)
Expand Down
5 changes: 1 addition & 4 deletions src/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,7 @@ pub trait FromPyObject<'source>: Sized {

/// Identity conversion: allows using existing `PyObject` instances where
/// `T: ToPyObject` is expected.
impl<'a, T: ?Sized> ToPyObject for &'a T
where
T: ToPyObject,
{
impl<T: ?Sized + ToPyObject> ToPyObject for &'_ T {
#[inline]
fn to_object(&self, py: Python) -> PyObject {
<T as ToPyObject>::to_object(*self, py)
Expand Down
148 changes: 99 additions & 49 deletions src/types/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,19 @@ impl ToPyObject for String {
}
}

impl ToPyObject for char {
fn to_object(&self, py: Python) -> PyObject {
self.into_py(py)
}
}

impl IntoPy<PyObject> for char {
fn into_py(self, py: Python) -> PyObject {
let mut bytes = [0u8; 4];
PyString::new(py, self.encode_utf8(&mut bytes)).into()
}
}

impl IntoPy<PyObject> for String {
fn into_py(self, py: Python) -> PyObject {
PyString::new(py, &self).into()
Expand All @@ -156,22 +169,36 @@ impl<'a> IntoPy<PyObject> for &'a String {

/// Allows extracting strings from Python objects.
/// Accepts Python `str` and `unicode` objects.
impl<'source> crate::FromPyObject<'source> for &'source str {
impl<'source> FromPyObject<'source> for &'source str {
fn extract(ob: &'source PyAny) -> PyResult<Self> {
<PyString as PyTryFrom>::try_from(ob)?.to_str()
}
}

/// Allows extracting strings from Python objects.
/// Accepts Python `str` and `unicode` objects.
impl<'source> FromPyObject<'source> for String {
fn extract(obj: &'source PyAny) -> PyResult<Self> {
impl FromPyObject<'_> for String {
fn extract(obj: &PyAny) -> PyResult<Self> {
<PyString as PyTryFrom>::try_from(obj)?
.to_str()
.map(ToOwned::to_owned)
}
}

impl FromPyObject<'_> for char {
fn extract(obj: &PyAny) -> PyResult<Self> {
let s = PyString::try_from(obj)?.to_str()?;
let mut iter = s.chars();
if let (Some(ch), None) = (iter.next(), iter.next()) {
Ok(ch)
} else {
Err(crate::exceptions::PyValueError::new_err(
"expected a string of length 1",
))
}
}
}

#[cfg(test)]
mod test {
use super::PyString;
Expand All @@ -180,80 +207,103 @@ mod test {

#[test]
fn test_non_bmp() {
let gil = Python::acquire_gil();
let py = gil.python();
let s = "\u{1F30F}";
let py_string = s.to_object(py);
assert_eq!(s, py_string.extract::<String>(py).unwrap());
Python::with_gil(|py| {
let s = "\u{1F30F}";
let py_string = s.to_object(py);
assert_eq!(s, py_string.extract::<String>(py).unwrap());
})
}

#[test]
fn test_extract_str() {
let gil = Python::acquire_gil();
let py = gil.python();
let s = "Hello Python";
let py_string = s.to_object(py);
Python::with_gil(|py| {
let s = "Hello Python";
let py_string = s.to_object(py);

let s2: &str = FromPyObject::extract(py_string.as_ref(py)).unwrap();
assert_eq!(s, s2);
})
}

let s2: &str = FromPyObject::extract(py_string.as_ref(py)).unwrap();
assert_eq!(s, s2);
#[test]
fn test_extract_char() {
Python::with_gil(|py| {
let ch = '😃';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😃

let py_string = ch.to_object(py);
let ch2: char = FromPyObject::extract(py_string.as_ref(py)).unwrap();
assert_eq!(ch, ch2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test for the error case (string with len > 1)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 good idea

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

})
}

#[test]
fn test_extract_char_err() {
Python::with_gil(|py| {
let s = "Hello Python";
let py_string = s.to_object(py);
let err: crate::PyResult<char> = FromPyObject::extract(py_string.as_ref(py));
assert!(err
.unwrap_err()
.to_string()
.contains("expected a string of length 1"));
})
}

#[test]
fn test_to_str_ascii() {
let gil = Python::acquire_gil();
let py = gil.python();
let s = "ascii 🐈";
let obj: PyObject = PyString::new(py, s).into();
let py_string = <PyString as PyTryFrom>::try_from(obj.as_ref(py)).unwrap();
assert_eq!(s, py_string.to_str().unwrap());
Python::with_gil(|py| {
let s = "ascii 🐈";
let obj: PyObject = PyString::new(py, s).into();
let py_string = <PyString as PyTryFrom>::try_from(obj.as_ref(py)).unwrap();
assert_eq!(s, py_string.to_str().unwrap());
})
}

#[test]
fn test_to_str_surrogate() {
let gil = Python::acquire_gil();
let py = gil.python();
let obj: PyObject = py.eval(r#"'\ud800'"#, None, None).unwrap().into();
let py_string = <PyString as PyTryFrom>::try_from(obj.as_ref(py)).unwrap();
assert!(py_string.to_str().is_err());
Python::with_gil(|py| {
let obj: PyObject = py.eval(r#"'\ud800'"#, None, None).unwrap().into();
let py_string = <PyString as PyTryFrom>::try_from(obj.as_ref(py)).unwrap();
assert!(py_string.to_str().is_err());
})
}

#[test]
fn test_to_str_unicode() {
let gil = Python::acquire_gil();
let py = gil.python();
let s = "哈哈🐈";
let obj: PyObject = PyString::new(py, s).into();
let py_string = <PyString as PyTryFrom>::try_from(obj.as_ref(py)).unwrap();
assert_eq!(s, py_string.to_str().unwrap());
Python::with_gil(|py| {
let s = "哈哈🐈";
let obj: PyObject = PyString::new(py, s).into();
let py_string = <PyString as PyTryFrom>::try_from(obj.as_ref(py)).unwrap();
assert_eq!(s, py_string.to_str().unwrap());
})
}

#[test]
fn test_to_string_lossy() {
let gil = Python::acquire_gil();
let py = gil.python();
let obj: PyObject = py
.eval(r#"'🐈 Hello \ud800World'"#, None, None)
.unwrap()
.into();
let py_string = <PyString as PyTryFrom>::try_from(obj.as_ref(py)).unwrap();
assert_eq!(py_string.to_string_lossy(), "🐈 Hello ���World");
Python::with_gil(|py| {
let obj: PyObject = py
.eval(r#"'🐈 Hello \ud800World'"#, None, None)
.unwrap()
.into();
let py_string = <PyString as PyTryFrom>::try_from(obj.as_ref(py)).unwrap();
assert_eq!(py_string.to_string_lossy(), "🐈 Hello ���World");
})
}

#[test]
fn test_debug_string() {
let gil = Python::acquire_gil();
let py = gil.python();
let v = "Hello\n".to_object(py);
let s = <PyString as PyTryFrom>::try_from(v.as_ref(py)).unwrap();
assert_eq!(format!("{:?}", s), "'Hello\\n'");
Python::with_gil(|py| {
let v = "Hello\n".to_object(py);
let s = <PyString as PyTryFrom>::try_from(v.as_ref(py)).unwrap();
assert_eq!(format!("{:?}", s), "'Hello\\n'");
})
}

#[test]
fn test_display_string() {
let gil = Python::acquire_gil();
let py = gil.python();
let v = "Hello\n".to_object(py);
let s = <PyString as PyTryFrom>::try_from(v.as_ref(py)).unwrap();
assert_eq!(format!("{}", s), "Hello\n");
Python::with_gil(|py| {
let v = "Hello\n".to_object(py);
let s = <PyString as PyTryFrom>::try_from(v.as_ref(py)).unwrap();
assert_eq!(format!("{}", s), "Hello\n");
})
}
}