Repository: WorksApplications/sudachi.rs (language: python)

Issue:
Provide more user-friendly __repr__ and __str__ for Morpheme/MorphemeList/Dictionary/Tokenizer



Discussion / hints:

Fix patch:
diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md
index 688dafa3..42a1e806 100644
--- a/python/CHANGELOG.md
+++ b/python/CHANGELOG.md
@@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
   - `sudachipy.Tokenizer` will ignore the provided logger.
   - Ref: [#76]
 - `Morpheme.part_of_speech` method now returns Tuple of POS components instead of a list.
+- `Morpheme`/`MorphemeList` now have readable `__repr__` and `__str__`
+  - https://github.com/WorksApplications/sudachi.rs/pull/187
 
 ## [0.6.0] - 2021/10/11
 
diff --git a/python/src/morpheme.rs b/python/src/morpheme.rs
index 237b6bf0..99d273d5 100644
--- a/python/src/morpheme.rs
+++ b/python/src/morpheme.rs
@@ -14,10 +14,11 @@
  *  limitations under the License.
  */
 
+use std::fmt::Write;
 use std::ops::Deref;
 use std::sync::Arc;
 
-use pyo3::exceptions::{self, PyException};
+use pyo3::exceptions::{self, PyException, PyIndexError};
 use pyo3::prelude::*;
 use pyo3::types::{PyList, PyString, PyTuple, PyType};
 
@@ -107,8 +108,8 @@ impl PyMorphemeListWrapper {
         }
 
         if idx < 0 || len <= idx {
-            return Err(PyErr::new::<exceptions::PyIndexError, _>(format!(
-                "morphemelist index out of range: the len is {} but the index is {}",
+            return Err(PyIndexError::new_err(format!(
+                "MorphemeList index out of range: the len is {} but the index is {}",
                 list.size(py),
                 idx
             )));
@@ -123,7 +124,38 @@ impl PyMorphemeListWrapper {
     }
 
     fn __str__<'py>(&'py self, py: Python<'py>) -> &PyString {
-        PyString::new(py, self.internal(py).surface().deref())
+        // do a simple tokenization __str__
+        let list = self.internal(py);
+        let mut result = String::with_capacity(list.surface().len() * 2);
+        let nmorphs = list.len();
+        for (i, m) in list.iter().enumerate() {
+            result.push_str(m.surface().deref());
+            if i + 1 != nmorphs {
+                result.push_str(" ");
+            }
+        }
+        PyString::new(py, result.as_str())
+    }
+
+    fn __repr__(slf: Py<PyMorphemeListWrapper>, py: Python) -> PyResult<&PyString> {
+        let self_ref = slf.borrow(py);
+        let list = self_ref.internal(py);
+        let mut result = String::with_capacity(list.surface().len() * 10);
+        result.push_str("<MorphemeList[\n");
+        let nmorphs = list.len();
+        for i in 0..nmorphs {
+            result.push_str("  ");
+            let pymorph = PyMorpheme {
+                list: slf.clone_ref(py),
+                index: i,
+            };
+            pymorph
+                .write_repr(py, &mut result)
+                .map_err(|_| PyException::new_err("format failed"))?;
+            result.push_str(",\n");
+        }
+        result.push_str("]>");
+        Ok(PyString::new(py, result.as_str()))
     }
 
     fn __iter__(slf: Py<Self>) -> PyMorphemeIter {
@@ -207,14 +239,24 @@ impl PyMorpheme {
         let morph = unsafe { std::mem::transmute(list.internal(py).get(self.index)) };
         MorphemeRef { list, morph }
     }
+
+    fn write_repr<'py, W: Write>(&'py self, py: Python<'py>, out: &mut W) -> std::fmt::Result {
+        // per https://github.com/WorksApplications/SudachiPy/pull/166#issuecomment-932043063
+        let mrp = self.morph(py);
+        let surf = mrp.surface();
+        write!(
+            out,
+            "<Morpheme({}, {}:{}, {})>",
+            surf.deref(),
+            mrp.begin_c(),
+            mrp.end_c(),
+            mrp.word_id()
+        )
+    }
 }
 
 #[pymethods]
 impl PyMorpheme {
-    fn __str__<'py>(&'py self, py: Python<'py>) -> PyObject {
-        self.surface(py)
-    }
-
     /// Returns the begin index of this in the input text
     #[pyo3(text_signature = "($self)")]
     fn begin(&self, py: Python) -> usize {
@@ -358,4 +400,15 @@ impl PyMorpheme {
         let m = self.morph(py);
         m.end_c() - m.begin_c()
     }
+
+    pub fn __str__<'py>(&'py self, py: Python<'py>) -> PyObject {
+        self.surface(py)
+    }
+
+    pub fn __repr__<'py>(&'py self, py: Python<'py>) -> PyResult<String> {
+        let mut result = String::new();
+        self.write_repr(py, &mut result)
+            .map_err(|_| PyException::new_err("failed to format repr"))?;
+        Ok(result)
+    }
 }
diff --git a/python/src/pos_matcher.rs b/python/src/pos_matcher.rs
index 26182957..8ac83d06 100644
--- a/python/src/pos_matcher.rs
+++ b/python/src/pos_matcher.rs
@@ -121,7 +121,7 @@ impl PyPosMatcher {
     }
 
     pub fn __str__(&self) -> String {
-        format!("PosMatcher<{} pos>", self.matcher.num_entries())
+        format!("<PosMatcher:{} pos>", self.matcher.num_entries())
     }
 
     pub fn __iter__(&self) -> PyPosIter {


Test patch:
diff --git a/python/tests/test_morpheme.py b/python/tests/test_morpheme.py
index 243a634c..66928117 100644
--- a/python/tests/test_morpheme.py
+++ b/python/tests/test_morpheme.py
@@ -148,6 +148,16 @@ def test_morpheme_split_out(self):
         self.assertEqual(ms_a[0].surface(), '東京')
         self.assertEqual(ms_a[1].surface(), '都')
 
+    def test_morpheme_str_repr(self):
+        ms = self.tokenizer_obj.tokenize('東京都', SplitMode.A)
+        self.assertEqual(2, ms.size())
+        self.assertEqual(str(ms), '東京 都')
+        self.assertEqual(repr(ms), '<MorphemeList[\n  <Morpheme(東京, 0:2, (0, 5))>,\n  <Morpheme(都, 2:3, (0, 9))>,\n]>')
+        self.assertEqual(str(ms[0]), '東京')
+        self.assertEqual(str(ms[1]), '都')
+        self.assertEqual(repr(ms[0]), '<Morpheme(東京, 0:2, (0, 5))>')
+        self.assertEqual(repr(ms[1]), '<Morpheme(都, 2:3, (0, 9))>')
+
 
 if __name__ == '__main__':
     unittest.main()