Add `num-rational` support for Python's `fractions.Fraction` type (#4148)
* Add `num-rational` support for Python's `fractions.Fraction` type * Add newsfragment * Use Bound instead * Handle objs which atts are incorrect * Add extra test * Add tests for wasm32 arch * add type for wasm32 clipppy
This commit is contained in:
parent
635cb8075c
commit
2d19b7e2a7
|
@ -40,6 +40,7 @@ hashbrown = { version = ">= 0.9, < 0.15", optional = true }
|
|||
indexmap = { version = ">= 1.6, < 3", optional = true }
|
||||
num-bigint = { version = "0.4", optional = true }
|
||||
num-complex = { version = ">= 0.2, < 0.5", optional = true }
|
||||
num-rational = {version = "0.4.1", optional = true }
|
||||
rust_decimal = { version = "1.0.0", default-features = false, optional = true }
|
||||
serde = { version = "1.0", optional = true }
|
||||
smallvec = { version = "1.0", optional = true }
|
||||
|
@ -127,6 +128,7 @@ full = [
|
|||
"indexmap",
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-rational",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"smallvec",
|
||||
|
|
|
@ -19,6 +19,7 @@ The table below contains the Python type and the corresponding function argument
|
|||
| `int` | `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `isize`, `usize`, `num_bigint::BigInt`[^1], `num_bigint::BigUint`[^1] | `PyLong` |
|
||||
| `float` | `f32`, `f64` | `PyFloat` |
|
||||
| `complex` | `num_complex::Complex`[^2] | `PyComplex` |
|
||||
| `fractions.Fraction`| `num_rational::Ratio`[^8] | - |
|
||||
| `list[T]` | `Vec<T>` | `PyList` |
|
||||
| `dict[K, V]` | `HashMap<K, V>`, `BTreeMap<K, V>`, `hashbrown::HashMap<K, V>`[^3], `indexmap::IndexMap<K, V>`[^4] | `PyDict` |
|
||||
| `tuple[T, U]` | `(T, U)`, `Vec<T>` | `PyTuple` |
|
||||
|
@ -113,3 +114,5 @@ Finally, the following Rust types are also able to convert to Python as return v
|
|||
[^6]: Requires the `chrono-tz` optional feature.
|
||||
|
||||
[^7]: Requires the `rust_decimal` optional feature.
|
||||
|
||||
[^8]: Requires the `num-rational` optional feature.
|
||||
|
|
|
@ -157,6 +157,10 @@ Adds a dependency on [num-bigint](https://docs.rs/num-bigint) and enables conver
|
|||
|
||||
Adds a dependency on [num-complex](https://docs.rs/num-complex) and enables conversions into its [`Complex`](https://docs.rs/num-complex/latest/num_complex/struct.Complex.html) type.
|
||||
|
||||
### `num-rational`
|
||||
|
||||
Adds a dependency on [num-rational](https://docs.rs/num-rational) and enables conversions into its [`Ratio`](https://docs.rs/num-rational/latest/num_rational/struct.Ratio.html) type.
|
||||
|
||||
### `rust_decimal`
|
||||
|
||||
Adds a dependency on [rust_decimal](https://docs.rs/rust_decimal) and enables conversions into its [`Decimal`](https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html) type.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Conversion between [num-rational](https://github.com/rust-num/num-rational) and Python's fractions.Fraction.
|
|
@ -9,6 +9,7 @@ pub mod hashbrown;
|
|||
pub mod indexmap;
|
||||
pub mod num_bigint;
|
||||
pub mod num_complex;
|
||||
pub mod num_rational;
|
||||
pub mod rust_decimal;
|
||||
pub mod serde;
|
||||
pub mod smallvec;
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
#![cfg(feature = "num-rational")]
|
||||
//! Conversions to and from [num-rational](https://docs.rs/num-rational) types.
|
||||
//!
|
||||
//! This is useful for converting between Python's [fractions.Fraction](https://docs.python.org/3/library/fractions.html) into and from a native Rust
|
||||
//! type.
|
||||
//!
|
||||
//!
|
||||
//! To use this feature, add to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"num-rational\"] }")]
|
||||
//! num-rational = "0.4.1"
|
||||
//! ```
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Rust code to create a function that adds five to a fraction:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use num_rational::Ratio;
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn add_five_to_fraction(fraction: Ratio<i32>) -> Ratio<i32> {
|
||||
//! fraction + Ratio::new(5, 1)
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(add_five_to_fraction, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code that validates the functionality:
|
||||
//! ```python
|
||||
//! from my_module import add_five_to_fraction
|
||||
//! from fractions import Fraction
|
||||
//!
|
||||
//! fraction = Fraction(2,1)
|
||||
//! fraction_plus_five = add_five_to_fraction(f)
|
||||
//! assert fraction + 5 == fraction_plus_five
|
||||
//! ```
|
||||
|
||||
use crate::ffi;
|
||||
use crate::sync::GILOnceCell;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::types::PyType;
|
||||
use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
#[cfg(feature = "num-bigint")]
|
||||
use num_bigint::BigInt;
|
||||
use num_rational::Ratio;
|
||||
|
||||
static FRACTION_CLS: GILOnceCell<Py<PyType>> = GILOnceCell::new();
|
||||
|
||||
fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
|
||||
FRACTION_CLS.get_or_try_init_type_ref(py, "fractions", "Fraction")
|
||||
}
|
||||
|
||||
macro_rules! rational_conversion {
|
||||
($int: ty) => {
|
||||
impl<'py> FromPyObject<'py> for Ratio<$int> {
|
||||
fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> {
|
||||
let py = obj.py();
|
||||
let py_numerator_obj = unsafe {
|
||||
Bound::from_owned_ptr_or_err(
|
||||
py,
|
||||
ffi::PyObject_GetAttrString(
|
||||
obj.as_ptr(),
|
||||
"numerator\0".as_ptr() as *const c_char,
|
||||
),
|
||||
)
|
||||
};
|
||||
let py_denominator_obj = unsafe {
|
||||
Bound::from_owned_ptr_or_err(
|
||||
py,
|
||||
ffi::PyObject_GetAttrString(
|
||||
obj.as_ptr(),
|
||||
"denominator\0".as_ptr() as *const c_char,
|
||||
),
|
||||
)
|
||||
};
|
||||
let numerator_owned = unsafe {
|
||||
Bound::from_owned_ptr_or_err(
|
||||
py,
|
||||
ffi::PyNumber_Long(py_numerator_obj?.as_ptr()),
|
||||
)?
|
||||
};
|
||||
let denominator_owned = unsafe {
|
||||
Bound::from_owned_ptr_or_err(
|
||||
py,
|
||||
ffi::PyNumber_Long(py_denominator_obj?.as_ptr()),
|
||||
)?
|
||||
};
|
||||
let rs_numerator: $int = numerator_owned.extract()?;
|
||||
let rs_denominator: $int = denominator_owned.extract()?;
|
||||
Ok(Ratio::new(rs_numerator, rs_denominator))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPyObject for Ratio<$int> {
|
||||
fn to_object(&self, py: Python<'_>) -> PyObject {
|
||||
let fraction_cls = get_fraction_cls(py).expect("failed to load fractions.Fraction");
|
||||
let ret = fraction_cls
|
||||
.call1((self.numer().clone(), self.denom().clone()))
|
||||
.expect("failed to call fractions.Fraction(value)");
|
||||
ret.to_object(py)
|
||||
}
|
||||
}
|
||||
impl IntoPy<PyObject> for Ratio<$int> {
|
||||
fn into_py(self, py: Python<'_>) -> PyObject {
|
||||
self.to_object(py)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
rational_conversion!(i8);
|
||||
rational_conversion!(i16);
|
||||
rational_conversion!(i32);
|
||||
rational_conversion!(isize);
|
||||
rational_conversion!(i64);
|
||||
#[cfg(feature = "num-bigint")]
|
||||
rational_conversion!(BigInt);
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::dict::PyDictMethods;
|
||||
use crate::types::PyDict;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use proptest::prelude::*;
|
||||
#[test]
|
||||
fn test_negative_fraction() {
|
||||
Python::with_gil(|py| {
|
||||
let locals = PyDict::new_bound(py);
|
||||
py.run_bound(
|
||||
"import fractions\npy_frac = fractions.Fraction(-0.125)",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("py_frac").unwrap().unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
let rs_frac = Ratio::new(-1, 8);
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn test_obj_with_incorrect_atts() {
|
||||
Python::with_gil(|py| {
|
||||
let locals = PyDict::new_bound(py);
|
||||
py.run_bound(
|
||||
"not_fraction = \"contains_incorrect_atts\"",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("not_fraction").unwrap().unwrap();
|
||||
assert!(py_frac.extract::<Ratio<i32>>().is_err());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fraction_with_fraction_type() {
|
||||
Python::with_gil(|py| {
|
||||
let locals = PyDict::new_bound(py);
|
||||
py.run_bound(
|
||||
"import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("py_frac").unwrap().unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
let rs_frac = Ratio::new(10, 1);
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fraction_with_decimal() {
|
||||
Python::with_gil(|py| {
|
||||
let locals = PyDict::new_bound(py);
|
||||
py.run_bound(
|
||||
"import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("py_frac").unwrap().unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
let rs_frac = Ratio::new(11, 10);
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fraction_with_num_den() {
|
||||
Python::with_gil(|py| {
|
||||
let locals = PyDict::new_bound(py);
|
||||
py.run_bound(
|
||||
"import fractions\npy_frac = fractions.Fraction(10,5)",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("py_frac").unwrap().unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
let rs_frac = Ratio::new(10, 5);
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[test]
|
||||
fn test_int_roundtrip() {
|
||||
Python::with_gil(|py| {
|
||||
let rs_frac = Ratio::new(1, 2);
|
||||
let py_frac: PyObject = rs_frac.into_py(py);
|
||||
let roundtripped: Ratio<i32> = py_frac.extract(py).unwrap();
|
||||
assert_eq!(rs_frac, roundtripped);
|
||||
// float conversion
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[test]
|
||||
fn test_big_int_roundtrip() {
|
||||
Python::with_gil(|py| {
|
||||
let rs_frac = Ratio::from_float(5.5).unwrap();
|
||||
let py_frac: PyObject = rs_frac.clone().into_py(py);
|
||||
let roundtripped: Ratio<BigInt> = py_frac.extract(py).unwrap();
|
||||
assert_eq!(rs_frac, roundtripped);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_int_roundtrip(num in any::<i32>(), den in any::<i32>()) {
|
||||
Python::with_gil(|py| {
|
||||
let rs_frac = Ratio::new(num, den);
|
||||
let py_frac = rs_frac.into_py(py);
|
||||
let roundtripped: Ratio<i32> = py_frac.extract(py).unwrap();
|
||||
assert_eq!(rs_frac, roundtripped);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "num-bigint")]
|
||||
fn test_big_int_roundtrip(num in any::<f32>()) {
|
||||
Python::with_gil(|py| {
|
||||
let rs_frac = Ratio::from_float(num).unwrap();
|
||||
let py_frac = rs_frac.clone().into_py(py);
|
||||
let roundtripped: Ratio<BigInt> = py_frac.extract(py).unwrap();
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infinity() {
|
||||
Python::with_gil(|py| {
|
||||
let locals = PyDict::new_bound(py);
|
||||
let py_bound = py.run_bound(
|
||||
"import fractions\npy_frac = fractions.Fraction(\"Infinity\")",
|
||||
None,
|
||||
Some(&locals),
|
||||
);
|
||||
assert!(py_bound.is_err());
|
||||
})
|
||||
}
|
||||
}
|
|
@ -108,6 +108,7 @@
|
|||
//! [`BigUint`] types.
|
||||
//! - [`num-complex`]: Enables conversions between Python objects and [num-complex]'s [`Complex`]
|
||||
//! type.
|
||||
//! - [`num-rational`]: Enables conversions between Python's fractions.Fraction and [num-rational]'s types
|
||||
//! - [`rust_decimal`]: Enables conversions between Python's decimal.Decimal and [rust_decimal]'s
|
||||
//! [`Decimal`] type.
|
||||
//! - [`serde`]: Allows implementing [serde]'s [`Serialize`] and [`Deserialize`] traits for
|
||||
|
@ -288,6 +289,7 @@
|
|||
//! [`maturin`]: https://github.com/PyO3/maturin "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages"
|
||||
//! [`num-bigint`]: ./num_bigint/index.html "Documentation about the `num-bigint` feature."
|
||||
//! [`num-complex`]: ./num_complex/index.html "Documentation about the `num-complex` feature."
|
||||
//! [`num-rational`]: ./num_rational/index.html "Documentation about the `num-rational` feature."
|
||||
//! [`pyo3-build-config`]: https://docs.rs/pyo3-build-config
|
||||
//! [rust_decimal]: https://docs.rs/rust_decimal
|
||||
//! [`rust_decimal`]: ./rust_decimal/index.html "Documenation about the `rust_decimal` feature."
|
||||
|
@ -303,6 +305,7 @@
|
|||
//! [manual_builds]: https://pyo3.rs/latest/building-and-distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide"
|
||||
//! [num-bigint]: https://docs.rs/num-bigint
|
||||
//! [num-complex]: https://docs.rs/num-complex
|
||||
//! [num-rational]: https://docs.rs/num-rational
|
||||
//! [serde]: https://docs.rs/serde
|
||||
//! [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions"
|
||||
//! [the guide]: https://pyo3.rs "PyO3 user guide"
|
||||
|
|
Loading…
Reference in New Issue