[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
def _deserializeNode(self, nodeData: dict, nodeName: str, fromGraph: "Graph"):
# Retrieve version from
# Retrieve version info from:
# 1. nodeData: node saved from a CompatibilityNode
# 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:
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)
node = nodeFactory(nodeData, nodeName, inTemplate=inTemplate)
self._addNode(node, nodeName)

View file

@ -100,7 +100,9 @@ class GraphSerializer:
"""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)])
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).
return dict(sorted(nodeTypesVersions.items()))

View file

@ -1608,7 +1608,8 @@ class CompatibilityNode(BaseNode):
# Make a deepcopy of nodeDict to handle CompatibilityNode duplication
# and be able to change modified inputs (see CompatibilityNode.toDict)
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._internalInputs = self.nodeDict.get("internalInputs", {})

View file

@ -95,7 +95,10 @@ class _NodeCreator:
nodeCreatedFromCurrentVersion = self.version is None
if nodeCreatedFromCurrentVersion:
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
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.node import CompatibilityNode, CompatibilityIssue, Node
from .utils import registeredNodeTypes
from .utils import registeredNodeTypes, overrideNodeTypeVersion
SampleGroupV1 = [
@ -473,6 +473,36 @@ class TestGraphTemplateLoading:
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):
inputs = [

View file

@ -1,7 +1,11 @@
import json
from textwrap import dedent
from meshroom.core import desc
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):
@ -193,6 +197,21 @@ class TestImportGraphContent:
assert len(otherGraph.compatibilityNodes) == 2
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:
def test_emptyGraph(self):
@ -297,3 +316,23 @@ class TestGraphCopy:
graphCopy = graph.copy()
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 unittest.mock import patch
from typing import Type
from meshroom.core import registerNodeType, unregisterNodeType
import meshroom
from meshroom.core import registerNodeType, unregisterNodeType
from meshroom.core import desc
@contextmanager
@ -13,3 +15,14 @@ def registeredNodeTypes(nodeTypes: list[Type[desc.Node]]):
for nodeType in nodeTypes:
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