Skip to content

Commit

Permalink
Merge pull request #1282 from PyO3/char-support
Browse files Browse the repository at this point in the history
Char support
  • Loading branch information
kngwyu authored Nov 20, 2020
2 parents 096b0a3 + a2490e3 commit c621b70
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 53 deletions.
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 = '😃';
let py_string = ch.to_object(py);
let ch2: char = FromPyObject::extract(py_string.as_ref(py)).unwrap();
assert_eq!(ch, ch2);
})
}

#[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");
})
}
}

0 comments on commit c621b70

Please sign in to comment.