|
1 | 1 | import logging |
| 2 | +from typing import Any, Iterable, Optional, Union |
2 | 3 |
|
3 | 4 | import meshroom.core |
4 | 5 | from meshroom.core import Version, desc |
5 | 6 | from meshroom.core.node import CompatibilityIssue, CompatibilityNode, Node, Position |
6 | 7 |
|
7 | 8 |
|
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]: |
9 | 15 | """ |
10 | 16 | Create a node instance by deserializing the given node data. |
11 | 17 | If the serialized data matches the corresponding node type description, a Node instance is created. |
12 | 18 | If any compatibility issue occurs, a NodeCompatibility instance is created instead. |
13 | 19 |
|
14 | 20 | 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. |
19 | 25 |
|
20 | 26 | Returns: |
21 | | - BaseNode: the created node |
| 27 | + The created Node instance. |
22 | 28 | """ |
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) |
59 | 63 | 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 |
0 commit comments