Skip to main content

pyo3/conversions/
ordered_float.rs

1#![cfg(feature = "ordered-float")]
2//! Conversions to and from [ordered-float](https://docs.rs/ordered-float) types.
3//! [`NotNan`]`<`[`f32`]`>` and [`NotNan`]`<`[`f64`]`>`.
4//! [`OrderedFloat`]`<`[`f32`]`>` and [`OrderedFloat`]`<`[`f64`]`>`.
5//!
6//! This is useful for converting between Python's float into and from a native Rust type.
7//!
8//! Take care when comparing sorted collections of float types between Python and Rust.
9//! They will likely differ due to the ambiguous sort order of NaNs in Python.
10//
11//!
12//! To use this feature, add to your **`Cargo.toml`**:
13//!
14//! ```toml
15//! [dependencies]
16#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"ordered-float\"] }")]
17//! ordered-float = "5.0.0"
18//! ```
19//!
20//! # Example
21//!
22//! Rust code to create functions that add ordered floats:
23//!
24//! ```rust,no_run
25//! use ordered_float::{NotNan, OrderedFloat};
26//! use pyo3::prelude::*;
27//!
28//! #[pyfunction]
29//! fn add_not_nans(a: NotNan<f64>, b: NotNan<f64>) -> NotNan<f64> {
30//!     a + b
31//! }
32//!
33//! #[pyfunction]
34//! fn add_ordered_floats(a: OrderedFloat<f64>, b: OrderedFloat<f64>) -> OrderedFloat<f64> {
35//!     a + b
36//! }
37//!
38//! #[pymodule]
39//! # #[pyo3(name = "example_ordered_float")]
40//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
41//!     m.add_function(wrap_pyfunction!(add_not_nans, m)?)?;
42//!     m.add_function(wrap_pyfunction!(add_ordered_floats, m)?)?;
43//!     Ok(())
44//! }
45//! ```
46//!
47//! Python code that validates the functionality:
48//! ```python
49//! from my_module import add_not_nans, add_ordered_floats
50//!
51//! assert add_not_nans(1.0,2.0) == 3.0
52//! assert add_ordered_floats(1.0,2.0) == 3.0
53//! ```
54
55use crate::conversion::IntoPyObject;
56use crate::exceptions::PyValueError;
57#[cfg(feature = "experimental-inspect")]
58use crate::inspect::PyStaticExpr;
59use crate::platform::prelude::*;
60use crate::types::PyFloat;
61use crate::{Borrowed, Bound, FromPyObject, PyAny, Python};
62use core::convert::Infallible;
63use ordered_float::{NotNan, OrderedFloat};
64
65macro_rules! float_conversions {
66    ($wrapper:ident, $float_type:ty, $constructor:expr) => {
67        impl<'a, 'py> FromPyObject<'a, 'py> for $wrapper<$float_type> {
68            type Error = <$float_type as FromPyObject<'a, 'py>>::Error;
69
70            #[cfg(feature = "experimental-inspect")]
71            const INPUT_TYPE: PyStaticExpr = <$float_type>::INPUT_TYPE;
72
73            fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
74                let val: $float_type = obj.extract()?;
75                $constructor(val)
76            }
77        }
78
79        impl<'py> IntoPyObject<'py> for $wrapper<$float_type> {
80            type Target = PyFloat;
81            type Output = Bound<'py, Self::Target>;
82            type Error = Infallible;
83
84            #[cfg(feature = "experimental-inspect")]
85            const OUTPUT_TYPE: PyStaticExpr = <$float_type>::OUTPUT_TYPE;
86
87            fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
88                self.into_inner().into_pyobject(py)
89            }
90        }
91
92        impl<'py> IntoPyObject<'py> for &$wrapper<$float_type> {
93            type Target = PyFloat;
94            type Output = Bound<'py, Self::Target>;
95            type Error = Infallible;
96
97            #[cfg(feature = "experimental-inspect")]
98            const OUTPUT_TYPE: PyStaticExpr = <$wrapper<$float_type>>::OUTPUT_TYPE;
99
100            #[inline]
101            fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
102                (*self).into_pyobject(py)
103            }
104        }
105    };
106}
107float_conversions!(OrderedFloat, f32, |val| Ok(OrderedFloat(val)));
108float_conversions!(OrderedFloat, f64, |val| Ok(OrderedFloat(val)));
109float_conversions!(NotNan, f32, |val| NotNan::new(val)
110    .map_err(|e| PyValueError::new_err(e.to_string())));
111float_conversions!(NotNan, f64, |val| NotNan::new(val)
112    .map_err(|e| PyValueError::new_err(e.to_string())));
113
114#[cfg(test)]
115mod test_ordered_float {
116    use super::*;
117    use crate::types::PyAnyMethods;
118    use crate::types::dict::IntoPyDict;
119    use alloc::ffi::CString;
120    use core::ffi::CStr;
121
122    #[cfg(not(target_arch = "wasm32"))]
123    use proptest::prelude::*;
124
125    fn py_run<'py>(py: Python<'py>, script: &CStr, locals: impl IntoPyDict<'py>) {
126        py.run(script, None, Some(&locals.into_py_dict(py).unwrap()))
127            .unwrap()
128    }
129
130    macro_rules! float_roundtrip_tests {
131        ($wrapper:ident, $float_type:ty, $constructor:expr, $standard_test:ident, $wasm_test:ident, $infinity_test:ident, $zero_test:ident) => {
132            #[cfg(not(target_arch = "wasm32"))]
133            proptest! {
134            #[test]
135            fn $standard_test(inner_f: $float_type) {
136                let f = $constructor(inner_f);
137
138                Python::attach(|py| {
139                    let f_py: Bound<'_, PyFloat>  = f.into_pyobject(py).unwrap();
140
141                    py_run(py, &CString::new(format!(
142                            "import math\nassert math.isclose(f_py, {})",
143                             inner_f as f64 // Always interpret the literal rs float value as f64
144                                            // so that it's comparable with the python float
145                        )).unwrap(), [("f_py", &f_py)]);
146
147                    let roundtripped_f: $wrapper<$float_type> = f_py.extract().unwrap();
148
149                    assert_eq!(f, roundtripped_f);
150                })
151            }
152            }
153
154            #[cfg(target_arch = "wasm32")]
155            #[test]
156            fn $wasm_test() {
157                let inner_f = 10.0;
158                let f = $constructor(inner_f);
159
160                Python::attach(|py| {
161                    let f_py: Bound<'_, PyFloat> = f.into_pyobject(py).unwrap();
162
163                    py_run(
164                        py,
165                        &CString::new(format!(
166                            "import math\nassert math.isclose(f_py, {})",
167                            inner_f as f64 // Always interpret the literal rs float value as f64
168                                           // so that it's comparable with the python float
169                        ))
170                        .unwrap(),
171                        [("f_py", &f_py)],
172                    );
173
174                    let roundtripped_f: $wrapper<$float_type> = f_py.extract().unwrap();
175
176                    assert_eq!(f, roundtripped_f);
177                })
178            }
179
180            #[test]
181            fn $infinity_test() {
182                let inner_pinf = <$float_type>::INFINITY;
183                let pinf = $constructor(inner_pinf);
184
185                let inner_ninf = <$float_type>::NEG_INFINITY;
186                let ninf = $constructor(inner_ninf);
187
188                Python::attach(|py| {
189                    let pinf_py: Bound<'_, PyFloat> = pinf.into_pyobject(py).unwrap();
190                    let ninf_py: Bound<'_, PyFloat> = ninf.into_pyobject(py).unwrap();
191
192                    py_run(
193                        py,
194                        c"\
195                        assert pinf_py == float('inf')\n\
196                        assert ninf_py == float('-inf')",
197                        [("pinf_py", &pinf_py), ("ninf_py", &ninf_py)],
198                    );
199
200                    let roundtripped_pinf: $wrapper<$float_type> = pinf_py.extract().unwrap();
201                    let roundtripped_ninf: $wrapper<$float_type> = ninf_py.extract().unwrap();
202
203                    assert_eq!(pinf, roundtripped_pinf);
204                    assert_eq!(ninf, roundtripped_ninf);
205                })
206            }
207
208            #[test]
209            fn $zero_test() {
210                let inner_pzero: $float_type = 0.0;
211                let pzero = $constructor(inner_pzero);
212
213                let inner_nzero: $float_type = -0.0;
214                let nzero = $constructor(inner_nzero);
215
216                Python::attach(|py| {
217                    let pzero_py: Bound<'_, PyFloat> = pzero.into_pyobject(py).unwrap();
218                    let nzero_py: Bound<'_, PyFloat> = nzero.into_pyobject(py).unwrap();
219
220                    // This python script verifies that the values are 0.0 in magnitude
221                    // and that the signs are correct(+0.0 vs -0.0)
222                    py_run(
223                        py,
224                        c"\
225                        import math\n\
226                        assert pzero_py == 0.0\n\
227                        assert math.copysign(1.0, pzero_py) > 0.0\n\
228                        assert nzero_py == 0.0\n\
229                        assert math.copysign(1.0, nzero_py) < 0.0",
230                        [("pzero_py", &pzero_py), ("nzero_py", &nzero_py)],
231                    );
232
233                    let roundtripped_pzero: $wrapper<$float_type> = pzero_py.extract().unwrap();
234                    let roundtripped_nzero: $wrapper<$float_type> = nzero_py.extract().unwrap();
235
236                    assert_eq!(pzero, roundtripped_pzero);
237                    assert_eq!(roundtripped_pzero.signum(), 1.0);
238                    assert_eq!(nzero, roundtripped_nzero);
239                    assert_eq!(roundtripped_nzero.signum(), -1.0);
240                })
241            }
242        };
243    }
244    float_roundtrip_tests!(
245        OrderedFloat,
246        f32,
247        OrderedFloat,
248        ordered_float_f32_standard,
249        ordered_float_f32_wasm,
250        ordered_float_f32_infinity,
251        ordered_float_f32_zero
252    );
253    float_roundtrip_tests!(
254        OrderedFloat,
255        f64,
256        OrderedFloat,
257        ordered_float_f64_standard,
258        ordered_float_f64_wasm,
259        ordered_float_f64_infinity,
260        ordered_float_f64_zero
261    );
262    float_roundtrip_tests!(
263        NotNan,
264        f32,
265        |val| NotNan::new(val).unwrap(),
266        not_nan_f32_standard,
267        not_nan_f32_wasm,
268        not_nan_f32_infinity,
269        not_nan_f32_zero
270    );
271    float_roundtrip_tests!(
272        NotNan,
273        f64,
274        |val| NotNan::new(val).unwrap(),
275        not_nan_f64_standard,
276        not_nan_f64_wasm,
277        not_nan_f64_infinity,
278        not_nan_f64_zero
279    );
280
281    macro_rules! ordered_float_pynan_tests {
282        ($test_name:ident, $float_type:ty) => {
283            #[test]
284            fn $test_name() {
285                let inner_nan: $float_type = <$float_type>::NAN;
286                let nan = OrderedFloat(inner_nan);
287
288                Python::attach(|py| {
289                    let nan_py: Bound<'_, PyFloat> = nan.into_pyobject(py).unwrap();
290
291                    py_run(
292                        py,
293                        c"import math\nassert math.isnan(nan_py)",
294                        [("nan_py", &nan_py)],
295                    );
296
297                    let roundtripped_nan: OrderedFloat<$float_type> = nan_py.extract().unwrap();
298
299                    assert_eq!(nan, roundtripped_nan);
300                })
301            }
302        };
303    }
304    ordered_float_pynan_tests!(test_ordered_float_pynan_f32, f32);
305    ordered_float_pynan_tests!(test_ordered_float_pynan_f64, f64);
306
307    macro_rules! not_nan_pynan_tests {
308        ($test_name:ident, $float_type:ty) => {
309            #[test]
310            fn $test_name() {
311                Python::attach(|py| {
312                    let nan_py = py.eval(c"float('nan')", None, None).unwrap();
313
314                    let nan_rs: Result<NotNan<$float_type>, _> = nan_py.extract();
315
316                    assert!(nan_rs.is_err());
317                })
318            }
319        };
320    }
321    not_nan_pynan_tests!(test_not_nan_pynan_f32, f32);
322    not_nan_pynan_tests!(test_not_nan_pynan_f64, f64);
323
324    macro_rules! py64_rs32 {
325        ($test_name:ident, $wrapper:ident, $float_type:ty) => {
326            #[test]
327            fn $test_name() {
328                Python::attach(|py| {
329                    let py_64 = py
330                        .import("sys")
331                        .unwrap()
332                        .getattr("float_info")
333                        .unwrap()
334                        .getattr("max")
335                        .unwrap();
336                    let rs_32 = py_64.extract::<$wrapper<f32>>().unwrap();
337                    // The python f64 is not representable in a rust f32
338                    assert!(rs_32.is_infinite());
339                })
340            }
341        };
342    }
343    py64_rs32!(ordered_float_f32, OrderedFloat, f32);
344    py64_rs32!(ordered_float_f64, OrderedFloat, f64);
345    py64_rs32!(not_nan_f32, NotNan, f32);
346    py64_rs32!(not_nan_f64, NotNan, f64);
347}