[core][graphIO] Improve node type version handling

* Deserialization: Replace the logic that defaulted the node type version to "0.0" when unspecified,
and assume that unspecified version on a node is targetting current node type version.
* Serialization: Only serialize node type versions for which a version info is available.

* Test suites:
  * Add helper context manager to manually override the version of a given node type.
  * Add new unit tests to cover version conflicts handling is various scenarios.
This commit is contained in:
Yann Lanthony 2025-02-06 16:46:05 +01:00
parent d9e59e330a
commit 87fbcee06d
7 changed files with 98 additions and 9 deletions

View file

@ -326,12 +326,13 @@ class Graph(BaseObject):
return graphContent return graphContent
def _deserializeNode(self, nodeData: dict, nodeName: str, fromGraph: "Graph"): def _deserializeNode(self, nodeData: dict, nodeName: str, fromGraph: "Graph"):
# Retrieve version from # Retrieve version info from:
# 1. nodeData: node saved from a CompatibilityNode # 1. nodeData: node saved from a CompatibilityNode
# 2. nodesVersion in file header: node saved from a Node # 2. nodesVersion in file header: node saved from a Node
# 3. fallback behavior: default to "0.0" # If unvailable, the "version" field will not be set in `nodeData`.
if "version" not in nodeData: if "version" not in nodeData:
nodeData["version"] = fromGraph._getNodeTypeVersionFromHeader(nodeData["nodeType"], "0.0") if version := fromGraph._getNodeTypeVersionFromHeader(nodeData["nodeType"]):
nodeData["version"] = version
inTemplate = fromGraph.header.get(GraphIO.Keys.Template, False) inTemplate = fromGraph.header.get(GraphIO.Keys.Template, False)
node = nodeFactory(nodeData, nodeName, inTemplate=inTemplate) node = nodeFactory(nodeData, nodeName, inTemplate=inTemplate)
self._addNode(node, nodeName) self._addNode(node, nodeName)

View file

@ -100,7 +100,9 @@ class GraphSerializer:
"""Get registered versions of each node types in `nodes`, excluding CompatibilityNode instances.""" """Get registered versions of each node types in `nodes`, excluding CompatibilityNode instances."""
nodeTypes = set([node.nodeDesc.__class__ for node in self.nodes if isinstance(node, Node)]) nodeTypes = set([node.nodeDesc.__class__ for node in self.nodes if isinstance(node, Node)])
nodeTypesVersions = { nodeTypesVersions = {
nodeType.__name__: meshroom.core.nodeVersion(nodeType, "0.0") for nodeType in nodeTypes nodeType.__name__: version
for nodeType in nodeTypes
if (version := meshroom.core.nodeVersion(nodeType)) is not None
} }
# Sort them by name (to avoid random order changing from one save to another). # Sort them by name (to avoid random order changing from one save to another).
return dict(sorted(nodeTypesVersions.items())) return dict(sorted(nodeTypesVersions.items()))

View file

@ -1608,7 +1608,8 @@ class CompatibilityNode(BaseNode):
# Make a deepcopy of nodeDict to handle CompatibilityNode duplication # Make a deepcopy of nodeDict to handle CompatibilityNode duplication
# and be able to change modified inputs (see CompatibilityNode.toDict) # and be able to change modified inputs (see CompatibilityNode.toDict)
self.nodeDict = copy.deepcopy(nodeDict) self.nodeDict = copy.deepcopy(nodeDict)
self.version = Version(self.nodeDict.get("version", None)) version = self.nodeDict.get("version")
self.version = Version(version) if version else None
self._inputs = self.nodeDict.get("inputs", {}) self._inputs = self.nodeDict.get("inputs", {})
self._internalInputs = self.nodeDict.get("internalInputs", {}) self._internalInputs = self.nodeDict.get("internalInputs", {})

View file

@ -95,7 +95,10 @@ class _NodeCreator:
nodeCreatedFromCurrentVersion = self.version is None nodeCreatedFromCurrentVersion = self.version is None
if nodeCreatedFromCurrentVersion: if nodeCreatedFromCurrentVersion:
return True return True
nodeTypeCurrentVersion = meshroom.core.nodeVersion(self.nodeDesc, "0.0") nodeTypeCurrentVersion = meshroom.core.nodeVersion(self.nodeDesc)
# If the node type has not current version information, assume compatibility.
if nodeTypeCurrentVersion is None:
return True
return Version(self.version).major == Version(nodeTypeCurrentVersion).major return Version(self.version).major == Version(nodeTypeCurrentVersion).major
def _checkDescriptionCompatibility(self) -> bool: def _checkDescriptionCompatibility(self) -> bool:

View file

