#![cfg(feature = "bigdecimal")] //! Conversions to and from [bigdecimal](https://docs.rs/bigdecimal)'s [`BigDecimal`] type. //! //! This is useful for converting Python's decimal.Decimal into and from a native Rust type. //! //! # Setup //! //! To use this feature, add to your **`Cargo.toml`**: //! //! ```toml //! [dependencies] #![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"bigdecimal\"] }")] //! bigdecimal = "0.4" //! ``` //! //! Note that you must use a compatible version of bigdecimal and PyO3. //! The required bigdecimal version may vary based on the version of PyO3. //! //! # Example //! //! Rust code to create a function that adds one to a BigDecimal //! //! ```rust //! use bigdecimal::BigDecimal; //! use pyo3::prelude::*; //! //! #[pyfunction] //! fn add_one(d: BigDecimal) -> BigDecimal { //! d + 1 //! } //! //! #[pymodule] //! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(add_one, m)?)?; //! Ok(()) //! } //! ``` //! //! Python code that validates the functionality //! //! //! ```python //! from my_module import add_one //! from decimal import Decimal //! //! d = Decimal("2") //! value = add_one(d) //! //! assert d + 1 == value //! ``` use core::str::FromStr; #[cfg(feature = "experimental-inspect")] use crate::inspect::PyStaticExpr; #[cfg(feature = "experimental-inspect")] use crate::type_hint_identifier; use crate::types::PyTuple; use crate::{ exceptions::PyValueError, sync::PyOnceLock, types::{PyAnyMethods, PyStringMethods, PyType}, Borrowed, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, }; use bigdecimal::BigDecimal; use num_bigint::Sign; fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { static DECIMAL_CLS: PyOnceLock> = PyOnceLock::new(); DECIMAL_CLS.import(py, "decimal", "Decimal") } fn get_invalid_operation_error_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { static INVALID_OPERATION_CLS: PyOnceLock> = PyOnceLock::new(); INVALID_OPERATION_CLS.import(py, "decimal", "InvalidOperation") } impl FromPyObject<'_, '_> for BigDecimal { type Error = PyErr; #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("decimal", "Decimal"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult { let py_str = &obj.str()?; let rs_str = &py_str.to_cow()?; BigDecimal::from_str(rs_str).map_err(|e| PyValueError::new_err(e.to_string())) } } impl<'py> IntoPyObject<'py> for BigDecimal { type Target = PyAny; type Output = Bound<'py, Self::Target>; type Error = PyErr; #[cfg(feature = "experimental-inspect")] const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("decimal", "Decimal"); fn into_pyobject(self, py: Python<'py>) -> Result { let cls = get_decimal_cls(py)?; let (bigint, scale) = self.into_bigint_and_scale(); if scale == 0 { return cls.call1((bigint,)); } let exponent = scale.checked_neg().ok_or_else(|| { get_invalid_operation_error_cls(py) .map_or_else(|err| err, |cls| PyErr::from_type(cls.clone(), ())) })?; let (sign, digits) = bigint.to_radix_be(10); let signed = matches!(sign, Sign::Minus).into_pyobject(py)?; let digits = PyTuple::new(py, digits)?; cls.call1(((signed, digits, exponent),)) } } #[cfg(test)] mod test_bigdecimal { use super::*; use crate::types::dict::PyDictMethods; use crate::types::PyDict; use alloc::ffi::CString; use bigdecimal::{One, Zero}; #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; macro_rules! convert_constants { ($name:ident, $rs:expr, $py:literal) => { #[test] fn $name() { Python::attach(|py| { let rs_orig = $rs; let rs_dec = rs_orig.clone().into_pyobject(py).unwrap(); let locals = PyDict::new(py); locals.set_item("rs_dec", &rs_dec).unwrap(); // Checks if BigDecimal -> Python Decimal conversion is correct py.run( &CString::new(format!( "import decimal\npy_dec = decimal.Decimal(\"{}\")\nassert py_dec == rs_dec", $py )) .unwrap(), None, Some(&locals), ) .unwrap(); // Checks if Python Decimal -> BigDecimal conversion is correct let py_dec = locals.get_item("py_dec").unwrap().unwrap(); let py_result: BigDecimal = py_dec.extract().unwrap(); assert_eq!(rs_orig, py_result); }) } }; } convert_constants!(convert_zero, BigDecimal::zero(), "0"); convert_constants!(convert_one, BigDecimal::one(), "1"); convert_constants!(convert_neg_one, -BigDecimal::one(), "-1"); convert_constants!(convert_two, BigDecimal::from(2), "2"); convert_constants!(convert_ten, BigDecimal::from_str("10").unwrap(), "10"); convert_constants!( convert_one_hundred_point_one, BigDecimal::from_str("100.1").unwrap(), "100.1" ); convert_constants!( convert_one_thousand, BigDecimal::from_str("1000").unwrap(), "1000" ); convert_constants!( convert_scientific, BigDecimal::from_str("1e10").unwrap(), "1e10" ); #[cfg(not(target_arch = "wasm32"))] proptest! { #[test] fn test_roundtrip( number in 0..28u32 ) { let num = BigDecimal::from(number); Python::attach(|py| { let rs_dec = num.clone().into_pyobject(py).unwrap(); let locals = PyDict::new(py); locals.set_item("rs_dec", &rs_dec).unwrap(); py.run( &CString::new(format!( "import decimal\npy_dec = decimal.Decimal(\"{num}\")\nassert py_dec == rs_dec")).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: BigDecimal = rs_dec.extract().unwrap(); assert_eq!(num, roundtripped); }) } #[test] fn test_integers(num in any::()) { Python::attach(|py| { let py_num = num.into_pyobject(py).unwrap(); let roundtripped: BigDecimal = py_num.extract().unwrap(); let rs_dec = BigDecimal::from(num); assert_eq!(rs_dec, roundtripped); }) } } #[test] fn test_nan() { Python::attach(|py| { let locals = PyDict::new(py); py.run( c"import decimal\npy_dec = decimal.Decimal(\"NaN\")", None, Some(&locals), ) .unwrap(); let py_dec = locals.get_item("py_dec").unwrap().unwrap(); let roundtripped: Result = py_dec.extract(); assert!(roundtripped.is_err()); }) } #[test] fn test_infinity() { Python::attach(|py| { let locals = PyDict::new(py); py.run( c"import decimal\npy_dec = decimal.Decimal(\"Infinity\")", None, Some(&locals), ) .unwrap(); let py_dec = locals.get_item("py_dec").unwrap().unwrap(); let roundtripped: Result = py_dec.extract(); assert!(roundtripped.is_err()); }) } #[test] fn test_no_precision_loss() { Python::attach(|py| { let src = "1e4"; let expected = get_decimal_cls(py) .unwrap() .call1((src,)) .unwrap() .call_method0("as_tuple") .unwrap(); let actual = src .parse::() .unwrap() .into_pyobject(py) .unwrap() .call_method0("as_tuple") .unwrap(); assert!(actual.eq(expected).unwrap()); }); } }