From 97e711ae4b594d789f53671c815fbc4bb563a27c Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Fri, 13 Oct 2023 02:44:38 +0900 Subject: [PATCH] =?UTF-8?q?Python=20API=E3=81=A8Java=20API=E3=81=AE?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92=E8=A9=B3=E7=B4=B0=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/test_pseudo_raii_for_synthesizer.py | 6 +- .../python/test/test_user_dict_manipulate.py | 3 +- .../python/voicevox_core/__init__.py | 38 ++++- .../python/voicevox_core/_rust.pyi | 89 +++++++++- .../voicevox_core_python_api/src/convert.rs | 93 +++++++++-- crates/voicevox_core_python_api/src/lib.rs | 152 +++++++++++------- 6 files changed, 302 insertions(+), 79 deletions(-) diff --git a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py index 165770dab..6c6b15355 100644 --- a/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py +++ b/crates/voicevox_core_python_api/python/test/test_pseudo_raii_for_synthesizer.py @@ -5,7 +5,7 @@ import conftest import pytest import pytest_asyncio -from voicevox_core import OpenJtalk, Synthesizer, VoicevoxError +from voicevox_core import OpenJtalk, Synthesizer def test_enter_returns_workable_self(synthesizer: Synthesizer) -> None: @@ -24,14 +24,14 @@ def test_closing_multiple_times_is_allowed(synthesizer: Synthesizer) -> None: def test_access_after_close_denied(synthesizer: Synthesizer) -> None: synthesizer.close() - with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): + with pytest.raises(ValueError, match="^The `Synthesizer` is closed$"): _ = synthesizer.metas def test_access_after_exit_denied(synthesizer: Synthesizer) -> None: with synthesizer: pass - with pytest.raises(VoicevoxError, match="^The `Synthesizer` is closed$"): + with pytest.raises(ValueError, match="^The `Synthesizer` is closed$"): _ = synthesizer.metas diff --git a/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py b/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py index f7902ea94..c283b48f1 100644 --- a/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py +++ b/crates/voicevox_core_python_api/python/test/test_user_dict_manipulate.py @@ -5,6 +5,7 @@ import tempfile from uuid import UUID +import pydantic import pytest import voicevox_core # noqa: F401 @@ -69,7 +70,7 @@ async def test_user_dict_load() -> None: assert uuid_c in dict_a.words # 単語のバリデーション - with pytest.raises(voicevox_core.VoicevoxError): + with pytest.raises(pydantic.ValidationError): dict_a.add_word( voicevox_core.UserDictWord( surface="", diff --git a/crates/voicevox_core_python_api/python/voicevox_core/__init__.py b/crates/voicevox_core_python_api/python/voicevox_core/__init__.py index ce6245bfc..abcd763fe 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/__init__.py +++ b/crates/voicevox_core_python_api/python/voicevox_core/__init__.py @@ -12,11 +12,28 @@ UserDictWordType, ) from ._rust import ( # noqa: F401 + ExtractFullContextLabelError, + GetSupportedDevicesError, + GpuSupportError, + InferenceFailedError, + InvalidModelDataError, + InvalidWordError, + LoadUserDictError, + ModelAlreadyLoadedError, + ModelNotFoundError, + NotLoadedOpenjtalkDictError, OpenJtalk, + OpenZipFileError, + ParseKanaError, + ReadZipEntryError, + SaveUserDictError, + StyleAlreadyLoadedError, + StyleNotFoundError, Synthesizer, UserDict, + UseUserDictError, VoiceModel, - VoicevoxError, + WordNotFoundError, __version__, supported_devices, ) @@ -26,15 +43,32 @@ "AccelerationMode", "AccentPhrase", "AudioQuery", + "ExtractFullContextLabelError", + "GetSupportedDevicesError", + "GpuSupportError", + "InferenceFailedError", + "InvalidModelDataError", + "InvalidWordError", + "LoadUserDictError", + "ModelAlreadyLoadedError", + "ModelNotFoundError", "Mora", + "NotLoadedOpenjtalkDictError", "OpenJtalk", + "OpenZipFileError", + "ParseKanaError", + "ReadZipEntryError", + "SaveUserDictError", "SpeakerMeta", + "StyleAlreadyLoadedError", + "StyleNotFoundError", "SupportedDevices", "Synthesizer", - "VoicevoxError", "VoiceModel", "supported_devices", + "UseUserDictError", "UserDict", "UserDictWord", "UserDictWordType", + "WordNotFoundError", ] diff --git a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi b/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi index b4e44c8e2..734bfb321 100644 --- a/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi +++ b/crates/voicevox_core_python_api/python/voicevox_core/_rust.pyi @@ -426,8 +426,93 @@ class UserDict: """ ... -class VoicevoxError(Exception): - """VOICEVOX COREのエラー。""" +class NotLoadedOpenjtalkDictError(Exception): + """open_jtalk辞書ファイルが読み込まれていない。""" + + ... + +class GpuSupportError(Exception): + """GPUモードがサポートされていない。""" + + ... + +class OpenZipFileError(Exception): + """ZIPファイルを開くことに失敗した。""" + + ... + +class ReadZipEntryError(Exception): + """ZIP内のファイルが読めなかった。""" + + ... + +class ModelAlreadyLoadedError(Exception): + """すでに読み込まれている音声モデルを読み込もうとした。""" + + ... + +class StyleAlreadyLoadedError(Exception): + """すでに読み込まれているスタイルを読み込もうとした。""" + + ... + +class InvalidModelDataError(Exception): + """無効なモデルデータ。""" + + ... + +class GetSupportedDevicesError(Exception): + """サポートされているデバイス情報取得に失敗した。""" + + ... + +class StyleNotFoundError(Exception): + """スタイルIDに対するスタイルが見つからなかった。""" + + ... + +class ModelNotFoundError(Exception): + """音声モデルIDに対する音声モデルが見つからなかった。""" + + ... + +class InferenceFailedError(Exception): + """推論に失敗した。""" + + ... + +class ExtractFullContextLabelError(Exception): + """コンテキストラベル出力に失敗した。""" + + ... + +class ParseKanaError(ValueError): + """AquesTalk風記法のテキストの解析に失敗した。""" + + ... + +class LoadUserDictError(Exception): + """ユーザー辞書を読み込めなかった。""" + + ... + +class SaveUserDictError(Exception): + """ユーザー辞書を書き込めなかった。""" + + ... + +class WordNotFoundError(Exception): + """ユーザー辞書に単語が見つからなかった。""" + + ... + +class UseUserDictError(Exception): + """OpenJTalkのユーザー辞書の設定に失敗した。""" + + ... + +class InvalidWordError(ValueError): + """ユーザー辞書の単語のバリデーションに失敗した。""" ... diff --git a/crates/voicevox_core_python_api/src/convert.rs b/crates/voicevox_core_python_api/src/convert.rs index 5eff85fb4..d4f9cb0d7 100644 --- a/crates/voicevox_core_python_api/src/convert.rs +++ b/crates/voicevox_core_python_api/src/convert.rs @@ -1,8 +1,11 @@ -use crate::VoicevoxError; -use std::{fmt::Display, future::Future, path::PathBuf}; +use std::{error::Error as _, future::Future, iter, path::PathBuf}; use easy_ext::ext; -use pyo3::{types::PyList, FromPyObject as _, PyAny, PyObject, PyResult, Python, ToPyObject}; +use pyo3::{ + exceptions::{PyException, PyValueError}, + types::PyList, + FromPyObject as _, PyAny, PyObject, PyResult, Python, ToPyObject, +}; use serde::{de::DeserializeOwned, Serialize}; use serde_json::json; use uuid::Uuid; @@ -10,6 +13,14 @@ use voicevox_core::{ AccelerationMode, AccentPhraseModel, StyleId, UserDictWordType, VoiceModelMeta, }; +use crate::{ + ExtractFullContextLabelError, GetSupportedDevicesError, GpuSupportError, InferenceFailedError, + InvalidModelDataError, InvalidWordError, LoadUserDictError, ModelAlreadyLoadedError, + ModelNotFoundError, NotLoadedOpenjtalkDictError, OpenZipFileError, ParseKanaError, + ReadZipEntryError, SaveUserDictError, StyleAlreadyLoadedError, StyleNotFoundError, + UseUserDictError, WordNotFoundError, +}; + pub fn from_acceleration_mode(ob: &PyAny) -> PyResult { let py = ob.py(); @@ -31,7 +42,7 @@ pub fn from_utf8_path(ob: &PyAny) -> PyResult { PathBuf::extract(ob)? .into_os_string() .into_string() - .map_err(|s| VoicevoxError::new_err(format!("{s:?} cannot be encoded to UTF-8"))) + .map_err(|s| PyValueError::new_err(format!("{s:?} cannot be encoded to UTF-8"))) } pub fn from_dataclass(ob: &PyAny) -> PyResult { @@ -42,7 +53,7 @@ pub fn from_dataclass(ob: &PyAny) -> PyResult { .import("json")? .call_method1("dumps", (ob,))? .extract::()?; - serde_json::from_str(json).into_py_result() + serde_json::from_str(json).into_py_value_result() } pub fn to_pydantic_voice_model_meta<'py>( @@ -63,7 +74,7 @@ pub fn to_pydantic_voice_model_meta<'py>( pub fn to_pydantic_dataclass(x: impl Serialize, class: &PyAny) -> PyResult<&PyAny> { let py = class.py(); - let x = serde_json::to_string(&x).into_py_result()?; + let x = serde_json::to_string(&x).into_py_value_result()?; let x = py.import("json")?.call_method1("loads", (x,))?.downcast()?; class.call((), Some(x)) } @@ -86,11 +97,10 @@ where py, pyo3_asyncio::tokio::get_current_locals(py)?, async move { - let replaced_accent_phrases = method(rust_accent_phrases, speaker_id) - .await - .into_py_result()?; + let replaced_accent_phrases = method(rust_accent_phrases, speaker_id).await; Python::with_gil(|py| { let replaced_accent_phrases = replaced_accent_phrases + .into_py_result(py)? .iter() .map(move |accent_phrase| { to_pydantic_dataclass( @@ -107,7 +117,7 @@ where } pub fn to_rust_uuid(ob: &PyAny) -> PyResult { let uuid = ob.getattr("hex")?.extract::()?; - uuid.parse().into_py_result() + uuid.parse::().into_py_value_result() } pub fn to_py_uuid(py: Python, uuid: Uuid) -> PyResult { let uuid = uuid.hyphenated().to_string(); @@ -122,7 +132,7 @@ pub fn to_rust_user_dict_word(ob: &PyAny) -> PyResult( py: Python<'py>, @@ -137,12 +147,65 @@ pub fn to_py_user_dict_word<'py>( pub fn to_rust_word_type(word_type: &PyAny) -> PyResult { let name = word_type.getattr("name")?.extract::()?; - serde_json::from_value::(json!(name)).into_py_result() + serde_json::from_value::(json!(name)).into_py_value_result() +} + +#[ext] +pub impl voicevox_core::Result { + fn into_py_result(self, py: Python<'_>) -> PyResult { + use voicevox_core::ErrorKind; + + self.map_err(|err| { + let msg = err.to_string(); + let top = match err.kind() { + ErrorKind::NotLoadedOpenjtalkDict => NotLoadedOpenjtalkDictError::new_err(msg), + ErrorKind::GpuSupport => GpuSupportError::new_err(msg), + ErrorKind::OpenZipFile => OpenZipFileError::new_err(msg), + ErrorKind::ReadZipEntry => ReadZipEntryError::new_err(msg), + ErrorKind::ModelAlreadyLoaded => ModelAlreadyLoadedError::new_err(msg), + ErrorKind::StyleAlreadyLoaded => StyleAlreadyLoadedError::new_err(msg), + ErrorKind::InvalidModelData => InvalidModelDataError::new_err(msg), + ErrorKind::GetSupportedDevices => GetSupportedDevicesError::new_err(msg), + ErrorKind::StyleNotFound => StyleNotFoundError::new_err(msg), + ErrorKind::ModelNotFound => ModelNotFoundError::new_err(msg), + ErrorKind::InferenceFailed => InferenceFailedError::new_err(msg), + ErrorKind::ExtractFullContextLabel => ExtractFullContextLabelError::new_err(msg), + ErrorKind::ParseKana => ParseKanaError::new_err(msg), + ErrorKind::LoadUserDict => LoadUserDictError::new_err(msg), + ErrorKind::SaveUserDict => SaveUserDictError::new_err(msg), + ErrorKind::WordNotFound => WordNotFoundError::new_err(msg), + ErrorKind::UseUserDict => UseUserDictError::new_err(msg), + ErrorKind::InvalidWord => InvalidWordError::new_err(msg), + }; + + [top] + .into_iter() + .chain( + iter::successors(err.source(), |&source| source.source()) + .map(|source| PyException::new_err(source.to_string())), + ) + .collect::>() + .into_iter() + .rev() + .reduce(|prev, source| { + source.set_cause(py, Some(prev)); + source + }) + .expect("should not be empty") + }) + } +} + +#[ext] +impl std::result::Result { + fn into_py_value_result(self) -> PyResult { + self.map_err(|e| PyValueError::new_err(e.to_string())) + } } #[ext] -pub impl Result { - fn into_py_result(self) -> PyResult { - self.map_err(|e| VoicevoxError::new_err(e.to_string())) +impl serde_json::Result { + fn into_py_value_result(self) -> PyResult { + self.map_err(|e| PyValueError::new_err(e.to_string())) } } diff --git a/crates/voicevox_core_python_api/src/lib.rs b/crates/voicevox_core_python_api/src/lib.rs index 42d3feaeb..e3165189f 100644 --- a/crates/voicevox_core_python_api/src/lib.rs +++ b/crates/voicevox_core_python_api/src/lib.rs @@ -6,7 +6,7 @@ use log::debug; use once_cell::sync::Lazy; use pyo3::{ create_exception, - exceptions::PyException, + exceptions::{PyException, PyValueError}, pyclass, pyfunction, pymethods, pymodule, types::{IntoPyDict as _, PyBytes, PyDict, PyList, PyModule}, wrap_pyfunction, PyAny, PyObject, PyRef, PyResult, PyTypeInfo, Python, ToPyObject, @@ -22,7 +22,7 @@ static RUNTIME: Lazy = Lazy::new(|| Runtime::new().unwrap()); #[pymodule] #[pyo3(name = "_rust")] -fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { +fn rust(_: Python<'_>, module: &PyModule) -> PyResult<()> { pyo3_log::init(); module.add("__version__", env!("CARGO_PKG_VERSION"))?; @@ -34,16 +34,45 @@ fn rust(py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; - module.add("VoicevoxError", py.get_type::())?; - Ok(()) + + add_exceptions(module) } -create_exception!( - voicevox_core, - VoicevoxError, - PyException, - "voicevox_core Error." -); +macro_rules! exceptions { + ($($name:ident: $base:ty;)*) => { + $( + create_exception!(voicevox_core, $name, $base); + )* + + fn add_exceptions(module: &PyModule) -> PyResult<()> { + $( + module.add(stringify!($name), module.py().get_type::<$name>())?; + )* + Ok(()) + } + }; +} + +exceptions! { + NotLoadedOpenjtalkDictError: PyException; + GpuSupportError: PyException; + OpenZipFileError: PyException; + ReadZipEntryError: PyException; + ModelAlreadyLoadedError: PyException; + StyleAlreadyLoadedError: PyException; + InvalidModelDataError: PyException; + GetSupportedDevicesError: PyException; + StyleNotFoundError: PyException; + ModelNotFoundError: PyException; + InferenceFailedError: PyException; + ExtractFullContextLabelError: PyException; + ParseKanaError: PyValueError; + LoadUserDictError: PyException; + SaveUserDictError: PyException; + WordNotFoundError: PyException; + UseUserDictError: PyException; + InvalidWordError: PyValueError; +} #[pyclass] #[derive(Clone)] @@ -57,7 +86,7 @@ fn supported_devices(py: Python) -> PyResult<&PyAny> { .import("voicevox_core")? .getattr("SupportedDevices")? .downcast()?; - let s = voicevox_core::SupportedDevices::create().into_py_result()?; + let s = voicevox_core::SupportedDevices::create().into_py_result(py)?; to_pydantic_dataclass(s, class) } @@ -69,9 +98,8 @@ impl VoiceModel { #[pyo3(from_py_with = "from_utf8_path")] path: String, ) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { - let model = voicevox_core::VoiceModel::from_path(path) - .await - .into_py_result()?; + let model = voicevox_core::VoiceModel::from_path(path).await; + let model = Python::with_gil(|py| model.into_py_result(py))?; Ok(Self { model }) }) } @@ -96,19 +124,22 @@ struct OpenJtalk { #[pymethods] impl OpenJtalk { #[new] - fn new(#[pyo3(from_py_with = "from_utf8_path")] open_jtalk_dict_dir: String) -> PyResult { + fn new( + #[pyo3(from_py_with = "from_utf8_path")] open_jtalk_dict_dir: String, + py: Python<'_>, + ) -> PyResult { Ok(Self { open_jtalk: Arc::new( voicevox_core::OpenJtalk::new_with_initialize(open_jtalk_dict_dir) - .into_py_result()?, + .into_py_result(py)?, ), }) } - fn use_user_dict(&self, user_dict: UserDict) -> PyResult<()> { + fn use_user_dict(&self, user_dict: UserDict, py: Python<'_>) -> PyResult<()> { self.open_jtalk .use_user_dict(&user_dict.dict) - .into_py_result() + .into_py_result(py) } } @@ -139,9 +170,8 @@ impl Synthesizer { cpu_num_threads, }, ) - .await - .into_py_result()? - .into(); + .await; + let synthesizer = Python::with_gil(|py| synthesizer.into_py_result(py))?.into(); Ok(Self { synthesizer: Closable::new(Arc::new(synthesizer)), }) @@ -186,20 +216,21 @@ impl Synthesizer { let model: VoiceModel = model.extract()?; let synthesizer = self.synthesizer.get()?.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { - synthesizer + let synthesizer = synthesizer .lock() .await .load_voice_model(&model.model) - .await - .into_py_result() + .await; + + Python::with_gil(|py| synthesizer.into_py_result(py)) }) } - fn unload_voice_model(&mut self, voice_model_id: &str) -> PyResult<()> { + fn unload_voice_model(&mut self, voice_model_id: &str, py: Python<'_>) -> PyResult<()> { RUNTIME .block_on(self.synthesizer.get()?.lock()) .unload_voice_model(&VoiceModelId::new(voice_model_id.to_string())) - .into_py_result() + .into_py_result(py) } fn is_loaded_voice_model(&self, voice_model_id: &str) -> PyResult { @@ -224,12 +255,11 @@ impl Synthesizer { .lock() .await .audio_query_from_kana(&kana, StyleId::new(style_id)) - .await - .into_py_result()?; + .await; Python::with_gil(|py| { let class = py.import("voicevox_core")?.getattr("AudioQuery")?; - let ret = to_pydantic_dataclass(audio_query, class)?; + let ret = to_pydantic_dataclass(audio_query.into_py_result(py)?, class)?; Ok(ret.to_object(py)) }) }, @@ -247,10 +277,10 @@ impl Synthesizer { .lock() .await .audio_query(&text, StyleId::new(style_id)) - .await - .into_py_result()?; + .await; Python::with_gil(|py| { + let audio_query = audio_query.into_py_result(py)?; let class = py.import("voicevox_core")?.getattr("AudioQuery")?; let ret = to_pydantic_dataclass(audio_query, class)?; Ok(ret.to_object(py)) @@ -275,11 +305,11 @@ impl Synthesizer { .lock() .await .create_accent_phrases_from_kana(&kana, StyleId::new(style_id)) - .await - .into_py_result()?; + .await; Python::with_gil(|py| { let class = py.import("voicevox_core")?.getattr("AccentPhrase")?; let accent_phrases = accent_phrases + .into_py_result(py)? .iter() .map(|ap| to_pydantic_dataclass(ap, class)) .collect::>>(); @@ -306,11 +336,11 @@ impl Synthesizer { .lock() .await .create_accent_phrases(&text, StyleId::new(style_id)) - .await - .into_py_result()?; + .await; Python::with_gil(|py| { let class = py.import("voicevox_core")?.getattr("AccentPhrase")?; let accent_phrases = accent_phrases + .into_py_result(py)? .iter() .map(|ap| to_pydantic_dataclass(ap, class)) .collect::>>(); @@ -389,9 +419,11 @@ impl Synthesizer { enable_interrogative_upspeak, }, ) - .await - .into_py_result()?; - Python::with_gil(|py| Ok(PyBytes::new(py, &wav).to_object(py))) + .await; + Python::with_gil(|py| { + let wav = wav.into_py_result(py)?; + Ok(PyBytes::new(py, &wav).to_object(py)) + }) }, ) } @@ -422,9 +454,12 @@ impl Synthesizer { .lock() .await .tts_from_kana(&kana, style_id, &options) - .await - .into_py_result()?; - Python::with_gil(|py| Ok(PyBytes::new(py, &wav).to_object(py))) + .await; + + Python::with_gil(|py| { + let wav = wav.into_py_result(py)?; + Ok(PyBytes::new(py, &wav).to_object(py)) + }) }, ) } @@ -455,9 +490,12 @@ impl Synthesizer { .lock() .await .tts(&text, style_id, &options) - .await - .into_py_result()?; - Python::with_gil(|py| Ok(PyBytes::new(py, &wav).to_object(py))) + .await; + + Python::with_gil(|py| { + let wav = wav.into_py_result(py)?; + Ok(PyBytes::new(py, &wav).to_object(py)) + }) }, ) } @@ -488,7 +526,7 @@ impl Closable { fn get(&self) -> PyResult<&T> { match &self.content { MaybeClosed::Open(content) => Ok(content), - MaybeClosed::Closed => Err(VoicevoxError::new_err(format!( + MaybeClosed::Closed => Err(PyValueError::new_err(format!( "The `{}` is closed", C::NAME, ))), @@ -510,8 +548,8 @@ impl Drop for Closable { } #[pyfunction] -fn _validate_pronunciation(pronunciation: &str) -> PyResult<()> { - voicevox_core::__internal::validate_pronunciation(pronunciation).into_py_result() +fn _validate_pronunciation(pronunciation: &str, py: Python<'_>) -> PyResult<()> { + voicevox_core::__internal::validate_pronunciation(pronunciation).into_py_result(py) } #[pyfunction] @@ -532,12 +570,12 @@ impl UserDict { Self::default() } - fn load(&mut self, path: &str) -> PyResult<()> { - self.dict.load(path).into_py_result() + fn load(&mut self, path: &str, py: Python<'_>) -> PyResult<()> { + self.dict.load(path).into_py_result(py) } - fn save(&self, path: &str) -> PyResult<()> { - self.dict.save(path).into_py_result() + fn save(&self, path: &str, py: Python<'_>) -> PyResult<()> { + self.dict.save(path).into_py_result(py) } fn add_word( @@ -545,7 +583,7 @@ impl UserDict { #[pyo3(from_py_with = "to_rust_user_dict_word")] word: UserDictWord, py: Python, ) -> PyResult { - let uuid = self.dict.add_word(word).into_py_result()?; + let uuid = self.dict.add_word(word).into_py_result(py)?; to_py_uuid(py, uuid) } @@ -554,21 +592,23 @@ impl UserDict { &mut self, #[pyo3(from_py_with = "to_rust_uuid")] word_uuid: Uuid, #[pyo3(from_py_with = "to_rust_user_dict_word")] word: UserDictWord, + py: Python<'_>, ) -> PyResult<()> { - self.dict.update_word(word_uuid, word).into_py_result()?; + self.dict.update_word(word_uuid, word).into_py_result(py)?; Ok(()) } fn remove_word( &mut self, #[pyo3(from_py_with = "to_rust_uuid")] word_uuid: Uuid, + py: Python<'_>, ) -> PyResult<()> { - self.dict.remove_word(word_uuid).into_py_result()?; + self.dict.remove_word(word_uuid).into_py_result(py)?; Ok(()) } - fn import_dict(&mut self, other: &UserDict) -> PyResult<()> { - self.dict.import(&other.dict).into_py_result()?; + fn import_dict(&mut self, other: &UserDict, py: Python<'_>) -> PyResult<()> { + self.dict.import(&other.dict).into_py_result(py)?; Ok(()) }