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