|
| 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 | +
|
| 54 | +use crate::conversion::IntoPyObject; |
| 55 | +use crate::exceptions::PyValueError; |
| 56 | +use crate::types::{any::PyAnyMethods, PyFloat}; |
| 57 | +use crate::{Bound, FromPyObject, PyAny, PyResult, Python}; |
| 58 | +use ordered_float::{NotNan, OrderedFloat}; |
| 59 | +use std::convert::Infallible; |
| 60 | + |
| 61 | +macro_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 | +} |
| 92 | +float_conversions!(OrderedFloat, f32, |val| Ok(OrderedFloat(val))); |
| 93 | +float_conversions!(OrderedFloat, f64, |val| Ok(OrderedFloat(val))); |
| 94 | +float_conversions!(NotNan, f32, |val| NotNan::new(val) |
| 95 | + .map_err(|e| PyValueError::new_err(e.to_string()))); |
| 96 | +float_conversions!(NotNan, f64, |val| NotNan::new(val) |
| 97 | + .map_err(|e| PyValueError::new_err(e.to_string()))); |
| 98 | + |
| 99 | +#[cfg(test)] |
| 100 | +mod 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 | +} |
0 commit comments