mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-17 14:02:27 +00:00
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.
This commit is contained in:
@@ -92,7 +92,7 @@ def coerce_value(
|
|||||||
|
|
||||||
# dict types
|
# dict types
|
||||||
if origin is dict:
|
if origin is dict:
|
||||||
return json.loads(value_str)
|
return _floatify_numeric_lists(json.loads(value_str))
|
||||||
|
|
||||||
# Simple types
|
# Simple types
|
||||||
if type_hint is str:
|
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]
|
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]:
|
def _split_list(value_str: str) -> list[str]:
|
||||||
"""Split a comma-separated string, handling JSON arrays too."""
|
"""Split a comma-separated string, handling JSON arrays too."""
|
||||||
value_str = value_str.strip()
|
value_str = value_str.strip()
|
||||||
|
|||||||
@@ -83,6 +83,19 @@ class TestDictCoercion:
|
|||||||
result = coerce_value('{"IsExternal": true, "FireRating": "2HR"}', dict[str, object])
|
result = coerce_value('{"IsExternal": true, "FireRating": "2HR"}', dict[str, object])
|
||||||
assert result == {"IsExternal": True, "FireRating": "2HR"}
|
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:
|
class TestListCoercion:
|
||||||
def test_comma_separated(self):
|
def test_comma_separated(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user