pyo3/conversions/
num_rational.rs1#![cfg(feature = "num-rational")]
2#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"num-rational\"] }")]
13use crate::conversion::IntoPyObject;
47use crate::ffi;
48use crate::sync::PyOnceLock;
49use crate::types::any::PyAnyMethods;
50use crate::types::PyType;
51use crate::{Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
52
53#[cfg(feature = "num-bigint")]
54use num_bigint::BigInt;
55use num_rational::Ratio;
56
57static FRACTION_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
58
59fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
60 FRACTION_CLS.import(py, "fractions", "Fraction")
61}
62
63macro_rules! rational_conversion {
64 ($int: ty) => {
65 impl<'py> FromPyObject<'_, 'py> for Ratio<$int> {
66 type Error = PyErr;
67
68 fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
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 impl<'py> IntoPyObject<'py> for Ratio<$int> {
88 type Target = PyAny;
89 type Output = Bound<'py, Self::Target>;
90 type Error = PyErr;
91
92 #[inline]
93 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
94 (&self).into_pyobject(py)
95 }
96 }
97
98 impl<'py> IntoPyObject<'py> for &Ratio<$int> {
99 type Target = PyAny;
100 type Output = Bound<'py, Self::Target>;
101 type Error = PyErr;
102
103 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
104 get_fraction_cls(py)?.call1((self.numer().clone(), self.denom().clone()))
105 }
106 }
107 };
108}
109rational_conversion!(i8);
110rational_conversion!(i16);
111rational_conversion!(i32);
112rational_conversion!(isize);
113rational_conversion!(i64);
114#[cfg(feature = "num-bigint")]
115rational_conversion!(BigInt);
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::types::dict::PyDictMethods;
120 use crate::types::PyDict;
121
122 #[cfg(not(target_arch = "wasm32"))]
123 use proptest::prelude::*;
124 #[test]
125 fn test_negative_fraction() {
126 Python::attach(|py| {
127 let locals = PyDict::new(py);
128 py.run(
129 c"import fractions\npy_frac = fractions.Fraction(-0.125)",
130 None,
131 Some(&locals),
132 )
133 .unwrap();
134 let py_frac = locals.get_item("py_frac").unwrap().unwrap();
135 let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
136 let rs_frac = Ratio::new(-1, 8);
137 assert_eq!(roundtripped, rs_frac);
138 })
139 }
140 #[test]
141 fn test_obj_with_incorrect_atts() {
142 Python::attach(|py| {
143 let locals = PyDict::new(py);
144 py.run(
145 c"not_fraction = \"contains_incorrect_atts\"",
146 None,
147 Some(&locals),
148 )
149 .unwrap();
150 let py_frac = locals.get_item("not_fraction").unwrap().unwrap();
151 assert!(py_frac.extract::<Ratio<i32>>().is_err());
152 })
153 }
154
155 #[test]
156 fn test_fraction_with_fraction_type() {
157 Python::attach(|py| {
158 let locals = PyDict::new(py);
159 py.run(
160 c"import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))",
161 None,
162 Some(&locals),
163 )
164 .unwrap();
165 let py_frac = locals.get_item("py_frac").unwrap().unwrap();
166 let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
167 let rs_frac = Ratio::new(10, 1);
168 assert_eq!(roundtripped, rs_frac);
169 })
170 }
171
172 #[test]
173 fn test_fraction_with_decimal() {
174 Python::attach(|py| {
175 let locals = PyDict::new(py);
176 py.run(
177 c"import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))",
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(11, 10);
185 assert_eq!(roundtripped, rs_frac);
186 })
187 }
188
189 #[test]
190 fn test_fraction_with_num_den() {
191 Python::attach(|py| {
192 let locals = PyDict::new(py);
193 py.run(
194 c"import fractions\npy_frac = fractions.Fraction(10,5)",
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(10, 5);
202 assert_eq!(roundtripped, rs_frac);
203 })
204 }
205
206 #[cfg(target_arch = "wasm32")]
207 #[test]
208 fn test_int_roundtrip() {
209 Python::attach(|py| {
210 let rs_frac = Ratio::new(1i32, 2);
211 let py_frac = rs_frac.into_pyobject(py).unwrap();
212 let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
213 assert_eq!(rs_frac, roundtripped);
214 })
216 }
217
218 #[cfg(target_arch = "wasm32")]
219 #[test]
220 fn test_big_int_roundtrip() {
221 Python::attach(|py| {
222 let rs_frac = Ratio::from_float(5.5).unwrap();
223 let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
224 let roundtripped: Ratio<BigInt> = py_frac.extract().unwrap();
225 assert_eq!(rs_frac, roundtripped);
226 })
227 }
228
229 #[cfg(not(target_arch = "wasm32"))]
230 proptest! {
231 #[test]
232 fn test_int_roundtrip(num in any::<i32>(), den in any::<i32>()) {
233 Python::attach(|py| {
234 let rs_frac = Ratio::new(num, den);
235 let py_frac = rs_frac.into_pyobject(py).unwrap();
236 let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
237 assert_eq!(rs_frac, roundtripped);
238 })
239 }
240
241 #[test]
242 #[cfg(feature = "num-bigint")]
243 fn test_big_int_roundtrip(num in any::<f32>()) {
244 Python::attach(|py| {
245 let rs_frac = Ratio::from_float(num).unwrap();
246 let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
247 let roundtripped: Ratio<BigInt> = py_frac.extract().unwrap();
248 assert_eq!(roundtripped, rs_frac);
249 })
250 }
251
252 }
253
254 #[test]
255 fn test_infinity() {
256 Python::attach(|py| {
257 let locals = PyDict::new(py);
258 let py_bound = py.run(
259 c"import fractions\npy_frac = fractions.Fraction(\"Infinity\")",
260 None,
261 Some(&locals),
262 );
263 assert!(py_bound.is_err());
264 })
265 }
266}