Skip to content

Commit f636374

Browse files
committed
[core] Refactor nodeFactory function
Rewrite `nodeFactory` to reduce cognitive complexity, while preserving the current behavior.
1 parent 1408962 commit f636374

File tree

3 files changed

+185
-103
lines changed

3 files changed

+185
-103
lines changed

meshroom/core/graph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def _load(self, filepath, setupProjectFile, importProject, publishOutputs):
333333
if isTemplate and not publishOutputs and nodeData["nodeType"] == "Publish":
334334
continue
335335

336-
n = nodeFactory(nodeData, nodeName, template=isTemplate)
336+
n = nodeFactory(nodeData, nodeName, inTemplate=isTemplate)
337337

338338
# Add node to the graph with raw attributes values
339339
self._addNode(n, nodeName)
@@ -386,14 +386,14 @@ def _evaluateUidConflicts(self, data):
386386
# Different UIDs, remove the existing node from the graph and replace it with a CompatibilityNode
387387
logging.debug("UID conflict detected for {}".format(nodeName))
388388
self.removeNode(nodeName)
389-
n = nodeFactory(nodeData, nodeName, template=False, uidConflict=True)
389+
n = nodeFactory(nodeData, nodeName, expectedUid=graphUid)
390390
self._addNode(n, nodeName)
391391
else:
392392
# f connecting nodes have UID conflicts and are removed/re-added to the graph, some edges may be lost:
393393
# the links will be erroneously updated, and any further resolution will fail.
394394
# Recreating the entire graph as it was ensures that all edges will be correctly preserved.
395395
self.removeNode(nodeName)
396-
n = nodeFactory(nodeData, nodeName, template=False, uidConflict=False)
396+
n = nodeFactory(nodeData, nodeName)
397397
self._addNode(n, nodeName)
398398

399399
def updateImportedProject(self, data):

meshroom/core/nodeFactory.py

Lines changed: 180 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,197 @@
11
import logging
2+
from typing import Any, Iterable, Optional, Union
23

34
import meshroom.core
45
from meshroom.core import Version, desc
56
from meshroom.core.node import CompatibilityIssue, CompatibilityNode, Node, Position
67

78

