From 385fd4268ba401a0a5bb20bb45de1acc03d80863 Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Fri, 15 May 2026 13:52:56 +1200 Subject: [PATCH 1/2] 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. --- src/convert.rs | 40 +++++++++++++++++++++++++++++----------- tests/test_roundtrip.py | 25 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/convert.rs b/src/convert.rs index 7aa504b..faf1ffd 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -31,13 +31,7 @@ pub fn py_to_yaml_value(obj: &Bound<'_, PyAny>) -> PyResult { } } else if obj.is_instance_of::() { let f: f64 = obj.extract()?; - if f.is_finite() { - Ok(Value::Number(serde_yaml::Number::from(f))) - } else { - Err(PyErr::new::(format!( - "Cannot convert float value {f} to YAML number" - ))) - } + Ok(Value::Number(serde_yaml::Number::from(f))) } else if obj.is_instance_of::() { Ok(Value::String(obj.extract::()?)) } else if obj.is_instance_of::() { @@ -174,20 +168,44 @@ mod tests { } #[test] - fn test_py_to_yaml_float_nan_rejected() { + fn test_py_to_yaml_float_nan() { pyo3::prepare_freethreaded_python(); Python::with_gil(|py| { let nan = f64::NAN.into_pyobject(py).unwrap().into_any(); - assert!(py_to_yaml_value(&nan).is_err()); + let val = py_to_yaml_value(&nan).unwrap(); + match val { + Value::Number(n) => assert!(n.is_nan()), + _ => panic!("expected Number"), + } }); } #[test] - fn test_py_to_yaml_float_inf_rejected() { + fn test_py_to_yaml_float_inf() { pyo3::prepare_freethreaded_python(); Python::with_gil(|py| { let inf = f64::INFINITY.into_pyobject(py).unwrap().into_any(); - assert!(py_to_yaml_value(&inf).is_err()); + let val = py_to_yaml_value(&inf).unwrap(); + match val { + Value::Number(n) => assert!(n.is_infinite()), + _ => panic!("expected Number"), + } + }); + } + + #[test] + fn test_py_to_yaml_float_neg_inf() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let neg_inf = f64::NEG_INFINITY.into_pyobject(py).unwrap().into_any(); + let val = py_to_yaml_value(&neg_inf).unwrap(); + match val { + Value::Number(n) => { + assert!(n.is_infinite()); + assert_eq!(n.as_f64().unwrap(), f64::NEG_INFINITY); + } + _ => panic!("expected Number"), + } }); } diff --git a/tests/test_roundtrip.py b/tests/test_roundtrip.py index f759959..7f3714f 100644 --- a/tests/test_roundtrip.py +++ b/tests/test_roundtrip.py @@ -1,5 +1,7 @@ """Round-trip preservation tests.""" +import math + from yamltrip import Document @@ -112,3 +114,26 @@ def test_multi_operation_preserves_structure(self): assert doc["server", "host"] == "localhost" assert doc["server", "port"] == 9090 assert doc["database", "pool"] == 10 + + +class TestNonFiniteFloatRoundTrip: + def test_nan_replace_roundtrip(self): + source = "val: .nan\n" + doc = Document(source) + assert math.isnan(doc[("val",)]) + doc2 = doc.replace("val", value=float("nan")) + assert math.isnan(doc2[("val",)]) + + def test_inf_replace_roundtrip(self): + source = "val: .inf\n" + doc = Document(source) + assert doc[("val",)] == float("inf") + doc2 = doc.replace("val", value=float("inf")) + assert doc2[("val",)] == float("inf") + + def test_neg_inf_replace_roundtrip(self): + source = "val: -.inf\n" + doc = Document(source) + assert doc[("val",)] == float("-inf") + doc2 = doc.replace("val", value=float("-inf")) + assert doc2[("val",)] == float("-inf") From c720a19a09b82fa8864e893eafaa50af5b0e4aaf Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Fri, 15 May 2026 14:32:57 +1200 Subject: [PATCH 2/2] Update README: non-finite floats now round-trip --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ddd12cf..0ce9c2b 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,8 @@ All yamltrip errors inherit from `YAMLTripError`: - **No custom Python class serialization.** Values convert to/from `str`, `int`, `float`, `bool`, `None`, `list`, and `dict` only. - **UTF-8 only.** Other encodings raise `ParseError`. -- **Non-finite floats rejected.** `float("inf")`, `float("-inf")`, and - `float("nan")` cannot be serialized. +- **Non-finite floats round-trip.** `float("inf")`, `float("-inf")`, and + `float("nan")` map to YAML's `.inf`, `-.inf`, and `.nan`. - **Integer keys cannot create structures.** `upsert()` with integer path components can update existing sequence entries but cannot create new intermediate mappings. Only string keys create new mappings.