Skip to content

Commit 6692915

Browse files
authored
Merge pull request #2598 from alicevision/fix/attributeCallbackOnGraphLoad
Discard attribute changed callbacks during graph loading
2 parents c3bb55a + b0808f9 commit 6692915

File tree

5 files changed

+98
-5
lines changed

5 files changed

+98
-5
lines changed

meshroom/core/exception.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ class GraphException(MeshroomException):
1212
pass
1313

1414

15+
class GraphCompatibilityError(GraphException):
16+
"""
17+
Raised when node compatibility issues occur when loading a graph.
18+
19+
Args:
20+
filepath: The path to the file that caused the error.
21+
issues: A dictionnary of node names and their respective compatibility issues.
22+
"""
23+
def __init__(self, filepath, issues: dict[str, str]) -> None:
24+
self.filepath = filepath
25+
self.issues = issues
26+
msg = f"Compatibility issues found when loading {self.filepath}: {self.issues}"
27+
super().__init__(msg)
28+
29+
1530
class UnknownNodeTypeError(GraphException):
1631
"""
1732
Raised when asked to create a unknown node type.

meshroom/core/graph.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from meshroom.common import BaseObject, DictModel, Slot, Signal, Property
1616
from meshroom.core import Version
1717
from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute
18-
from meshroom.core.exception import StopGraphVisit, StopBranchVisit
18+
from meshroom.core.exception import GraphCompatibilityError, StopGraphVisit, StopBranchVisit
1919
from meshroom.core.node import nodeFactory, Status, Node, CompatibilityNode
2020

2121
# Replace default encoder to support Enums
@@ -214,6 +214,7 @@ def getFeaturesForVersion(fileVersion):
214214
def __init__(self, name, parent=None):
215215
super(Graph, self).__init__(parent)
216216
self.name = name
217+
self._loading = False
217218
self._updateEnabled = True
218219
self._updateRequested = False
219220
self.dirtyTopology = False
@@ -246,6 +247,11 @@ def fileFeatures(self):
246247
""" Get loaded file supported features based on its version. """
247248
return Graph.IO.getFeaturesForVersion(self.header.get(Graph.IO.Keys.FileVersion, "0.0"))
248249

250+
@property
251+
def isLoading(self):
252+
""" Return True if the graph is currently being loaded. """
253+
return self._loading
254+
249255
@Slot(str)
250256
def load(self, filepath, setupProjectFile=True, importProject=False, publishOutputs=False):
251257
"""
@@ -259,6 +265,13 @@ def load(self, filepath, setupProjectFile=True, importProject=False, publishOutp
259265
of opened.
260266
publishOutputs: True if "Publish" nodes from templates should not be ignored.
261267
"""
268+
self._loading = True
269+
try:
270+
self._load(filepath, setupProjectFile, importProject, publishOutputs)
271+
finally:
272+
self._loading = False
273+
274+
def _load(self, filepath, setupProjectFile, importProject, publishOutputs):
262275
if not importProject:
263276
self.clear()
264277
with open(filepath) as jsonFile:
@@ -1633,11 +1646,27 @@ def setVerbose(self, v):
16331646
canComputeLeaves = Property(bool, lambda self: self._canComputeLeaves, notify=canComputeLeavesChanged)
16341647

16351648

1636-
def loadGraph(filepath):
1649+
def loadGraph(filepath, strictCompatibility: bool = False) -> Graph:
16371650
"""
1651+
Load a Graph from a Meshroom Graph (.mg) file.
1652+
1653+
Args:
1654+
filepath: The path to the Meshroom Graph file.
1655+
strictCompatibility: If True, raise a GraphCompatibilityError if the loaded Graph has node compatibility issues.
1656+
1657+
Returns:
1658+
Graph: The loaded Graph instance.
1659+
1660+
Raises:
1661+
GraphCompatibilityError: If the Graph has node compatibility issues and `strictCompatibility` is True.
16381662
"""
16391663
graph = Graph("")
16401664
graph.load(filepath)
1665+
1666+
compatibilityIssues = len(graph.compatibilityNodes) > 0
1667+
if compatibilityIssues and strictCompatibility:
1668+
raise GraphCompatibilityError(filepath, {n.name: str(n.issue) for n in graph.compatibilityNodes})
1669+
16411670
graph.update()
16421671
return graph
16431672

meshroom/core/node.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,10 @@ def _onAttributeChanged(self, attr: Attribute):
964964
if attr.value is None:
965965
# Discard dynamic values depending on the graph processing.
966966
return
967+
968+
if self.graph and self.graph.isLoading:
969+
# Do not trigger attribute callbacks during the graph loading.
970+
return
967971

968972
callback = self._getAttributeChangedCallback(attr)
969973

tests/test_compatibility.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import meshroom.core
1010
from meshroom.core import desc, registerNodeType, unregisterNodeType
11-
from meshroom.core.exception import NodeUpgradeError
11+
from meshroom.core.exception import GraphCompatibilityError, NodeUpgradeError
1212
from meshroom.core.graph import Graph, loadGraph
1313
from meshroom.core.node import CompatibilityNode, CompatibilityIssue, Node
1414

@@ -395,3 +395,24 @@ def test_conformUpgrade():
395395

396396
unregisterNodeType(SampleNodeV5)
397397
unregisterNodeType(SampleNodeV6)
398+
399+
400+
class TestGraphLoadingWithStrictCompatibility:
401+
402+
def test_failsOnNodeDescriptionCompatibilityIssue(self, graphSavedOnDisk):
403+
registerNodeType(SampleNodeV1)
404+
registerNodeType(SampleNodeV2)
405+
406+
graph: Graph = graphSavedOnDisk
407+
graph.addNewNode(SampleNodeV1.__name__)
408+
graph.save()
409+
410+
# Replace saved node description by V2
411+
meshroom.core.nodesDesc[SampleNodeV1.__name__] = SampleNodeV2
412+
413+
with pytest.raises(GraphCompatibilityError):
414+
loadGraph(graph.filepath, strictCompatibility=True)
415+
416+
unregisterNodeType(SampleNodeV1)
417+
unregisterNodeType(SampleNodeV2)
418+

tests/test_nodeAttributeChangedCallback.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def test_loadingGraphDoesNotTriggerCallback(self, graphSavedOnDisk):
156156
node.affectedInput.value = 2
157157
graph.save()
158158

159-
loadedGraph = loadGraph(graph.filepath)
159+
loadedGraph = loadGraph(graph.filepath, strictCompatibility=True)
160160
loadedNode = loadedGraph.node(node.name)
161161
assert loadedNode
162162
assert loadedNode.affectedInput.value == 2
@@ -170,11 +170,13 @@ def test_loadingGraphDoesNotTriggerCallbackForConnectedAttributes(
170170

171171
graph.addEdge(nodeA.input, nodeB.input)
172172
nodeA.input.value = 5
173+
assert nodeB.affectedInput.value == nodeB.input.value * 2
174+
173175
nodeB.affectedInput.value = 2
174176

175177
graph.save()
176178

177-
loadedGraph = loadGraph(graph.filepath)
179+
loadedGraph = loadGraph(graph.filepath, strictCompatibility=True)
178180
loadedNodeB = loadedGraph.node(nodeB.name)
179181
assert loadedNodeB
180182
assert loadedNodeB.affectedInput.value == 2
@@ -407,3 +409,25 @@ def test_clearingDynamicOutputValueDoesNotTriggerDownstreamAttributeChangedCallb
407409
nodeA.clearData()
408410
assert nodeA.output.value == nodeB.input.value is None
409411
assert nodeB.affectedInput.value == expectedPreClearValue
412+
413+
def test_loadingGraphWithComputedDynamicOutputValueDoesNotTriggerDownstreamAttributeChangedCallback(
414+
self, graphSavedOnDisk
415+
):
416+
graph: Graph = graphSavedOnDisk
417+
nodeA = graph.addNewNode(NodeWithDynamicOutputValue.__name__)
418+
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
419+
420+
nodeA.input.value = 10
421+
graph.addEdge(nodeA.output, nodeB.input)
422+
executeGraph(graph)
423+
424+
assert nodeA.output.value == nodeB.input.value == 20
425+
assert nodeB.affectedInput.value == 0
426+
427+
graph.save()
428+
429+
loadGraph(graph.filepath, strictCompatibility=True)
430+
431+
assert nodeB.affectedInput.value == 0
432+
433+

0 commit comments

Comments
 (0)