8-
def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
9+
def nodeFactory(
10+
nodeData: dict,
11+
name: Optional[str] = None,
12+
inTemplate: bool = False,
13+
expectedUid: Optional[str] = None,
14+
) -> Union[Node, CompatibilityNode]:
915
"""
1016
Create a node instance by deserializing the given node data.
1117
If the serialized data matches the corresponding node type description, a Node instance is created.
1218
If any compatibility issue occurs, a NodeCompatibility instance is created instead.
1319
1420
Args:
15-
nodeDict (dict): the serialization of the node
16-
name (str): (optional) the node's name
17-
template (bool): (optional) true if the node is part of a template, false otherwise
18-
uidConflict (bool): (optional) true if a UID conflict has been detected externally on that node
21+
nodeDict: The serialized Node data.
22+
name: (optional) The node's name.
23+
inTemplate: (optional) True if the node is created as part of a graph template.
24+
expectedUid: (optional) The expected UID of the node within the context of a Graph.
1925
2026
Returns:
21-
BaseNode: the created node
27+
The created Node instance.
2228
"""
23-
nodeType = nodeDict["nodeType"]
24-
25-
# Retro-compatibility: inputs were previously saved as "attributes"
26-
if "inputs" not in nodeDict and "attributes" in nodeDict:
27-
nodeDict["inputs"] = nodeDict["attributes"]
28-
del nodeDict["attributes"]
29-
30-
# Get node inputs/outputs
31-
inputs = nodeDict.get("inputs", {})
32-
internalInputs = nodeDict.get("internalInputs", {})
33-
outputs = nodeDict.get("outputs", {})
34-
version = nodeDict.get("version", None)
35-
internalFolder = nodeDict.get("internalFolder", None)
36-
position = Position(*nodeDict.get("position", []))
37-
uid = nodeDict.get("uid", None)
38-
39-
compatibilityIssue = None
40-
41-
nodeDesc = None
42-
try:
43-
nodeDesc = meshroom.core.nodesDesc[nodeType]
44-
except KeyError:
45-
# Unknown node type
46-
compatibilityIssue = CompatibilityIssue.UnknownNodeType
47-
48-
# Unknown node type should take precedence over UID conflict, as it cannot be resolved
49-
if uidConflict and nodeDesc:
50-
compatibilityIssue = CompatibilityIssue.UidConflict
51-
52-
if nodeDesc and not uidConflict: # if uidConflict, there is no need to look for another compatibility issue
53-
# Compare serialized node version with current node version
54-
currentNodeVersion = meshroom.core.nodeVersion(nodeDesc)
55-
# If both versions are available, check for incompatibility in major version
56-
if version and currentNodeVersion and Version(version).major != Version(currentNodeVersion).major:
57-
compatibilityIssue = CompatibilityIssue.VersionConflict
58-
# In other cases, check attributes compatibility between serialized node and its description
29+
return _NodeCreator(nodeData, name, inTemplate, expectedUid).create()
30+
31+
32+
class _NodeCreator:
33+
34+
def __init__(
35+
self,
36+
nodeData: dict,
37+
name: Optional[str] = None,
38+
inTemplate: bool = False,
39+
expectedUid: Optional[str] = None,
40+
):
41+
self.nodeData = nodeData
42+
self.name = name
43+
self.inTemplate = inTemplate
44+
self.expectedUid = expectedUid
45+
46+
self._normalizeNodeData()
47+
48+
self.nodeType = self.nodeData["nodeType"]
49+
self.inputs = self.nodeData.get("inputs", {})
50+
self.internalInputs = self.nodeData.get("internalInputs", {})
51+
self.outputs = self.nodeData.get("outputs", {})
52+
self.version = self.nodeData.get("version", None)
53+
self.internalFolder = self.nodeData.get("internalFolder")
54+
self.position = Position(*self.nodeData.get("position", []))
55+
self.uid = self.nodeData.get("uid", None)
56+
self.nodeDesc = meshroom.core.nodesDesc.get(self.nodeType, None)
57+
58+
def create(self) -> Union[Node, CompatibilityNode]:
59+
compatibilityIssue = self._checkCompatibilityIssues()
60+
if compatibilityIssue:
61+
node = self._createCompatibilityNode(compatibilityIssue)
62+
node = self._tryUpgradeCompatibilityNode(node)
5963
else:
60-
# Check that the node has the exact same set of inputs/outputs as its description, except
61-
# if the node is described in a template file, in which only non-default parameters are saved;
62-
# do not perform that check for internal attributes because there is no point in
63-
# raising compatibility issues if their number differs: in that case, it is only useful
64-
# if some internal attributes do not exist or are invalid
65-
if not template and (sorted([attr.name for attr in nodeDesc.inputs
66-
if not isinstance(attr, desc.PushButtonParam)]) != sorted(inputs.keys()) or
67-
sorted([attr.name for attr in nodeDesc.outputs if not attr.isDynamicValue]) !=
68-
sorted(outputs.keys())):
69-
compatibilityIssue = CompatibilityIssue.DescriptionConflict
70-
71-
# Check whether there are any internal attributes that are invalidating in the node description: if there
72-
# are, then check that these internal attributes are part of nodeDict; if they are not, a compatibility
73-
# issue must be raised to warn the user, as this will automatically change the node's UID
74-
if not template:
75-
invalidatingIntInputs = []
76-
for attr in nodeDesc.internalInputs:
77-
if attr.invalidate:
78-
invalidatingIntInputs.append(attr.name)
79-
for attr in invalidatingIntInputs:
80-
if attr not in internalInputs.keys():
81-
compatibilityIssue = CompatibilityIssue.DescriptionConflict
82-
break
83-
84-
# Verify that all inputs match their descriptions
85-
for attrName, value in inputs.items():
86-
if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value):
87-
compatibilityIssue = CompatibilityIssue.DescriptionConflict
88-
break
89-
# Verify that all internal inputs match their description
90-
for attrName, value in internalInputs.items():
91-
if not CompatibilityNode.attributeDescFromName(nodeDesc.internalInputs, attrName, value):
92-
compatibilityIssue = CompatibilityIssue.DescriptionConflict
93-
break
94-
# Verify that all outputs match their descriptions
95-
for attrName, value in outputs.items():
96-
if not CompatibilityNode.attributeDescFromName(nodeDesc.outputs, attrName, value):
97-
compatibilityIssue = CompatibilityIssue.DescriptionConflict
98-
break
99-
100-
if compatibilityIssue is None:
101-
node = Node(nodeType, position, uid=uid, **inputs, **internalInputs, **outputs)
102-
else:
103-
logging.debug("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name))
104-
node = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue)
105-
# Retro-compatibility: no internal folder saved
106-
# can't spawn meaningful CompatibilityNode with precomputed outputs
107-
# => automatically try to perform node upgrade
108-
if not internalFolder and nodeDesc:
109-
logging.warning("No serialized output data: performing automatic upgrade on '{}'".format(name))
110-
node = node.upgrade()
111-
# If the node comes from a template file and there is a conflict, it should be upgraded anyway unless it is
112-
# an "unknown node type" conflict (in which case the upgrade would fail)
113-
elif template and compatibilityIssue is not CompatibilityIssue.UnknownNodeType:
114-
node = node.upgrade()
115-
116-
return node
64+
node = self._createNode()
65+
return node
66+
67+
def _normalizeNodeData(self):
68+
"""Consistency fixes for backward compatibility with older serialized data."""
69+
# Inputs were previously saved as "attributes".
70+
if "inputs" not in self.nodeData and "attributes" in self.nodeData:
71+
self.nodeData["inputs"] = self.nodeData["attributes"]
72+
del self.nodeData["attributes"]
73+
74+
def _checkCompatibilityIssues(self) -> Optional[CompatibilityIssue]:
75+
if self.nodeDesc is None:
76+
return CompatibilityIssue.UnknownNodeType
77+
78+
if not self._checkUidCompatibility():
79+
return CompatibilityIssue.UidConflict
80+
81+
if not self._checkVersionCompatibility():
82+
return CompatibilityIssue.VersionConflict
83+
84+
if not self._checkDescriptionCompatibility():
85+
return CompatibilityIssue.DescriptionConflict
86+
87+
return None
88+
89+
def _checkUidCompatibility(self) -> bool:
90+
return self.expectedUid is None or self.expectedUid == self.uid
91+
92+
def _checkVersionCompatibility(self) -> bool:
93+
# Special case: a node with a version set to None indicates
94+
# that it has been created from the current version of the node type.
95+
nodeCreatedFromCurrentVersion = self.version is None
96+
if nodeCreatedFromCurrentVersion:
97+
return True
98+
nodeTypeCurrentVersion = meshroom.core.nodeVersion(self.nodeDesc, "0.0")
99+
return Version(self.version).major == Version(nodeTypeCurrentVersion).major
100+
101+
def _checkDescriptionCompatibility(self) -> bool:
102+
# Only perform strict attribute name matching for non-template graphs,
103+
# since only non-default-value input attributes are serialized in templates.
104+
if not self.inTemplate:
105+
if not self._checkAttributesNamesMatchDescription():
106+
return False
107+
108+
return self._checkAttributesAreCompatibleWithDescription()
109+
110+
def _checkAttributesNamesMatchDescription(self) -> bool:
111+
return (
112+
self._checkInputAttributesNames()
113+
and self._checkOutputAttributesNames()
114+
and self._checkInternalAttributesNames()
115+
)
116+
117+
def _checkAttributesAreCompatibleWithDescription(self) -> bool:
118+
return (
119+
self._checkAttributesCompatibility(self.nodeDesc.inputs, self.inputs)
120+
and self._checkAttributesCompatibility(self.nodeDesc.internalInputs, self.internalInputs)
121+
and self._checkAttributesCompatibility(self.nodeDesc.outputs, self.outputs)
122+
)
123+
124+
def _checkInputAttributesNames(self) -> bool:
125+
def serializedInput(attr: desc.Attribute) -> bool:
126+
"""Filter that excludes not-serialized desc input attributes."""
127+
if isinstance(attr, desc.PushButtonParam):
128+
# PushButtonParam are not serialized has they do not hold a value.
129+
return False
130+
return True
131+
132+
refAttributes = filter(serializedInput, self.nodeDesc.inputs)
133+
return self._checkAttributesNamesStrictlyMatch(refAttributes, self.inputs)
134+
135+
def _checkOutputAttributesNames(self) -> bool:
136+
def serializedOutput(attr: desc.Attribute) -> bool:
137+
"""Filter that excludes not-serialized desc output attributes."""
138+
if attr.isDynamicValue:
139+
# Dynamic outputs values are not serialized with the node,
140+
# as their value is written in the computed output data.
141+
return False
142+
return True
143+
144+
refAttributes = filter(serializedOutput, self.nodeDesc.outputs)
145+
return self._checkAttributesNamesStrictlyMatch(refAttributes, self.outputs)
146+
147+
def _checkInternalAttributesNames(self) -> bool:
148+
invalidatingDescAttributes = [attr.name for attr in self.nodeDesc.internalInputs if attr.invalidate]
149+
return all(attr in self.internalInputs.keys() for attr in invalidatingDescAttributes)
150+
151+
def _checkAttributesNamesStrictlyMatch(
152+
self, descAttributes: Iterable[desc.Attribute], attributesDict: dict[str, Any]
153+
) -> bool:
154+
refNames = sorted([attr.name for attr in descAttributes])
155+
attrNames = sorted(attributesDict.keys())
156+
return refNames == attrNames
157+
158+
def _checkAttributesCompatibility(
159+
self, descAttributes: list[desc.Attribute], attributesDict: dict[str, Any]
160+
) -> bool:
161+
return all(
162+
CompatibilityNode.attributeDescFromName(descAttributes, attrName, value) is not None
163+
for attrName, value in attributesDict.items()
164+
)
165+
166+
def _createNode(self) -> Node:
167+
logging.info(f"Creating node '{self.name}'")
168+
return Node(
169+
self.nodeType,
170+
position=self.position,
171+
uid=self.uid,
172+
**self.inputs,
173+
**self.internalInputs,
174+
**self.outputs,
175+
)
176+
177+
def _createCompatibilityNode(self, compatibilityIssue) -> CompatibilityNode:
178+
logging.warning(f"Compatibility issue detected for node '{self.name}': {compatibilityIssue.name}")
179+
return CompatibilityNode(
180+
self.nodeType, self.nodeData, position=self.position, issue=compatibilityIssue
181+
)
182+
183+
def _tryUpgradeCompatibilityNode(self, node: CompatibilityNode) -> Union[Node, CompatibilityNode]:
184+
"""Handle possible upgrades of CompatibilityNodes, when no computed data is associated to the Node."""
185+
if node.issue == CompatibilityIssue.UnknownNodeType:
186+
return node
187+
188+
# Nodes in templates are not meant to hold computation data.
189+
if self.inTemplate:
190+
logging.warning(f"Compatibility issue in template: performing automatic upgrade on '{self.name}'")
191+
return node.upgrade()
192+
193+
# Backward compatibility: "internalFolder" was not serialized.
194+
if not self.internalFolder:
195+
logging.warning(f"No serialized output data: performing automatic upgrade on '{self.name}'")
196+
197+
return node

meshroom/ui/commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,11 +432,12 @@ def redoImpl(self):
432432

433433
def undoImpl(self):
434434
# delete upgraded node
435+
expectedUid = self.graph.node(self.nodeName).uid
435436
self.graph.removeNode(self.nodeName)
436437
# recreate compatibility node
437438
with GraphModification(self.graph):
438439
# We come back from an upgrade, so we enforce uidConflict=True as there was a uid conflict before
439-
node = nodeFactory(self.nodeDict, name=self.nodeName, uidConflict=True)
440+
node = nodeFactory(self.nodeDict, name=self.nodeName, expectedUid=expectedUid)
440441
self.graph.addNode(node, self.nodeName)
441442
# recreate out edges
442443
for dstAttr, srcAttr in self.outEdges.items():

0 commit comments

Comments
 (0)