@ -13,7 +13,7 @@ from meshroom.core.exception import GraphCompatibilityError, NodeUpgradeError
from meshroom.core.graph import Graph, loadGraph from meshroom.core.graph import Graph, loadGraph
from meshroom.core.node import CompatibilityNode, CompatibilityIssue, Node from meshroom.core.node import CompatibilityNode, CompatibilityIssue, Node
from .utils import registeredNodeTypes from .utils import registeredNodeTypes, overrideNodeTypeVersion
SampleGroupV1 = [ SampleGroupV1 = [
@ -473,6 +473,36 @@ class TestGraphTemplateLoading:
loadGraph(graph.filepath, strictCompatibility=True) loadGraph(graph.filepath, strictCompatibility=True)
class TestVersionConflict:
def test_loadingConflictingNodeVersionCreatesCompatibilityNodes(self, graphSavedOnDisk):
graph: Graph = graphSavedOnDisk
with registeredNodeTypes([SampleNodeV1]):
with overrideNodeTypeVersion(SampleNodeV1, "1.0"):
node = graph.addNewNode(SampleNodeV1.__name__)
graph.save()
with overrideNodeTypeVersion(SampleNodeV1, "2.0"):
otherGraph = Graph("")
otherGraph.load(graph.filepath)
assert len(otherGraph.compatibilityNodes) == 1
assert otherGraph.node(node.name).issue is CompatibilityIssue.VersionConflict
def test_loadingUnspecifiedNodeVersionAssumesCurrentVersion(self, graphSavedOnDisk):
graph: Graph = graphSavedOnDisk
with registeredNodeTypes([SampleNodeV1]):
graph.addNewNode(SampleNodeV1.__name__)
graph.save()
with overrideNodeTypeVersion(SampleNodeV1, "2.0"):
otherGraph = Graph("")
otherGraph.load(graph.filepath)
assert len(otherGraph.compatibilityNodes) == 0
class UidTestingNodeV1(desc.Node): class UidTestingNodeV1(desc.Node):
inputs = [ inputs = [

View file

@ -1,7 +1,11 @@
import json
from textwrap import dedent
from meshroom.core import desc from meshroom.core import desc
from meshroom.core.graph import Graph from meshroom.core.graph import Graph
from meshroom.core.node import CompatibilityIssue
from .utils import registeredNodeTypes from .utils import registeredNodeTypes, overrideNodeTypeVersion
class SimpleNode(desc.Node): class SimpleNode(desc.Node):
@ -193,6 +197,21 @@ class TestImportGraphContent:
assert len(otherGraph.compatibilityNodes) == 2 assert len(otherGraph.compatibilityNodes) == 2
assert not compareGraphsContent(graph, otherGraph) assert not compareGraphsContent(graph, otherGraph)
def test_importingDifferentNodeVersionCreatesCompatibilityNodes(self, graphSavedOnDisk):
graph: Graph = graphSavedOnDisk
with registeredNodeTypes([SimpleNode]):
with overrideNodeTypeVersion(SimpleNode, "1.0"):
node = graph.addNewNode(SimpleNode.__name__)
graph.save()
with overrideNodeTypeVersion(SimpleNode, "2.0"):
otherGraph = Graph("")
nodes = otherGraph.importGraphContentFromFile(graph.filepath)
assert len(nodes) == 1
assert len(otherGraph.compatibilityNodes) == 1
assert otherGraph.node(node.name).issue is CompatibilityIssue.VersionConflict
class TestGraphPartialSerialization: class TestGraphPartialSerialization:
def test_emptyGraph(self): def test_emptyGraph(self):
@ -297,3 +316,23 @@ class TestGraphCopy:
graphCopy = graph.copy() graphCopy = graph.copy()
assert not compareGraphsContent(graph, graphCopy) assert not compareGraphsContent(graph, graphCopy)
class TestImportGraphContentFromMinimalGraphData:
def test_nodeWithoutVersionInfoIsUpgraded(self):
graph = Graph("")
with (
registeredNodeTypes([SimpleNode]),
overrideNodeTypeVersion(SimpleNode, "2.0"),
):
sampleGraphContent = dedent("""
{
"SimpleNode_1": { "nodeType": "SimpleNode" }
}
""")
graph._deserialize(json.loads(sampleGraphContent))
assert len(graph.nodes) == 1
assert len(graph.compatibilityNodes) == 0

View file

@ -1,7 +1,9 @@
from contextlib import contextmanager from contextlib import contextmanager
from unittest.mock import patch
from typing import Type from typing import Type
from meshroom.core import registerNodeType, unregisterNodeType
import meshroom
from meshroom.core import registerNodeType, unregisterNodeType
from meshroom.core import desc from meshroom.core import desc
@contextmanager @contextmanager
@ -13,3 +15,14 @@ def registeredNodeTypes(nodeTypes: list[Type[desc.Node]]):
for nodeType in nodeTypes: for nodeType in nodeTypes:
unregisterNodeType(nodeType) unregisterNodeType(nodeType)
@contextmanager
def overrideNodeTypeVersion(nodeType: Type[desc.Node], version: str):
"""Helper context manager to override the version of a given node type."""
unpatchedFunc = meshroom.core.nodeVersion
with patch.object(
meshroom.core,
"nodeVersion",
side_effect=lambda type: version if type is nodeType else unpatchedFunc(type),
):
yield