pyo3/conversions/
num_rational.rs

1#![cfg(feature = "num-rational")]
2//! Conversions to and from [num-rational](https://docs.rs/num-rational) types.
3//!
4//! This is useful for converting between Python's [fractions.Fraction](https://docs.python.org/3/library/fractions.html) into and from a native Rust
5//! type.
6//!
7//!
8//! To use this feature, add to your **`Cargo.toml`**:
9//!
10//! ```toml
11//! [dependencies]
12#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"num-rational\"] }")]
13//! num-rational = "0.4.1"
14//! ```
15//!
16//! # Example
17//!
18//! Rust code to create a function that adds five to a fraction:
19//!
20//! ```rust
21//! use num_rational::Ratio;
22//! use pyo3::prelude::*;
23//!
24//! #[pyfunction]
25//! fn add_five_to_fraction(fraction: Ratio<i32>) -> Ratio<i32> {
26//!     fraction + Ratio::new(5, 1)
27//! }
28//!
29//! #[pymodule]
30//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
31//!     m.add_function(wrap_pyfunction!(add_five_to_fraction, m)?)?;
32//!     Ok(())
33//! }
34//! ```
35//!
36//! Python code that validates the functionality:
37//! ```python
38//! from my_module import add_five_to_fraction
39//! from fractions import Fraction
40//!
41//! fraction = Fraction(2,1)
42//! fraction_plus_five = add_five_to_fraction(f)
43//! assert fraction + 5 == fraction_plus_five
44//! ```
45
46use crate::conversion::IntoPyObject;
47use crate::ffi;
48use crate::sync::GILOnceCell;
49use crate::types::any::PyAnyMethods;
50use crate::types::PyType;
51use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python};
52#[allow(deprecated)]
53use crate::{IntoPy, ToPyObject};
54
55#[cfg(feature = "num-bigint")]
56use num_bigint::BigInt;
57use num_rational::Ratio;
58
59static FRACTION_CLS: GILOnceCell<Py<PyType>> = GILOnceCell::new();
60
61fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
62    FRACTION_CLS.import(py, "fractions", "Fraction")
63}
64
65macro_rules! rational_conversion {
66    ($int: ty) => {
67        impl<'py> FromPyObject<'py> for Ratio<$int> {
68            fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> {
69                let py = obj.py();
70                let py_numerator_obj = obj.getattr(crate::intern!(py, "numerator"))?;
71                let py_denominator_obj = obj.getattr(crate::intern!(py, "denominator"))?;
72                let numerator_owned = unsafe {
73                    Bound::from_owned_ptr_or_err(py, ffi::PyNumber_Long(py_numerator_obj.as_ptr()))?
74                };
75                let denominator_owned = unsafe {
76                    Bound::from_owned_ptr_or_err(
77                        py,
78                        ffi::PyNumber_Long(py_denominator_obj.as_ptr()),
79                    )?
80                };
81                let rs_numerator: $int = numerator_owned.extract()?;
82                let rs_denominator: $int = denominator_owned.extract()?;
83                Ok(Ratio::new(rs_numerator, rs_denominator))
84            }
85        }
86
87        #[allow(deprecated)]
88        impl ToPyObject for Ratio<$int> {
89            #[inline]
90            fn to_object(&self, py: Python<'_>) -> PyObject {
91                self.into_pyobject(py).unwrap().into_any().unbind()
92            }
93        }
94        #[allow(deprecated)]
95        impl IntoPy<PyObject> for Ratio<$int> {
96            #[inline]
97            fn into_py(self, py: Python<'_>) -> PyObject {
98                self.into_pyobject(py).unwrap().into_any().unbind()
99            }
100        }
101
102        impl<'py> IntoPyObject<'py> for Ratio<$int> {
103            type Target = PyAny;
104            type Output = Bound<'py, Self::Target>;
105            type Error = PyErr;
106
107            #[inline]
108            fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
109                (&self).into_pyobject(py)
110            }
111        }
112
113        impl<'py> IntoPyObject<'py> for &Ratio<$int> {
114            type Target = PyAny;
115            type Output = Bound<'py, Self::Target>;
116            type Error = PyErr;
117
118            fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
119                get_fraction_cls(py)?.call1((self.numer().clone(), self.denom().clone()))
120            }
121        }
122    };
123}
124rational_conversion!(i8);
125rational_conversion!(i16);
126rational_conversion!(i32);
127rational_conversion!(isize);
128rational_conversion!(i64);
129#[cfg(feature = "num-bigint")]
130rational_conversion!(BigInt);
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::types::dict::PyDictMethods;
135    use crate::types::PyDict;
136
137    #[cfg(not(target_arch = "wasm32"))]
138    use proptest::prelude::*;
139    #[test]
140    fn test_negative_fraction() {
141        Python::with_gil(|py| {
142            let locals = PyDict::new(py);
143            py.run(
144                ffi::c_str!("import fractions\npy_frac = fractions.Fraction(-0.125)"),
145                None,
146                Some(&locals),
147            )
148            .unwrap();
149            let py_frac = locals.get_item("py_frac").unwrap().unwrap();
150            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
151            let rs_frac = Ratio::new(-1, 8);
152            assert_eq!(roundtripped, rs_frac);
153        })
154    }
155    #[test]
156    fn test_obj_with_incorrect_atts() {
157        Python::with_gil(|py| {
158            let locals = PyDict::new(py);
159            py.run(
160                ffi::c_str!("not_fraction = \"contains_incorrect_atts\""),
161                None,
162                Some(&locals),
163            )
164            .unwrap();
165            let py_frac = locals.get_item("not_fraction").unwrap().unwrap();
166            assert!(py_frac.extract::<Ratio<i32>>().is_err());
167        })
168    }
169
170    #[test]
171    fn test_fraction_with_fraction_type() {
172        Python::with_gil(|py| {
173            let locals = PyDict::new(py);
174            py.run(
175                ffi::c_str!(
176                    "import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))"
177                ),
178                None,
179                Some(&locals),
180            )
181            .unwrap();
182            let py_frac = locals.get_item("py_frac").unwrap().unwrap();
183            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
184            let rs_frac = Ratio::new(10, 1);
185            assert_eq!(roundtripped, rs_frac);
186        })
187    }
188
189    #[test]
190    fn test_fraction_with_decimal() {
191        Python::with_gil(|py| {
192            let locals = PyDict::new(py);
193            py.run(
194                ffi::c_str!("import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))"),
195                None,
196                Some(&locals),
197            )
198            .unwrap();
199            let py_frac = locals.get_item("py_frac").unwrap().unwrap();
200            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
201            let rs_frac = Ratio::new(11, 10);
202            assert_eq!(roundtripped, rs_frac);
203        })
204    }
205
206    #[test]
207    fn test_fraction_with_num_den() {
208        Python::with_gil(|py| {
209            let locals = PyDict::new(py);
210            py.run(
211                ffi::c_str!("import fractions\npy_frac = fractions.Fraction(10,5)"),
212                None,
213                Some(&locals),
214            )
215            .unwrap();
216            let py_frac = locals.get_item("py_frac").unwrap().unwrap();
217            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
218            let rs_frac = Ratio::new(10, 5);
219            assert_eq!(roundtripped, rs_frac);
220        })
221    }
222
223    #[cfg(target_arch = "wasm32")]
224    #[test]
225    fn test_int_roundtrip() {
226        Python::with_gil(|py| {
227            let rs_frac = Ratio::new(1i32, 2);
228            let py_frac = rs_frac.into_pyobject(py).unwrap();
229            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
230            assert_eq!(rs_frac, roundtripped);
231            // float conversion
232        })
233    }
234
235    #[cfg(target_arch = "wasm32")]
236    #[test]
237    fn test_big_int_roundtrip() {
238        Python::with_gil(|py| {
239            let rs_frac = Ratio::from_float(5.5).unwrap();
240            let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
241            let roundtripped: Ratio<BigInt> = py_frac.extract().unwrap();
242            assert_eq!(rs_frac, roundtripped);
243        })
244    }
245
246    #[cfg(not(target_arch = "wasm32"))]
247    proptest! {
248        #[test]
249        fn test_int_roundtrip(num in any::<i32>(), den in any::<i32>()) {
250            Python::with_gil(|py| {
251                let rs_frac = Ratio::new(num, den);
252                let py_frac = rs_frac.into_pyobject(py).unwrap();
253                let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
254                assert_eq!(rs_frac, roundtripped);
255            })
256        }
257
258        #[test]
259        #[cfg(feature = "num-bigint")]
260        fn test_big_int_roundtrip(num in any::<f32>()) {
261            Python::with_gil(|py| {
262                let rs_frac = Ratio::from_float(num).unwrap();
263                let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
264                let roundtripped: Ratio<BigInt> = py_frac.extract().unwrap();
265                assert_eq!(roundtripped, rs_frac);
266            })
267        }
268
269    }
270
271    #[test]
272    fn test_infinity() {
273        Python::with_gil(|py| {
274            let locals = PyDict::new(py);
275            let py_bound = py.run(
276                ffi::c_str!("import fractions\npy_frac = fractions.Fraction(\"Infinity\")"),
277                None,
278                Some(&locals),
279            );
280            assert!(py_bound.is_err());
281        })
282    }
283}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here