From 9ecc587cb9ecc904da3d59d7a9d08826e02bca0c Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Sun, 1 Mar 2026 20:27:38 +0000 Subject: [PATCH] coerce: floatify mixed int/float lists in attribute dicts JSON parses whole numbers as int, but ifcopenshell's C++ binding requires Python floats for AGGREGATE OF DOUBLE attributes such as DirectionRatios and Coordinates. Convert list elements to float when the list already contains at least one float, leaving pure-integer lists (face indices etc.) unchanged. Generated with the assistance of an AI coding tool. --- src/ifcedit/ifcedit/coerce.py | 18 +++++++++++++++++- src/ifcedit/tests/test_coerce.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/ifcedit/ifcedit/coerce.py b/src/ifcedit/ifcedit/coerce.py index d191caab11..a192cd721c 100644 --- a/src/ifcedit/ifcedit/coerce.py +++ b/src/ifcedit/ifcedit/coerce.py @@ -92,7 +92,7 @@ def coerce_value( # dict types if origin is dict: - return json.loads(value_str) + return _floatify_numeric_lists(json.loads(value_str)) # Simple types if type_hint is str: @@ -148,6 +148,22 @@ def _coerce_entity_list(value_str: str, lookup_file: ifcopenshell.file | None) - return [_coerce_entity(item.strip(), lookup_file) for item in items] +def _floatify_numeric_lists(obj): + """Recursively convert lists of numbers to lists of floats. + + IFC C++ bindings require Python floats (not ints) for AGGREGATE OF DOUBLE + attributes (e.g. DirectionRatios, Coordinates). JSON parsing produces ints + for whole numbers like 0, which causes a TypeError at the binding level. + """ + if isinstance(obj, dict): + return {k: _floatify_numeric_lists(v) for k, v in obj.items()} + if isinstance(obj, list) and obj and all(isinstance(v, (int, float)) for v in obj) and any( + isinstance(v, float) for v in obj + ): + return [float(v) for v in obj] + return obj + + def _split_list(value_str: str) -> list[str]: """Split a comma-separated string, handling JSON arrays too.""" value_str = value_str.strip() diff --git a/src/ifcedit/tests/test_coerce.py b/src/ifcedit/tests/test_coerce.py index 620ab139b7..4ee6895717 100644 --- a/src/ifcedit/tests/test_coerce.py +++ b/src/ifcedit/tests/test_coerce.py @@ -83,6 +83,19 @@ class TestDictCoercion: result = coerce_value('{"IsExternal": true, "FireRating": "2HR"}', dict[str, object]) assert result == {"IsExternal": True, "FireRating": "2HR"} + def test_mixed_float_int_list_coerced_to_float(self): + # [0.419, 0, 0.908] — JSON integer 0 mixed with floats must become float + # so ifcopenshell AGGREGATE OF DOUBLE attributes (e.g. DirectionRatios) don't reject the list + result = coerce_value('{"DirectionRatios": [0.419, 0, 0.908]}', dict[str, object]) + assert result["DirectionRatios"] == pytest.approx([0.419, 0.0, 0.908]) + assert all(isinstance(v, float) for v in result["DirectionRatios"]) + + def test_pure_int_list_not_coerced(self): + # All-integer lists (e.g. face indices) must stay as ints + result = coerce_value('{"CoordIndex": [0, 1, 2]}', dict[str, object]) + assert result["CoordIndex"] == [0, 1, 2] + assert all(isinstance(v, int) for v in result["CoordIndex"]) + class TestListCoercion: def test_comma_separated(self):