Skip to content

Commit 385fd42

Browse files
Support non-finite floats (NaN, Inf, -Inf) in round-trip conversion
Remove the is_finite() guard in py_to_yaml_value so float('nan'), float('inf'), and float('-inf') convert to serde_yaml::Number, which natively supports these values. This fixes a ValueError when trying to write back YAML .nan/.inf/-.inf values that were read successfully. Update Rust unit tests and add Python round-trip tests.
1 parent 1bee8eb commit 385fd42

2 files changed

Lines changed: 54 additions & 11 deletions

File tree

src/convert.rs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,7 @@ pub fn py_to_yaml_value(obj: &Bound<'_, PyAny>) -> PyResult<Value> {
3131
}
3232
} else if obj.is_instance_of::<PyFloat>() {
3333
let f: f64 = obj.extract()?;
34-
if f.is_finite() {
35-
Ok(Value::Number(serde_yaml::Number::from(f)))
36-
} else {
37-
Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
38-
"Cannot convert float value {f} to YAML number"
39-
)))
40-
}
34+
Ok(Value::Number(serde_yaml::Number::from(f)))
4135
} else if obj.is_instance_of::<PyString>() {
4236
Ok(Value::String(obj.extract::<String>()?))
4337
} else if obj.is_instance_of::<PyList>() {
@@ -174,20 +168,44 @@ mod tests {
174168
}
175169

176170
#[test]
177-
fn test_py_to_yaml_float_nan_rejected() {
171+
fn test_py_to_yaml_float_nan() {
178172
pyo3::prepare_freethreaded_python();
179173
Python::with_gil(|py| {
180174
let nan = f64::NAN.into_pyobject(py).unwrap().into_any();
181-
assert!(py_to_yaml_value(&nan).is_err());
175+
let val = py_to_yaml_value(&nan).unwrap();
176+
match val {
177+
Value::Number(n) => assert!(n.is_nan()),
178+
_ => panic!("expected Number"),
179+
}
182180
});
183181
}
184182

185183
#[test]
186-
fn test_py_to_yaml_float_inf_rejected() {
184+
fn test_py_to_yaml_float_inf() {
187185
pyo3::prepare_freethreaded_python();
188186
Python::with_gil(|py| {
189187
let inf = f64::INFINITY.into_pyobject(py).unwrap().into_any();
190-
assert!(py_to_yaml_value(&inf).is_err());
188+
let val = py_to_yaml_value(&inf).unwrap();
189+
match val {
190+
Value::Number(n) => assert!(n.is_infinite()),
191+
_ => panic!("expected Number"),
192+
}
193+
});
194+
}
195+
196+
#[test]
197+
fn test_py_to_yaml_float_neg_inf() {
198+
pyo3::prepare_freethreaded_python();
199+
Python::with_gil(|py| {
200+
let neg_inf = f64::NEG_INFINITY.into_pyobject(py).unwrap().into_any();
201+
let val = py_to_yaml_value(&neg_inf).unwrap();
202+
match val {
203+
Value::Number(n) => {
204+
assert!(n.is_infinite());
205+
assert_eq!(n.as_f64().unwrap(), f64::NEG_INFINITY);
206+
}
207+
_ => panic!("expected Number"),
208+
}
191209
});
192210
}
193211

tests/test_roundtrip.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Round-trip preservation tests."""
22

3+
import math
4+
35
from yamltrip import Document
46

57

@@ -112,3 +114,26 @@ def test_multi_operation_preserves_structure(self):
112114
assert doc["server", "host"] == "localhost"
113115
assert doc["server", "port"] == 9090
114116
assert doc["database", "pool"] == 10
117+
118+
119+
class TestNonFiniteFloatRoundTrip:
120+
def test_nan_replace_roundtrip(self):
121+
source = "val: .nan\n"
122+
doc = Document(source)
123+
assert math.isnan(doc[("val",)])
124+
doc2 = doc.replace("val", value=float("nan"))
125+
assert math.isnan(doc2[("val",)])
126+
127+
def test_inf_replace_roundtrip(self):
128+
source = "val: .inf\n"
129+
doc = Document(source)
130+
assert doc[("val",)] == float("inf")
131+
doc2 = doc.replace("val", value=float("inf"))
132+
assert doc2[("val",)] == float("inf")
133+
134+
def test_neg_inf_replace_roundtrip(self):
135+
source = "val: -.inf\n"
136+
doc = Document(source)
137+
assert doc[("val",)] == float("-inf")
138+
doc2 = doc.replace("val", value=float("-inf"))
139+
assert doc2[("val",)] == float("-inf")

0 commit comments

Comments
 (0)