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;
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 let numerator_owned = unsafe {
80 Bound::from_owned_ptr_or_err(py, ffi::PyNumber_Long(py_numerator_obj.as_ptr()))?
81 };
82 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 })
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}