From 062bc3ca283d7e0cfe3edff58f425215cf8f354b Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Fri, 21 Feb 2025 10:45:26 +0100 Subject: [PATCH] [core] ChoiceParam: add option to serialize overriden values Introduce a new `saveValuesOverride` parameter on desc.ChoiceParam to define whether to serialize the list of possible values if they have been overridden at runtime. --- meshroom/core/attribute.py | 29 ++++- meshroom/core/desc/attribute.py | 28 ++++- tests/test_attributeChoiceParam.py | 169 +++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 tests/test_attributeChoiceParam.py diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 5274d3cf..387d2ce1 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -472,11 +472,16 @@ class PushButtonParam(Attribute): class ChoiceParam(Attribute): - def __init__(self, node, attributeDesc, isOutput, root=None, parent=None): + def __init__(self, node, attributeDesc: desc.ChoiceParam, isOutput, root=None, parent=None): super(ChoiceParam, self).__init__(node, attributeDesc, isOutput, root, parent) self._values = None + def __len__(self): + return len(self.getValues()) + def getValues(self): + if (linkParam := self.getLinkParam()) is not None: + return linkParam.getValues() return self._values if self._values is not None else self.desc._values def conformValue(self, val): @@ -494,6 +499,15 @@ class ChoiceParam(Attribute): raise ValueError("Non exclusive ChoiceParam value should be iterable (param:{}, value:{}, type:{})". format(self.name, value, type(value))) return [self.conformValue(v) for v in value] + + def _set_value(self, value): + # Handle alternative serialization for ChoiceParam with overriden values. + serializedValueWithValuesOverrides = isinstance(value, dict) + if serializedValueWithValuesOverrides: + super()._set_value(value[self.desc._OVERRIDE_SERIALIZATION_KEY_VALUE]) + self.setValues(value[self.desc._OVERRIDE_SERIALIZATION_KEY_VALUES]) + else: + super()._set_value(value) def setValues(self, values): if values == self._values: @@ -501,9 +515,18 @@ class ChoiceParam(Attribute): self._values = values self.valuesChanged.emit() - def __len__(self): - return len(self.getValues()) + def getExportValue(self): + useStandardSerialization = self.isLink or not self.desc._saveValuesOverride or self._values is None + if useStandardSerialization: + return super().getExportValue() + + return { + self.desc._OVERRIDE_SERIALIZATION_KEY_VALUE: self._value, + self.desc._OVERRIDE_SERIALIZATION_KEY_VALUES: self._values, + } + + value = Property(Variant, Attribute._get_value, _set_value, notify=Attribute.valueChanged) valuesChanged = Signal() values = Property(Variant, getValues, setValues, notify=valuesChanged) diff --git a/meshroom/core/desc/attribute.py b/meshroom/core/desc/attribute.py index e1f6c4f3..4970ff87 100644 --- a/meshroom/core/desc/attribute.py +++ b/meshroom/core/desc/attribute.py @@ -411,16 +411,33 @@ class PushButtonParam(Param): class ChoiceParam(Param): """ + ChoiceParam is an Attribute that allows to choose a value among a list of possible values. + + When using `exclusive=True`, the value is a single element of the list of possible values. + When using `exclusive=False`, the value is a list of elements of the list of possible values. + + Despite this being the standard behavior, ChoiceParam also supports custom value: it is possible to set any value, + even outside list of possible values. + + The list of possible values on a ChoiceParam instance can be overriden at runtime. + If those changes needs to be persisted, `saveValuesOverride` should be set to True. """ - def __init__(self, name, label, description, value, values, exclusive=True, group="allParams", joinChar=" ", - advanced=False, enabled=True, invalidate=True, semantic="", validValue=True, errorMessage="", + + # Keys for values override serialization schema (saveValuesOverride=True). + _OVERRIDE_SERIALIZATION_KEY_VALUE = "__ChoiceParam_value__" + _OVERRIDE_SERIALIZATION_KEY_VALUES = "__ChoiceParam_values__" + + def __init__(self, name: str, label: str, description: str, value, values, exclusive=True, saveValuesOverride=False, + group="allParams", joinChar=" ", advanced=False, enabled=True, invalidate=True, semantic="", + validValue=True, errorMessage="", visible=True, exposed=False): - assert values + super(ChoiceParam, self).__init__(name=name, label=label, description=description, value=value, group=group, advanced=advanced, enabled=enabled, invalidate=invalidate, semantic=semantic, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) self._values = values + self._saveValuesOverride = saveValuesOverride self._exclusive = exclusive self._joinChar = joinChar if self._values: @@ -447,6 +464,11 @@ class ChoiceParam(Param): def validateValue(self, value): if value is None: return value + + serializedWithValuesOverride = isinstance(value, dict) + if serializedWithValuesOverride: + value = value[ChoiceParam._OVERRIDE_SERIALIZATION_KEY_VALUE] + if self.exclusive: return self.conformValue(value) diff --git a/tests/test_attributeChoiceParam.py b/tests/test_attributeChoiceParam.py new file mode 100644 index 00000000..6527f99d --- /dev/null +++ b/tests/test_attributeChoiceParam.py @@ -0,0 +1,169 @@ +from meshroom.core import desc, registerNodeType, unregisterNodeType +from meshroom.core.graph import Graph, loadGraph + + +class NodeWithChoiceParams(desc.Node): + inputs = [ + desc.ChoiceParam( + name="choice", + label="Choice Default Serialization", + description="A choice parameter with standard serialization", + value="A", + values=["A", "B", "C"], + saveValuesOverride=False, + exclusive=True, + exposed=True, + ), + desc.ChoiceParam( + name="choiceMulti", + label="Choice Default Serialization", + description="A choice parameter with standard serialization", + value=["A"], + values=["A", "B", "C"], + saveValuesOverride=False, + exclusive=False, + exposed=True, + ), + ] + +class NodeWithChoiceParamsSavingValuesOverride(desc.Node): + inputs = [ + desc.ChoiceParam( + name="choice", + label="Choice Custom Serialization", + description="A choice parameter with serialization of overriden values", + value="A", + values=["A", "B", "C"], + saveValuesOverride=True, + exclusive=True, + exposed=True, + ), + desc.ChoiceParam( + name="choiceMulti", + label="Choice Custom Serialization", + description="A choice parameter with serialization of overriden values", + value=["A"], + values=["A", "B", "C"], + saveValuesOverride=True, + exclusive=False, + exposed=True, + ) + ] + + +class TestChoiceParam: + @classmethod + def setup_class(cls): + registerNodeType(NodeWithChoiceParams) + + @classmethod + def teardown_class(cls): + unregisterNodeType(NodeWithChoiceParams) + + def test_customValueIsSerialized(self, graphSavedOnDisk): + graph: Graph = graphSavedOnDisk + + node = graph.addNewNode(NodeWithChoiceParams.__name__) + node.choice.value = "CustomValue" + graph.save() + + loadedGraph = loadGraph(graph.filepath) + assert loadedGraph.node(node.name).choice.value == "CustomValue" + + def test_customMultiValueIsSerialized(self, graphSavedOnDisk): + graph: Graph = graphSavedOnDisk + + node = graph.addNewNode(NodeWithChoiceParams.__name__) + node.choiceMulti.value = ["custom", "value"] + graph.save() + + loadedGraph = loadGraph(graph.filepath) + assert loadedGraph.node(node.name).choiceMulti.value == ["custom", "value"] + + def test_overridenValuesAreNotSerialized(self, graphSavedOnDisk): + graph: Graph = graphSavedOnDisk + node = graph.addNewNode(NodeWithChoiceParams.__name__) + node.choice.values = ["D", "E", "F"] + + graph.save() + loadedGraph = loadGraph(graph.filepath) + + assert loadedGraph.node(node.name).choice.values == ["A", "B", "C"] + + def test_connectionPropagatesOverridenValues(self): + graph = Graph("") + + nodeA = graph.addNewNode(NodeWithChoiceParams.__name__) + nodeB = graph.addNewNode(NodeWithChoiceParams.__name__) + nodeA.choice.values = ["D", "E", "F"] + graph.addEdge(nodeA.choice, nodeB.choice) + + assert nodeB.choice.values == ["D", "E", "F"] + + def test_connectionsAreSerialized(self, graphSavedOnDisk): + graph: Graph = graphSavedOnDisk + nodeA = graph.addNewNode(NodeWithChoiceParams.__name__) + nodeB = graph.addNewNode(NodeWithChoiceParams.__name__) + graph.addEdge(nodeA.choice, nodeB.choice) + graph.addEdge(nodeA.choiceMulti, nodeB.choiceMulti) + + graph.save() + + loadedGraph = loadGraph(graph.filepath) + loadedNodeA = loadedGraph.node(nodeA.name) + loadedNodeB = loadedGraph.node(nodeB.name) + assert loadedNodeB.choice.linkParam == loadedNodeA.choice + assert loadedNodeB.choiceMulti.linkParam == loadedNodeA.choiceMulti + + +class TestChoiceParamSavingCustomValues: + @classmethod + def setup_class(cls): + registerNodeType(NodeWithChoiceParamsSavingValuesOverride) + + @classmethod + def teardown_class(cls): + unregisterNodeType(NodeWithChoiceParamsSavingValuesOverride) + + def test_customValueIsSerialized(self, graphSavedOnDisk): + graph: Graph = graphSavedOnDisk + + node = graph.addNewNode(NodeWithChoiceParamsSavingValuesOverride.__name__) + node.choice.value = "CustomValue" + node.choiceMulti.value = ["custom", "value"] + graph.save() + + loadedGraph = loadGraph(graph.filepath) + assert loadedGraph.node(node.name).choice.value == "CustomValue" + assert loadedGraph.node(node.name).choiceMulti.value == ["custom", "value"] + + + def test_overridenValuesAreSerialized(self, graphSavedOnDisk): + graph: Graph = graphSavedOnDisk + node = graph.addNewNode(NodeWithChoiceParamsSavingValuesOverride.__name__) + node.choice.values = ["D", "E", "F"] + node.choiceMulti.values = ["D", "E", "F"] + + graph.save() + loadedGraph = loadGraph(graph.filepath) + + loadedNode = loadedGraph.node(node.name) + + assert loadedNode.choice.values == ["D", "E", "F"] + assert loadedNode.choiceMulti.values == ["D", "E", "F"] + + + def test_connectionsAreSerialized(self, graphSavedOnDisk): + graph: Graph = graphSavedOnDisk + nodeA = graph.addNewNode(NodeWithChoiceParamsSavingValuesOverride.__name__) + nodeB = graph.addNewNode(NodeWithChoiceParamsSavingValuesOverride.__name__) + graph.addEdge(nodeA.choice, nodeB.choice) + graph.addEdge(nodeA.choiceMulti, nodeB.choiceMulti) + + graph.save() + + loadedGraph = loadGraph(graph.filepath) + loadedNodeA = loadedGraph.node(nodeA.name) + loadedNodeB = loadedGraph.node(nodeB.name) + assert loadedNodeB.choice.linkParam == loadedNodeA.choice + assert loadedNodeB.choiceMulti.linkParam == loadedNodeA.choiceMulti \ No newline at end of file