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