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::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 })
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}