import json import os import tempfile import zipfile import xml.etree.ElementTree as ET import FreeCAD as App FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" class PropertyObserver: def __init__(self, document_name): self.document_name = document_name self.events = [] def clear(self): self.events = [] def slotOpenTransaction(self, document, name): if document.Name == self.document_name: self.events.append({"type": "transaction.opened", "name": str(name)}) def slotCommitTransaction(self, document): if document.Name == self.document_name: self.events.append({"type": "transaction.committed"}) def slotAbortTransaction(self, document): if document.Name == self.document_name: self.events.append({"type": "transaction.aborted"}) def slotBeforeChangeObject(self, obj, prop): if obj.Document and obj.Document.Name == self.document_name: self.events.append({"type": "property.before-change", "object": obj.Name, "property": str(prop)}) def slotChangedObject(self, obj, prop): if obj.Document and obj.Document.Name == self.document_name: self.events.append({"type": "property.changed", "object": obj.Name, "property": str(prop)}) def slotAppendDynamicProperty(self, obj, prop): if getattr(obj, "Document", None) and obj.Document.Name == self.document_name: self.events.append({"type": "property.added", "object": obj.Name, "property": str(prop)}) def slotRemoveDynamicProperty(self, obj, prop): if getattr(obj, "Document", None) and obj.Document.Name == self.document_name: self.events.append({"type": "property.removed", "object": obj.Name, "property": str(prop)}) def object_state(obj): return { "state": [str(value) for value in obj.State], "mustExecute": bool(obj.MustExecute), "documentTouched": bool(obj.Document.isTouched()), } def change_case(case_id, attr=0, runtime_status=None): document = App.newDocument("PropertyStatus_" + case_id) try: obj = document.addObject("App::FeatureTest", "Feature") obj.addProperty("App::PropertyInteger", "Value", "Oracle", "Status probe", attr) if runtime_status: obj.setPropertyStatus("Value", runtime_status) document.recompute() document.purgeTouched() before = object_state(obj) obj.Value = 7 changed = object_state(obj) recomputed = int(document.recompute()) after_recompute = object_state(obj) return { "id": case_id, "attr": int(attr), "runtimeStatus": runtime_status, "reportedStatus": [str(value) for value in obj.getPropertyStatus("Value")], "reportedType": [str(value) for value in obj.getTypeOfProperty("Value")], "before": before, "changed": changed, "recomputeCount": recomputed, "afterRecompute": after_recompute, } finally: App.closeDocument(document.Name) def locked_dynamic_case(): document = App.newDocument("PropertyStatus_LockDynamic") observer = None try: document.UndoMode = 1 obj = document.addObject("App::FeaturePython", "Feature") obj.addProperty("App::PropertyInteger", "LockedValue", "Oracle") obj.setPropertyStatus("LockedValue", "LockDynamic") observer = PropertyObserver(document.Name) App.addDocumentObserver(observer) document.openTransaction("dynamic-property-mutation") obj.addProperty("App::PropertyInteger", "MutableValue", "Oracle") obj.MutableValue = 3 failures = {} for operation, callback in { "remove": lambda: obj.removeProperty("LockedValue"), "rename": lambda: obj.renameProperty("LockedValue", "RenamedValue"), }.items(): try: callback() failures[operation] = None except Exception as error: failures[operation] = {"type": type(error).__name__, "message": str(error)} obj.renameProperty("MutableValue", "RenamedValue") removed_mutable = bool(obj.removeProperty("RenamedValue")) document.commitTransaction() return { "reportedStatus": [str(value) for value in obj.getPropertyStatus("LockedValue")], "propertyStillPresent": "LockedValue" in obj.PropertiesList, "renamedPropertyPresent": "RenamedValue" in obj.PropertiesList, "failures": failures, "mutableRenameAndRemove": removed_mutable and "MutableValue" not in obj.PropertiesList and "RenamedValue" not in obj.PropertiesList, "observerEvents": observer.events, "undoAvailable": document.UndoCount == 1, } finally: if observer: App.removeDocumentObserver(observer) App.closeDocument(document.Name) def partial_trigger_case(): document = App.newDocument("PropertyStatus_PartialTrigger") observer = PropertyObserver(document.Name) App.addDocumentObserver(observer) try: document.UndoMode = 1 obj = document.addObject("PartDesign::SubShapeBinder", "Binder") document.recompute() document.purgeTouched() observer.clear() document.openTransaction("partial-trigger-mutation") obj.PartialLoad = True recomputed = int(document.recompute()) document.commitTransaction() return { "reportedStatus": [str(value) for value in obj.getPropertyStatus("PartialLoad")], "value": bool(obj.PartialLoad), "recomputeCount": recomputed, "state": object_state(obj), "observerEvents": observer.events, "undoAvailable": document.UndoCount == 1, } finally: App.removeDocumentObserver(observer) App.closeDocument(document.Name) def archive_case(): with tempfile.TemporaryDirectory(prefix="freecad-property-status-") as temp_dir: path = os.path.join(temp_dir, "PropertyStatus.FCStd") document = App.newDocument("PropertyStatusArchive") try: obj = document.addObject("App::FeaturePython", "Feature") target = document.addObject("App::FeaturePython", "Target") obj.addProperty("App::PropertyInteger", "Persisted", "Oracle") obj.Persisted = 11 obj.setPropertyStatus("Persisted", "Output") obj.addProperty("App::PropertyInteger", "RuntimeTransient", "Oracle") obj.RuntimeTransient = 12 obj.setPropertyStatus("RuntimeTransient", "Transient") obj.addProperty("App::PropertyInteger", "TypeTransient", "Oracle", "", int(App.PropertyType.Prop_Transient)) obj.TypeTransient = 13 obj.addProperty("App::PropertyInteger", "NoPersist", "Oracle", "", int(App.PropertyType.Prop_NoPersist)) obj.NoPersist = 14 obj.addProperty("App::PropertyFloatConstraint", "FloatConstraint", "Oracle") obj.FloatConstraint = 0.25 obj.addProperty("App::PropertyQuantityConstraint", "QuantityConstraint", "Oracle") obj.QuantityConstraint = 12.5 obj.addProperty("App::PropertyPrecision", "Precision", "Oracle") obj.Precision = 0.001 obj.addProperty("App::PropertyLinkHidden", "HiddenLink", "Oracle") obj.HiddenLink = target document.recompute() document.saveAs(path) finally: App.closeDocument(document.Name) with zipfile.ZipFile(path, "r") as archive: document_xml = archive.read("Document.xml").decode("utf-8") xml_root = ET.fromstring(document_xml) xml_properties = { element.attrib.get("name"): element for element in xml_root.findall("./ObjectData/Object/Properties/Property") } def native_float_property(name, expected): element = xml_properties.get(name) child = element.find("Float") if element is not None else None return bool(element is not None and element.attrib.get("type") == "App::Property" + name and child is not None and abs(float(child.attrib["value"]) - expected) < 1e-12) reopened = App.openDocument(path) try: obj = reopened.getObject("Feature") return { "xml": { "persistedProperty": '