From 830c68648cf2529bab5a2c22c3fa6d08f0bbb07c Mon Sep 17 00:00:00 2001 From: kngwyu Date: Fri, 20 Nov 2020 01:31:30 +0900 Subject: [PATCH 1/5] Support conversion between char and PyString --- src/conversion.rs | 5 +---- src/types/string.rs | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index e5716429b50..5256d5e1cdb 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -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<'a, T: ?Sized + ToPyObject> ToPyObject for &'a T { #[inline] fn to_object(&self, py: Python) -> PyObject { ::to_object(*self, py) diff --git a/src/types/string.rs b/src/types/string.rs index 2bb5e3cd290..4d110c59260 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -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 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 for String { fn into_py(self, py: Python) -> PyObject { PyString::new(py, &self).into() @@ -156,7 +169,7 @@ impl<'a> IntoPy 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 { ::try_from(ob)?.to_str() } @@ -172,6 +185,20 @@ impl<'source> FromPyObject<'source> for String { } } +impl<'source> FromPyObject<'source> for char { + fn extract(obj: &'source PyAny) -> PyResult { + 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(format!( + "Expected a sting of length 1", + ))) + } + } +} + #[cfg(test)] mod test { use super::PyString; @@ -198,6 +225,16 @@ mod test { 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_to_str_ascii() { let gil = Python::acquire_gil(); From e98ad64f09844a63baf42a14271fdda08d539b42 Mon Sep 17 00:00:00 2001 From: kngwyu Date: Fri, 20 Nov 2020 01:33:25 +0900 Subject: [PATCH 2/5] Use Python::with_gil in test codes in string.rs --- src/types/string.rs | 92 ++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/types/string.rs b/src/types/string.rs index 4d110c59260..af5db1d2cc6 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -207,22 +207,22 @@ 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::(py).unwrap()); + Python::with_gil(|py| { + let s = "\u{1F30F}"; + let py_string = s.to_object(py); + assert_eq!(s, py_string.extract::(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] @@ -237,60 +237,60 @@ mod test { #[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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::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 = ::try_from(v.as_ref(py)).unwrap(); + assert_eq!(format!("{}", s), "Hello\n"); + }) } } From 3435014188c4056ed822aeb7d4a9e646c0638b92 Mon Sep 17 00:00:00 2001 From: kngwyu Date: Fri, 20 Nov 2020 14:40:04 +0900 Subject: [PATCH 3/5] Add a failure case test for extracting char --- src/types/string.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/types/string.rs b/src/types/string.rs index af5db1d2cc6..e8f20aba9bb 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -192,9 +192,9 @@ impl<'source> FromPyObject<'source> for char { if let (Some(ch), None) = (iter.next(), iter.next()) { Ok(ch) } else { - Err(crate::exceptions::PyValueError::new_err(format!( - "Expected a sting of length 1", - ))) + Err(crate::exceptions::PyValueError::new_err( + "expected a string of length 1", + )) } } } @@ -235,6 +235,19 @@ mod test { }) } + #[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 = 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() { Python::with_gil(|py| { From 7d217d2f1f8b5200307646cf9f83a5e22f8be61a Mon Sep 17 00:00:00 2001 From: kngwyu Date: Fri, 20 Nov 2020 14:55:24 +0900 Subject: [PATCH 4/5] Use some more anonymous lifetimes --- src/conversion.rs | 2 +- src/types/string.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 5256d5e1cdb..68c0cd9cf76 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -174,7 +174,7 @@ pub trait FromPyObject<'source>: Sized { /// Identity conversion: allows using existing `PyObject` instances where /// `T: ToPyObject` is expected. -impl<'a, T: ?Sized + ToPyObject> ToPyObject for &'a T { +impl ToPyObject for &'_ T { #[inline] fn to_object(&self, py: Python) -> PyObject { ::to_object(*self, py) diff --git a/src/types/string.rs b/src/types/string.rs index e8f20aba9bb..ee5d3314504 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -177,16 +177,16 @@ impl<'source> FromPyObject<'source> for &'source 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 { +impl FromPyObject<'_> for String { + fn extract(obj: &PyAny) -> PyResult { ::try_from(obj)? .to_str() .map(ToOwned::to_owned) } } -impl<'source> FromPyObject<'source> for char { - fn extract(obj: &'source PyAny) -> PyResult { +impl FromPyObject<'_> for char { + fn extract(obj: &PyAny) -> PyResult { let s = PyString::try_from(obj)?.to_str()?; let mut iter = s.chars(); if let (Some(ch), None) = (iter.next(), iter.next()) { From a2490e32ce8f6b40489f46fb1a653132ac79152f Mon Sep 17 00:00:00 2001 From: kngwyu Date: Fri, 20 Nov 2020 16:39:05 +0900 Subject: [PATCH 5/5] Add a CHANGELOG entry for char support --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45435985caf..8323bc89235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` to `PyResult<&str>`. [#1152](https://github.com/PyO3/pyo3/pull/1152)