Vendor deps.
This commit is contained in:
1 parent
294527085e
commit
91e18fde3a
1714 files changed
+556667
-7
No files matched your search
+264
@@ -0,0 +1,264 @@
|
||||
#![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<Py<PyType>> = PyOnceLock::new();
|
||||
DECIMAL_CLS.import(py, "decimal", "Decimal")
|
||||
}
|
||||
|
||||
fn get_invalid_operation_error_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
|
||||
static INVALID_OPERATION_CLS: PyOnceLock<Py<PyType>> = 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<Self> {
|
||||
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<Self::Output, Self::Error> {
|
||||
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::<i64>()) {
|
||||
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<BigDecimal, PyErr> = 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<BigDecimal, PyErr> = 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::<BigDecimal>()
|
||||
.unwrap()
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.call_method0("as_tuple")
|
||||
.unwrap();
|
||||
|
||||
assert!(actual.eq(expected).unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user