Skip to main content

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,no_run
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;
48#[cfg(feature = "experimental-inspect")]
49use crate::inspect::PyStaticExpr;
50use crate::sync::PyOnceLock;
51#[cfg(feature = "experimental-inspect")]
52use crate::type_hint_identifier;
53use crate::types::any::PyAnyMethods;
54use crate::types::PyType;
55use crate::{Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
56#[cfg(feature = "num-bigint")]
57use num_bigint::BigInt;
58use num_rational::Ratio;
59
60static FRACTION_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
61
62fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
63    FRACTION_CLS.import(py, "fractions", "Fraction")
64}
65
66macro_rules! rational_conversion {
67    ($int: ty) => {
68        impl<'py> FromPyObject<'_, 'py> for Ratio<$int> {
69            type Error = PyErr;
70
71            #[cfg(feature = "experimental-inspect")]
72            const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("fractions", "Fraction");
73
74            fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
75                let py = obj.py();
76                let py_numerator_obj = obj.getattr(crate::intern!(py, "numerator"))?;
77                let py_denominator_obj = obj.getattr(crate::intern!(py, "denominator"))?;
78                // SAFETY: `PyNumber_Long` returns a new owned reference or NULL with an error set
79                let numerator_owned = unsafe {
80                    Bound::from_owned_ptr_or_err(py, ffi::PyNumber_Long(py_numerator_obj.as_ptr()))?
81                };
82                // SAFETY: `PyNumber_Long` returns a new owned reference or NULL with an error set
83                let denominator_owned = unsafe {
84                    Bound::from_owned_ptr_or_err(
85                        py,
86                        ffi::PyNumber_Long(py_denominator_obj.as_ptr()),
87                    )?
88                };
89                let rs_numerator: $int = numerator_owned.extract()?;
90                let rs_denominator: $int = denominator_owned.extract()?;
91                Ok(Ratio::new(rs_numerator, rs_denominator))
92            }
93        }
94
95        impl<'py> IntoPyObject<'py> for Ratio<$int> {
96            type Target = PyAny;
97            type Output = Bound<'py, Self::Target>;
98            type Error = PyErr;
99
100            #[cfg(feature = "experimental-inspect")]
101            const OUTPUT_TYPE: PyStaticExpr = <&Ratio<$int>>::OUTPUT_TYPE;
102
103            #[inline]
104            fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
105                (&self).into_pyobject(py)
106            }
107        }
108
109        impl<'py> IntoPyObject<'py> for &Ratio<$int> {
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 = type_hint_identifier!("fractions", "Fraction");
116
117            fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
118                get_fraction_cls(py)?.call1((self.numer().clone(), self.denom().clone()))
119            }
120        }
121    };
122}
123rational_conversion!(i8);
124rational_conversion!(i16);
125rational_conversion!(i32);
126rational_conversion!(isize);
127rational_conversion!(i64);
128#[cfg(feature = "num-bigint")]
129rational_conversion!(BigInt);
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::types::dict::PyDictMethods;
134    use crate::types::PyDict;
135
136    #[cfg(not(target_arch = "wasm32"))]
137    use proptest::prelude::*;
138    #[test]
139    fn test_negative_fraction() {
140        Python::attach(|py| {
141            let locals = PyDict::new(py);
142            py.run(
143                c"import fractions\npy_frac = fractions.Fraction(-0.125)",
144                None,
145                Some(&locals),
146            )
147            .unwrap();
148            let py_frac = locals.get_item("py_frac").unwrap().unwrap();
149            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
150            let rs_frac = Ratio::new(-1, 8);
151            assert_eq!(roundtripped, rs_frac);
152        })
153    }
154    #[test]
155    fn test_obj_with_incorrect_atts() {
156        Python::attach(|py| {
157            let locals = PyDict::new(py);
158            py.run(
159                c"not_fraction = \"contains_incorrect_atts\"",
160                None,
161                Some(&locals),
162            )
163            .unwrap();
164            let py_frac = locals.get_item("not_fraction").unwrap().unwrap();
165            assert!(py_frac.extract::<Ratio<i32>>().is_err());
166        })
167    }
168
169    #[test]
170    fn test_fraction_with_fraction_type() {
171        Python::attach(|py| {
172            let locals = PyDict::new(py);
173            py.run(
174                c"import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))",
175                None,
176                Some(&locals),
177            )
178            .unwrap();
179            let py_frac = locals.get_item("py_frac").unwrap().unwrap();
180            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
181            let rs_frac = Ratio::new(10, 1);
182            assert_eq!(roundtripped, rs_frac);
183        })
184    }
185
186    #[test]
187    fn test_fraction_with_decimal() {
188        Python::attach(|py| {
189            let locals = PyDict::new(py);
190            py.run(
191                c"import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))",
192                None,
193                Some(&locals),
194            )
195            .unwrap();
196            let py_frac = locals.get_item("py_frac").unwrap().unwrap();
197            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
198            let rs_frac = Ratio::new(11, 10);
199            assert_eq!(roundtripped, rs_frac);
200        })
201    }
202
203    #[test]
204    fn test_fraction_with_num_den() {
205        Python::attach(|py| {
206            let locals = PyDict::new(py);
207            py.run(
208                c"import fractions\npy_frac = fractions.Fraction(10,5)",
209                None,
210                Some(&locals),
211            )
212            .unwrap();
213            let py_frac = locals.get_item("py_frac").unwrap().unwrap();
214            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
215            let rs_frac = Ratio::new(10, 5);
216            assert_eq!(roundtripped, rs_frac);
217        })
218    }
219
220    #[cfg(target_arch = "wasm32")]
221    #[test]
222    fn test_int_roundtrip() {
223        Python::attach(|py| {
224            let rs_frac = Ratio::new(1i32, 2);
225            let py_frac = rs_frac.into_pyobject(py).unwrap();
226            let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
227            assert_eq!(rs_frac, roundtripped);
228            // float conversion
229        })
230    }
231
232    #[cfg(target_arch = "wasm32")]
233    #[test]
234    fn test_big_int_roundtrip() {
235        Python::attach(|py| {
236            let rs_frac = Ratio::from_float(5.5).unwrap();
237            let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
238            let roundtripped: Ratio<BigInt> = py_frac.extract().unwrap();
239            assert_eq!(rs_frac, roundtripped);
240        })
241    }
242
243    #[cfg(not(target_arch = "wasm32"))]
244    proptest! {
245        #[test]
246        fn test_int_roundtrip(num in any::<i32>(), den in any::<i32>()) {
247            Python::attach(|py| {
248                let rs_frac = Ratio::new(num, den);
249                let py_frac = rs_frac.into_pyobject(py).unwrap();
250                let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
251                assert_eq!(rs_frac, roundtripped);
252            })
253        }
254
255        #[test]
256        #[cfg(feature = "num-bigint")]
257        fn test_big_int_roundtrip(num in any::<f32>()) {
258            Python::attach(|py| {
259                let rs_frac = Ratio::from_float(num).unwrap();
260                let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
261                let roundtripped: Ratio<BigInt> = py_frac.extract().unwrap();
262                assert_eq!(roundtripped, rs_frac);
263            })
264        }
265
266    }
267
268    #[test]
269    fn test_infinity() {
270        Python::attach(|py| {
271            let locals = PyDict::new(py);
272            let py_bound = py.run(
273                c"import fractions\npy_frac = fractions.Fraction(\"Infinity\")",
274                None,
275                Some(&locals),
276            );
277            assert!(py_bound.is_err());
278        })
279    }
280}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here