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.
This commit is contained in:
Petru Conduraru
2026-07-24 15:40:26 +03:00
committed by Dion Moult
parent 2f1b2f9638
commit fbe36532a0
2 changed files with 30 additions and 2 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.selector import ifcopenshell.util.selector
import numpy as np import numpy as np
from deepdiff import DeepDiff from deepdiff import DeepDiff
from orderly_set import OrderedSet from orderly_set import StableSet
__version__ = version = "0.0.0" __version__ = version = "0.0.0"
@@ -257,7 +257,7 @@ class IfcDiff:
def json_dump_default(self, obj): def json_dump_default(self, obj):
# result of DeepDiff may contain ordered sets # result of DeepDiff may contain ordered sets
if isinstance(obj, (OrderedSet, set)): if isinstance(obj, (StableSet, set)):
return list(obj) return list(obj)
return json.JSONEncoder.default(None, obj) return json.JSONEncoder.default(None, obj)
+28
View File
@@ -16,9 +16,14 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import tempfile
import ifcopenshell import ifcopenshell
import ifcopenshell.api.context import ifcopenshell.api.context
import ifcopenshell.api.geometry import ifcopenshell.api.geometry
import ifcopenshell.api.pset
import ifcopenshell.api.root import ifcopenshell.api.root
import ifcopenshell.api.unit import ifcopenshell.api.unit
import ifcopenshell.util.representation import ifcopenshell.util.representation
@@ -94,6 +99,29 @@ class TestIfcDiff:
assert ifc_diff.deleted_elements == set() assert ifc_diff.deleted_elements == set()
assert ifc_diff.change_register == {wall.GlobalId: {"attributes_changed": True}} 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): def test_changed_geometry(self):
ifc_file = setup_project() ifc_file = setup_project()
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo") wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall", name="Foo")