pyo3/conversions/
rust_decimal.rs1#![cfg(feature = "rust_decimal")]
2#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"rust_decimal\"] }")]
13use crate::conversion::IntoPyObject;
53use crate::exceptions::PyValueError;
54#[cfg(feature = "experimental-inspect")]
55use crate::inspect::PyStaticExpr;
56use crate::platform::prelude::*;
57use crate::sync::PyOnceLock;
58#[cfg(feature = "experimental-inspect")]
59use crate::type_hint_identifier;
60use crate::types::any::PyAnyMethods;
61use crate::types::string::PyStringMethods;
62use crate::types::PyType;
63use crate::{Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
64use core::str::FromStr;
65use rust_decimal::Decimal;
66
67impl FromPyObject<'_, '_> for Decimal {
68 type Error = PyErr;
69
70 #[cfg(feature = "experimental-inspect")]
71 const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("decimal", "Decimal");
72
73 fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
74 if let Ok(val) = obj.extract() {
76 Ok(Decimal::new(val, 0))
77 } else {
78 let py_str = &obj.str()?;
79 let rs_str = &py_str.to_cow()?;
80 Decimal::from_str(rs_str).or_else(|_| {
81 Decimal::from_scientific(rs_str).map_err(|e| PyValueError::new_err(e.to_string()))
82 })
83 }
84 }
85}
86
87static DECIMAL_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
88
89fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
90 DECIMAL_CLS.import(py, "decimal", "Decimal")
91}
92
93impl<'py> IntoPyObject<'py> for Decimal {
94 type Target = PyAny;
95 type Output = Bound<'py, Self::Target>;
96 type Error = PyErr;
97
98 #[cfg(feature = "experimental-inspect")]
99 const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("decimal", "Decimal");
100
101 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
102 let dec_cls = get_decimal_cls(py)?;
103 dec_cls.call1((self.to_string(),))
106 }
107}
108
109impl<'py> IntoPyObject<'py> for &Decimal {
110 type Target = PyAny;
111 type Output = Bound<'py, Self::Target>;
112 type Error = PyErr;
113
114 #[cfg(feature = "experimental-inspect")]
115 const OUTPUT_TYPE: PyStaticExpr = Decimal::OUTPUT_TYPE;
116
117 #[inline]
118 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
119 (*self).into_pyobject(py)
120 }
121}
122
123#[cfg(test)]
124mod test_rust_decimal {
125 use super::*;
126 use crate::types::dict::PyDictMethods;
127 use crate::types::PyDict;
128 use alloc::ffi::CString;
129
130 #[cfg(not(target_arch = "wasm32"))]
131 use proptest::prelude::*;
132
133 macro_rules! convert_constants {
134 ($name:ident, $rs:expr, $py:literal) => {
135 #[test]
136 fn $name() {
137 Python::attach(|py| {
138 let rs_orig = $rs;
139 let rs_dec = rs_orig.into_pyobject(py).unwrap();
140 let locals = PyDict::new(py);
141 locals.set_item("rs_dec", &rs_dec).unwrap();
142 py.run(
144 &CString::new(format!(
145 "import decimal\npy_dec = decimal.Decimal({})\nassert py_dec == rs_dec",
146 $py
147 ))
148 .unwrap(),
149 None,
150 Some(&locals),
151 )
152 .unwrap();
153 let py_dec = locals.get_item("py_dec").unwrap().unwrap();
155 let py_result: Decimal = py_dec.extract().unwrap();
156 assert_eq!(rs_orig, py_result);
157 })
158 }
159 };
160 }
161
162 convert_constants!(convert_zero, Decimal::ZERO, "0");
163 convert_constants!(convert_one, Decimal::ONE, "1");
164 convert_constants!(convert_neg_one, Decimal::NEGATIVE_ONE, "-1");
165 convert_constants!(convert_two, Decimal::TWO, "2");
166 convert_constants!(convert_ten, Decimal::TEN, "10");
167 convert_constants!(convert_one_hundred, Decimal::ONE_HUNDRED, "100");
168 convert_constants!(convert_one_thousand, Decimal::ONE_THOUSAND, "1000");
169
170 #[cfg(not(target_arch = "wasm32"))]
171 proptest! {
172 #[test]
173 fn test_roundtrip(
174 lo in any::<u32>(),
175 mid in any::<u32>(),
176 high in any::<u32>(),
177 negative in any::<bool>(),
178 scale in 0..28u32
179 ) {
180 let num = Decimal::from_parts(lo, mid, high, negative, scale);
181 Python::attach(|py| {
182 let rs_dec = num.into_pyobject(py).unwrap();
183 let locals = PyDict::new(py);
184 locals.set_item("rs_dec", &rs_dec).unwrap();
185 py.run(
186 &CString::new(format!(
187 "import decimal\npy_dec = decimal.Decimal(\"{num}\")\nassert py_dec == rs_dec")).unwrap(),
188 None, Some(&locals)).unwrap();
189 let roundtripped: Decimal = rs_dec.extract().unwrap();
190 assert_eq!(num, roundtripped);
191 })
192 }
193
194 #[test]
195 fn test_integers(num in any::<i64>()) {
196 Python::attach(|py| {
197 let py_num = num.into_pyobject(py).unwrap();
198 let roundtripped: Decimal = py_num.extract().unwrap();
199 let rs_dec = Decimal::new(num, 0);
200 assert_eq!(rs_dec, roundtripped);
201 })
202 }
203 }
204
205 #[test]
206 fn test_nan() {
207 Python::attach(|py| {
208 let locals = PyDict::new(py);
209 py.run(
210 c"import decimal\npy_dec = decimal.Decimal(\"NaN\")",
211 None,
212 Some(&locals),
213 )
214 .unwrap();
215 let py_dec = locals.get_item("py_dec").unwrap().unwrap();
216 let roundtripped: Result<Decimal, PyErr> = py_dec.extract();
217 assert!(roundtripped.is_err());
218 })
219 }
220
221 #[test]
222 fn test_scientific_notation() {
223 Python::attach(|py| {
224 let locals = PyDict::new(py);
225 py.run(
226 c"import decimal\npy_dec = decimal.Decimal(\"1e3\")",
227 None,
228 Some(&locals),
229 )
230 .unwrap();
231 let py_dec = locals.get_item("py_dec").unwrap().unwrap();
232 let roundtripped: Decimal = py_dec.extract().unwrap();
233 let rs_dec = Decimal::from_scientific("1e3").unwrap();
234 assert_eq!(rs_dec, roundtripped);
235 })
236 }
237
238 #[test]
239 fn test_infinity() {
240 Python::attach(|py| {
241 let locals = PyDict::new(py);
242 py.run(
243 c"import decimal\npy_dec = decimal.Decimal(\"Infinity\")",
244 None,
245 Some(&locals),
246 )
247 .unwrap();
248 let py_dec = locals.get_item("py_dec").unwrap().unwrap();
249 let roundtripped: Result<Decimal, PyErr> = py_dec.extract();
250 assert!(roundtripped.is_err());
251 })
252 }
253}