From fbe36532a0e0575fd6d60d6ae65ca831dfd0a6bc Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Fri, 24 Jul 2026 15:40:26 +0300 Subject: [PATCH] ifcdiff: fix crash when exporting property diffs to JSON DeepDiff's dictionary_item_added/set_item_added results are a deepdiff.helper.SetOrdered instance, which subclasses orderly_set's StableSetEq rather than the OrderedSet class json_dump_default checked for, so the property relationship check always crashed export() with "Object of type SetOrdered is not JSON serializable". Check against StableSet, the common base class shared by every orderly_set set flavour, instead. Fixes #8905 Generated with the assistance of an AI coding tool. --- src/ifcdiff/ifcdiff.py | 4 ++-- src/ifcdiff/test.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/ifcdiff/ifcdiff.py b/src/ifcdiff/ifcdiff.py index 9b40aa59e4..eb480bc12f 100755 --- a/src/ifcdiff/ifcdiff.py +++ b/src/ifcdiff/ifcdiff.py @@ -36,7 +36,7 @@ import ifcopenshell.util.representation import ifcopenshell.util.selector import numpy as np from deepdiff import DeepDiff -from orderly_set import OrderedSet +from orderly_set import StableSet __version__ = version = "0.0.0" @@ -257,7 +257,7 @@ class IfcDiff: def json_dump_default(self, obj): # result of DeepDiff may contain ordered sets - if isinstance(obj, (OrderedSet, set)): + if isinstance(obj, (StableSet, set)): return list(obj) return json.JSONEncoder.default(None, obj) diff --git a/src/ifcdiff/test.py b/src/ifcdiff/test.py index b661c0f70b..834e1a9d4f 100644 --- a/src/ifcdiff/test.py +++ b/src/ifcdiff/test.py @@ -16,9 +16,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +import json +import os +import tempfile + import ifcopenshell import ifcopenshell.api.context import ifcopenshell.api.geometry +import ifcopenshell.api.pset import ifcopenshell.api.root import ifcopenshell.api.unit import ifcopenshell.util.representation @@ -94,6 +99,29 @@ class TestIfcDiff: assert ifc_diff.deleted_elements == set() assert ifc_diff.change_register == {wall.GlobalId: {"attributes_changed": True}} + def test_property_diff_exports_to_json(self): + # Regression test for #8905: comparing "property" relationships makes + # DeepDiff report a dictionary_item_added as a SetOrdered, which + # json.dump couldn't serialise, crashing export() with no results. + ifc_file = setup_project() + wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo") + + new_file = ifc_file.from_string(ifc_file.to_string()) + wall_new = new_file.by_id(wall.id()) + pset = ifcopenshell.api.pset.add_pset(new_file, product=wall_new, name="Pset_WallCommon") + ifcopenshell.api.pset.edit_pset(new_file, pset=pset, properties={"FireRating": "2HR"}) + + ifc_diff = ifcdiff.IfcDiff(ifc_file, new_file, relationships=["property"]) + ifc_diff.diff() + assert ifc_diff.change_register[wall.GlobalId]["properties_changed"] + + with tempfile.TemporaryDirectory() as tmp_dir: + output = os.path.join(tmp_dir, "diff.json") + ifc_diff.export(output) + with open(output) as f: + results = json.load(f) + assert "Pset_WallCommon" in str(results["changed"][wall.GlobalId]["properties_changed"]) + def test_changed_geometry(self): ifc_file = setup_project() wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo")