[core] introduce CompatibilityNode for improved scene compatibilities

Improve node serialization/deserialization to be able to recreate the exact same node in the graph when loading a meshroom project, even if the corresponding node's description has changed or does not exist anymore. This allows to recover already computed data on disk, without being impacted by changed uids. CompatibilityNode also provides an on-demand upgrade system to turn into a Node that meets the current node description (if possible).
 
* new abstract class BaseNode, base class for Node and CompatibiliyNode 
* Node: serialize everything needed to spawn a CompatibilityNode with precomputed outputs: inputs, uids, parallelization settings, unresolved internal folders and outputs
* node_factory: handles node deserialization and compatibility issues to create either a Node or a CompatibilityNode
* add compatibility unit tests
This commit is contained in:
Yann Lanthony 2018-07-15 13:13:44 +02:00
parent b6cbb0cc63
commit 33eb7f3a7f
6 changed files with 458 additions and 101 deletions

View file

@ -16,7 +16,15 @@ class UnknownNodeTypeError(GraphException):
""" """
Raised when asked to create a unknown node type. Raised when asked to create a unknown node type.
""" """
def __init__(self, nodeType): def __init__(self, nodeType, msg=None):
msg = "Unknown Node Type: " + nodeType msg = "Unknown Node Type: " + nodeType
super(UnknownNodeTypeError, self).__init__(msg) super(UnknownNodeTypeError, self).__init__(msg)
self.nodeType = nodeType self.nodeType = nodeType
class NodeUpgradeError(GraphException):
def __init__(self, nodeName, details=None):
msg = "Failed to upgrade node {}".format(nodeName)
if details:
msg += ": {}".format(details)
super(NodeUpgradeError, self).__init__(msg)

View file

@ -14,7 +14,8 @@ import meshroom
import meshroom.core import meshroom.core
from meshroom.common import BaseObject, DictModel, Slot, Signal, Property from meshroom.common import BaseObject, DictModel, Slot, Signal, Property
from meshroom.core.attribute import Attribute from meshroom.core.attribute import Attribute
from meshroom.core.node import node_factory, Status from meshroom.core.exception import UnknownNodeTypeError
from meshroom.core.node import node_factory, Status, Node, CompatibilityNode
# Replace default encoder to support Enums # Replace default encoder to support Enums
@ -207,10 +208,13 @@ class Graph(BaseObject):
if not isinstance(nodeData, dict): if not isinstance(nodeData, dict):
raise RuntimeError('loadGraph error: Node is not a dict. File: {}'.format(filepath)) raise RuntimeError('loadGraph error: Node is not a dict. File: {}'.format(filepath))
n = node_factory(nodeData['nodeType'], # retrieve version from
# allow simple retro-compatibility, though cache might get invalidated # 1. nodeData: node saved from a CompatibilityNode
skipInvalidAttributes=True, # 2. nodesVersion in file header: node saved from a Node
**nodeData['attributes']) # 3. fallback to no version "0.0": retro-compatibility
if "version" not in nodeData:
nodeData["version"] = nodesVersions.get(nodeData["nodeType"], "0.0")
n = node_factory(nodeData, nodeName)
# Add node to the graph with raw attributes values # Add node to the graph with raw attributes values
self._addNode(n, nodeName) self._addNode(n, nodeName)
@ -314,7 +318,7 @@ class Graph(BaseObject):
if name and name in self._nodes.keys(): if name and name in self._nodes.keys():
name = self._createUniqueNodeName(name) name = self._createUniqueNodeName(name)
n = self.addNode(node_factory(nodeType, False, **kwargs), uniqueName=name) n = self.addNode(Node(nodeType, **kwargs), uniqueName=name)
n.updateInternals() n.updateInternals()
return n return n
@ -329,6 +333,29 @@ class Graph(BaseObject):
def node(self, nodeName): def node(self, nodeName):
return self._nodes.get(nodeName) return self._nodes.get(nodeName)
def upgradeNode(self, nodeName):
"""
Upgrade the CompatibilityNode identified as 'nodeName'
Args:
nodeName (str): the name of the CompatibilityNode to upgrade
Returns:
the list of deleted input/output edges
"""
node = self.node(nodeName)
if not isinstance(node, CompatibilityNode):
raise ValueError("Upgrade is only available on CompatibilityNode instances.")
upgradedNode = node.upgrade()
inEdges, outEdges = self.removeNode(nodeName)
self.addNode(upgradedNode, nodeName)
for dst, src in outEdges.items():
try:
self.addEdge(self.attribute(src), self.attribute(dst))
except (KeyError, ValueError) as e:
logging.warning("Failed to restore edge {} -> {}: {}".format(src, dst, str(e)))
return inEdges, outEdges
@Slot(str, result=Attribute) @Slot(str, result=Attribute)
def attribute(self, fullName): def attribute(self, fullName):
# type: (str) -> Attribute # type: (str) -> Attribute
@ -670,7 +697,8 @@ class Graph(BaseObject):
self.header[Graph.IO.ReleaseVersion] = meshroom.__version__ self.header[Graph.IO.ReleaseVersion] = meshroom.__version__
self.header[Graph.IO.FileVersion] = Graph.IO.__version__ self.header[Graph.IO.FileVersion] = Graph.IO.__version__
usedNodeTypes = set([n.nodeDesc.__class__ for n in self._nodes]) # store versions of node types present in the graph (excluding CompatibilityNode instances)
usedNodeTypes = set([n.nodeDesc.__class__ for n in self._nodes if isinstance(n, Node)])
self.header[Graph.IO.NodesVersions] = { self.header[Graph.IO.NodesVersions] = {
"{}".format(p.__name__): meshroom.core.nodeVersion(p, "0.0") "{}".format(p.__name__): meshroom.core.nodeVersion(p, "0.0")

View file

@ -9,15 +9,15 @@ import re
import shutil import shutil
import time import time
import uuid import uuid
from abc import ABCMeta
from collections import defaultdict from collections import defaultdict
from enum import Enum from enum import Enum
import meshroom import meshroom
from meshroom.common import Signal, Variant, Property, BaseObject, Slot, ListModel, DictModel from meshroom.common import Signal, Variant, Property, BaseObject, Slot, ListModel, DictModel
from meshroom.core import desc, stats, hashValue from meshroom.core import desc, stats, hashValue, pyCompatibility
from meshroom.core.attribute import attribute_factory, ListAttribute, GroupAttribute, Attribute from meshroom.core.attribute import attribute_factory, ListAttribute, GroupAttribute, Attribute
from meshroom.core.exception import UnknownNodeTypeError from meshroom.core.exception import NodeUpgradeError, UnknownNodeTypeError
class Status(Enum): class Status(Enum):
@ -282,15 +282,17 @@ class NodeChunk(BaseObject):
statisticsFile = Property(str, statisticsFile.fget, notify=nodeFolderChanged) statisticsFile = Property(str, statisticsFile.fget, notify=nodeFolderChanged)
class Node(BaseObject): class BaseNode(BaseObject):
""" """
Base Abstract class for Graph nodes.
""" """
__metaclass__ = ABCMeta
# Regexp handling complex attribute names with recursive understanding of Lists and Groups # Regexp handling complex attribute names with recursive understanding of Lists and Groups
# i.e: a.b, a[0], a[0].b.c[1] # i.e: a.b, a[0], a[0].b.c[1]
attributeRE = re.compile(r'\.?(?P<name>\w+)(?:\[(?P<index>\d+)\])?') attributeRE = re.compile(r'\.?(?P<name>\w+)(?:\[(?P<index>\d+)\])?')
def __init__(self, nodeDesc, parent=None, **kwargs): def __init__(self, nodeType, parent=None, **kwargs):
""" """
Create a new Node instance based on the given node description. Create a new Node instance based on the given node description.
Any other keyword argument will be used to initialize this node's attributes. Any other keyword argument will be used to initialize this node's attributes.
@ -300,10 +302,16 @@ class Node(BaseObject):
parent (BaseObject): this Node's parent parent (BaseObject): this Node's parent
**kwargs: attributes values **kwargs: attributes values
""" """
super(Node, self).__init__(parent) super(BaseNode, self).__init__(parent)
self.nodeDesc = nodeDesc self._nodeType = nodeType
self.packageName = self.nodeDesc.packageName self.nodeDesc = None
self.packageVersion = self.nodeDesc.packageVersion
# instantiate node description if nodeType is valid
if nodeType in meshroom.core.nodesDesc:
self.nodeDesc = meshroom.core.nodesDesc[nodeType]()
self.packageName = self.packageVersion = ""
self._internalFolder = ""
self._name = None # type: str self._name = None # type: str
self.graph = None # type: Graph self.graph = None # type: Graph
@ -314,26 +322,21 @@ class Node(BaseObject):
self._size = 0 self._size = 0
self._attributes = DictModel(keyAttrName='name', parent=self) self._attributes = DictModel(keyAttrName='name', parent=self)
self.attributesPerUid = defaultdict(set) self.attributesPerUid = defaultdict(set)
self._initFromDesc()
for k, v in kwargs.items():
self.attribute(k).value = v
self._updateChunks()
def __getattr__(self, k): def __getattr__(self, k):
try: try:
# Throws exception if not in prototype chain # Throws exception if not in prototype chain
# return object.__getattribute__(self, k) # doesn't work in python2 # return object.__getattribute__(self, k) # doesn't work in python2
return object.__getattr__(self, k) return object.__getattr__(self, k)
except AttributeError: except AttributeError as e:
try: try:
return self.attribute(k) return self.attribute(k)
except KeyError: except KeyError:
raise AttributeError(k) raise e
def getName(self): def getName(self):
return self._name return self._name
@property @property
def packageFullName(self): def packageFullName(self):
return '-'.join([self.packageName, self.packageVersion]) return '-'.join([self.packageName, self.packageVersion])
@ -364,29 +367,13 @@ class Node(BaseObject):
def getAttributes(self): def getAttributes(self):
return self._attributes return self._attributes
def _initFromDesc(self):
# Init from class and instance members
for attrDesc in self.nodeDesc.inputs:
assert isinstance(attrDesc, meshroom.core.desc.Attribute)
self._attributes.add(attribute_factory(attrDesc, None, False, self))
for attrDesc in self.nodeDesc.outputs:
assert isinstance(attrDesc, meshroom.core.desc.Attribute)
self._attributes.add(attribute_factory(attrDesc, None, True, self))
# List attributes per uid
for attr in self._attributes:
for uidIndex in attr.attributeDesc.uid:
self.attributesPerUid[uidIndex].add(attr)
def _applyExpr(self): def _applyExpr(self):
for attr in self._attributes: for attr in self._attributes:
attr._applyExpr() attr._applyExpr()
@property @property
def nodeType(self): def nodeType(self):
return self.nodeDesc.__class__.__name__ return self._nodeType
@property @property
def depth(self): def depth(self):
@ -397,13 +384,7 @@ class Node(BaseObject):
return self.graph.getDepth(self, minimal=True) return self.graph.getDepth(self, minimal=True)
def toDict(self): def toDict(self):
attributes = {k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isInput} pass
return {
'nodeType': self.nodeType,
'packageName': self.packageName,
'packageVersion': self.packageVersion,
'attributes': {k: v for k, v in attributes.items() if v is not None}, # filter empty values
}
def _computeUids(self): def _computeUids(self):
""" Compute node uids by combining associated attributes' uids. """ """ Compute node uids by combining associated attributes' uids. """
@ -471,7 +452,7 @@ class Node(BaseObject):
""" Delete this Node internal folder. """ Delete this Node internal folder.
Status will be reset to Status.NONE Status will be reset to Status.NONE
""" """
if os.path.exists(self.internalFolder): if self.internalFolder and os.path.exists(self.internalFolder):
shutil.rmtree(self.internalFolder) shutil.rmtree(self.internalFolder)
self.updateStatusFromCache() self.updateStatusFromCache()
@ -508,25 +489,7 @@ class Node(BaseObject):
chunk.updateStatisticsFromCache() chunk.updateStatisticsFromCache()
def _updateChunks(self): def _updateChunks(self):
""" Update Node's computation task splitting into NodeChunks based on its description """ pass
self.setSize(self.nodeDesc.size.computeSize(self))
if self.isParallelized:
try:
ranges = self.nodeDesc.parallelization.getRanges(self)
if len(ranges) != len(self._chunks):
self._chunks.setObjectList([NodeChunk(self, range) for range in ranges])
else:
for chunk, range in zip(self._chunks, ranges):
chunk.range = range
except RuntimeError:
# TODO: set node internal status to error
logging.warning("Invalid Parallelization on node {}".format(self._name))
self._chunks.clear()
else:
if len(self._chunks) != 1:
self._chunks.setObjectList([NodeChunk(self, desc.Range())])
else:
self._chunks[0].range = desc.Range()
def updateInternals(self, cacheDir=None): def updateInternals(self, cacheDir=None):
""" Update Node's internal parameters and output attributes. """ Update Node's internal parameters and output attributes.
@ -558,7 +521,7 @@ class Node(BaseObject):
@property @property
def internalFolder(self): def internalFolder(self):
return self.nodeDesc.internalFolder.format(**self._cmdVars) return self._internalFolder.format(**self._cmdVars)
def updateStatusFromCache(self): def updateStatusFromCache(self):
""" """
@ -623,42 +586,286 @@ class Node(BaseObject):
size = Property(int, getSize, notify=sizeChanged) size = Property(int, getSize, notify=sizeChanged)
def node_factory(nodeType, skipInvalidAttributes=False, **attributes): class Node(BaseNode):
""" """
Create a new Node of type NodeType and initialize its attributes with given kwargs. A standard Graph node based on a node type.
"""
def __init__(self, nodeType, parent=None, **kwargs):
super(Node, self).__init__(nodeType, parent, **kwargs)
if not self.nodeDesc:
raise UnknownNodeTypeError(nodeType)
self.packageName = self.nodeDesc.packageName
self.packageVersion = self.nodeDesc.packageVersion
self._internalFolder = self.nodeDesc.internalFolder
for attrDesc in self.nodeDesc.inputs:
self._attributes.add(attribute_factory(attrDesc, None, False, self))
for attrDesc in self.nodeDesc.outputs:
self._attributes.add(attribute_factory(attrDesc, None, True, self))
# List attributes per uid
for attr in self._attributes:
for uidIndex in attr.attributeDesc.uid:
self.attributesPerUid[uidIndex].add(attr)
# initialize attribute values
for k, v in kwargs.items():
attr = self.attribute(k)
if attr.isInput:
self.attribute(k).value = v
def toDict(self):
inputs = {k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isInput}
outputs = ({k: v.getExportValue() for k, v in self._attributes.objects.items() if v.isOutput})
return {
'nodeType': self.nodeType,
'parallelization': {
'blockSize': self.nodeDesc.parallelization.blockSize if self.isParallelized else 0,
'size': self.size,
'split': self.nbParallelizationBlocks
},
'uids': self._uids,
'internalFolder': self._internalFolder,
'attributes': {k: v for k, v in inputs.items() if v is not None}, # filter empty values
'outputs': outputs,
}
def _updateChunks(self):
""" Update Node's computation task splitting into NodeChunks based on its description """
self.setSize(self.nodeDesc.size.computeSize(self))
if self.isParallelized:
try:
ranges = self.nodeDesc.parallelization.getRanges(self)
if len(ranges) != len(self._chunks):
self._chunks.setObjectList([NodeChunk(self, range) for range in ranges])
else:
for chunk, range in zip(self._chunks, ranges):
chunk.range = range
except RuntimeError:
# TODO: set node internal status to error
logging.warning("Invalid Parallelization on node {}".format(self._name))
self._chunks.clear()
else:
if len(self._chunks) != 1:
self._chunks.setObjectList([NodeChunk(self, desc.Range())])
else:
self._chunks[0].range = desc.Range()
class CompatibilityIssue(Enum):
"""
Enum describing compatibility issues when deserializing a Node.
"""
UnknownIssue = 0 # unknown issue fallback
UnknownNodeType = 1 # the node type has no corresponding description class
VersionConflict = 2 # mismatch between node's description version and serialized node data
DescriptionConflict = 3 # mismatch between node's description attributes and serialized node data
UidConflict = 4 # mismatch between computed uids and uids stored in serialized node data
class CompatibilityNode(BaseNode):
"""
Fallback BaseNode subclass to instantiate Nodes having compatibility issues with current type description.
CompatibilityNode creates an 'empty-shell' exposing the deserialized node as-is,
with all its inputs and precomputed outputs.
"""
def __init__(self, nodeType, nodeDict, issue=CompatibilityIssue.UnknownIssue, parent=None):
super(CompatibilityNode, self).__init__(nodeType, parent)
self.issue = issue
self.nodeDict = nodeDict
self.inputs = nodeDict.get("inputs", {})
self.outputs = nodeDict.get("outputs", {})
self._internalFolder = self.nodeDict.get("internalFolder", "")
self._uids = self.nodeDict.get("uids", {})
# restore parallelization settings
self.parallelization = self.nodeDict.get("parallelization", {})
self.splitCount = self.parallelization.get("split", 1)
self.setSize(self.parallelization.get("size", 1))
# inputs matching current type description
self._commonInputs = []
# create input attributes
for attrName, value in self.inputs.items():
matchDesc = self._addAttribute(attrName, value, False)
# store attributes that could be used during node upgrade
if matchDesc:
self._commonInputs.append(attrName)
# create outputs attributes
for attrName, value in self.outputs.items():
self._addAttribute(attrName, value, True)
# create NodeChunks matching serialized parallelization settings
self._chunks.setObjectList([
NodeChunk(self, desc.Range(i, blockSize=self.parallelization.get("blockSize", 0)))
for i in range(self.splitCount)
])
@staticmethod
def attributeDescFromValue(attrName, value, isOutput):
"""
Generate an attribute description (desc.Attribute) that best matches 'value'.
Args:
attrName (str): the name of the attribute
value: the value of the attribute
isOutput (bool): whether the attribute is an output
Returns:
desc.Attribute: the generated attribute description
"""
params = {
"name": attrName, "label": attrName,
"description": "Incompatible parameter",
"value": value, "uid": (),
"group": "incompatible"
}
if isinstance(value, bool):
return desc.BoolParam(**params)
if isinstance(value, int):
return desc.IntParam(range=None, **params)
elif isinstance(value, float):
return desc.FloatParam(range=None, **params)
elif isinstance(value, pyCompatibility.basestring):
if isOutput or os.path.isabs(value) or Attribute.isLinkExpression(value):
return desc.File(**params)
else:
return desc.StringParam(**params)
# handle any other type of parameters (List/Group) as Strings
return desc.StringParam(**params)
@staticmethod
def attributeDescFromName(refAttributes, name, value):
"""
Try to find a matching attribute description in refAttributes for given attribute 'name' and 'value'.
Args:
refAttributes ([Attribute]): reference Attributes to look for a description
name (str): attribute's name
value: attribute's value
Returns:
desc.Attribute: an attribute description from refAttributes if a match is found, None otherwise.
"""
# from original node description based on attribute's name
attrDesc = next((d for d in refAttributes if d.name == name), None)
if attrDesc:
# ensure value is valid for this description
try:
attrDesc.validateValue(value)
except ValueError:
attrDesc = None
return attrDesc
def _addAttribute(self, name, val, isOutput):
"""
Add a new attribute on this node.
Args:
name (str): the name of the attribute
val: the attribute's value
isOutput: whether the attribute is an output
Returns:
bool: whether the attribute exists in the node description
"""
attrDesc = None
if self.nodeDesc:
refAttrs = self.nodeDesc.outputs if isOutput else self.nodeDesc.inputs
attrDesc = CompatibilityNode.attributeDescFromName(refAttrs, name, val)
matchDesc = attrDesc is not None
if not matchDesc:
attrDesc = CompatibilityNode.attributeDescFromValue(name, val, isOutput)
attribute = attribute_factory(attrDesc, val, isOutput, self)
self._attributes.add(attribute)
return matchDesc
def toDict(self):
"""
Return the original serialized node that generated a compatibility issue.
"""
return self.nodeDict
@property
def canUpgrade(self):
""" Return whether the node can be upgraded.
This is the case when the underlying node type has a corresponding description. """
return self.nodeDesc is not None
def upgrade(self):
"""
Return a new Node instance based on original node type with common inputs initialized.
"""
if not self.canUpgrade:
raise NodeUpgradeError(self.name, "no matching node type")
# TODO: use upgrade method of node description if available
return Node(self.nodeType, **{key: value for key, value in self.inputs.items() if key in self._commonInputs})
def node_factory(nodeDict, name=None):
"""
Create a node instance by deserializing the given node data.
If the serialized data matches the corresponding node type description, a Node instance is created.
If any compatibility issue occurs, a NodeCompatibility instance is created instead.
Args: Args:
nodeType (str): name of the node description class nodeDict (dict): the serialization of the node
skipInvalidAttributes (bool): whether to skip attributes not defined in name (str): (optional) the node's name
or incompatible with nodeType's description.
attributes (): serialized nodes attributes
Raises: Returns:
UnknownNodeTypeError if nodeType is unknown BaseNode: the created node
""" """
nodeType = nodeDict["nodeType"]
# get node inputs/outputs
if "inputs" not in nodeDict:
# retro-compatibility: inputs were previously saved as "attributes"
nodeDict["inputs"] = nodeDict.get("attributes", {})
inputs = nodeDict.get("inputs", {})
outputs = nodeDict.get("outputs", {})
version = nodeDict.get("version", None)
internalFolder = nodeDict.get("internalFolder", None)
compatibilityIssue = None
nodeDesc = None
try: try:
nodeDesc = meshroom.core.nodesDesc[nodeType]() nodeDesc = meshroom.core.nodesDesc[nodeType]
except KeyError: except KeyError:
# unknown node type # unknown node type
raise UnknownNodeTypeError(nodeType) compatibilityIssue = CompatibilityIssue.UnknownNodeType
if skipInvalidAttributes: if nodeDesc:
# compare given attributes with the ones from node desc # compare serialized node version with current node version
descAttrNames = set([attr.name for attr in nodeDesc.inputs]) currentNodeVersion = meshroom.core.nodeVersion(nodeDesc)
attrNames = set([name for name in attributes.keys()]) # if both versions are available, check for incompatibility in major version
invalidAttributes = list(attrNames.difference(descAttrNames)) if version and currentNodeVersion and version.split('.')[0] != currentNodeVersion.split('.')[0]:
commonAttributes = list(attrNames.intersection(descAttrNames)) compatibilityIssue = CompatibilityIssue.VersionConflict
# compare value types for common attributes # in other cases, check attributes compatibility between serialized node and its description
for attr in [attr for attr in nodeDesc.inputs if attr.name in commonAttributes]: else:
try: descAttrNames = set([attr.name for attr in nodeDesc.inputs + nodeDesc.outputs])
attr.validateValue(attributes[attr.name]) attrNames = set([name for name in list(inputs.keys()) + list(outputs.keys())])
except: if attrNames != descAttrNames:
invalidAttributes.append(attr.name) compatibilityIssue = CompatibilityIssue.DescriptionConflict
if invalidAttributes and skipInvalidAttributes: # no compatibility issues: instantiate a Node
# filter out invalid attributes if compatibilityIssue is None:
logging.info("Skipping invalid attributes initialization for {}: {}".format(nodeType, invalidAttributes)) n = Node(nodeType, **inputs)
for attr in invalidAttributes: # otherwise, instantiate a CompatibilityNode
del attributes[attr] else:
logging.warning("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name))
n = CompatibilityNode(nodeType, nodeDict, compatibilityIssue)
# retro-compatibility: no internal folder saved
# can't spawn meaningful CompatibilityNode with precomputed outputs
# => automatically try to perform node upgrade
if not internalFolder and nodeDesc:
logging.warning("No serialized output data: performing automatic upgrade on '{}'".format(name))
n = n.upgrade()
return Node(nodeDesc, **attributes) return n

View file

@ -7,6 +7,7 @@ from PySide2.QtCore import Property, Signal
from meshroom.core.attribute import ListAttribute, Attribute from meshroom.core.attribute import ListAttribute, Attribute
from meshroom.core.graph import GraphModification from meshroom.core.graph import GraphModification
from meshroom.core.node import node_factory
class UndoCommand(QUndoCommand): class UndoCommand(QUndoCommand):
@ -125,8 +126,8 @@ class RemoveNodeCommand(GraphCommand):
def undoImpl(self): def undoImpl(self):
with GraphModification(self.graph): with GraphModification(self.graph):
node = self.graph.addNewNode(nodeType=self.nodeDict["nodeType"], node = node_factory(self.nodeDict, self.nodeName)
name=self.nodeName, **self.nodeDict["attributes"]) self.graph.addNode(node, self.nodeName)
assert (node.getName() == self.nodeName) assert (node.getName() == self.nodeName)
# recreate out edges deleted on node removal # recreate out edges deleted on node removal
for dstAttr, srcAttr in self.outEdges.items(): for dstAttr, srcAttr in self.outEdges.items():

View file

@ -356,8 +356,7 @@ class Reconstruction(UIGraph):
# If cameraInit is None (i.e: SfM augmentation): # If cameraInit is None (i.e: SfM augmentation):
# * create an uninitialized node # * create an uninitialized node
# * wait for the result before actually creating new nodes in the graph (see onIntrinsicsAvailable) # * wait for the result before actually creating new nodes in the graph (see onIntrinsicsAvailable)
attributes = cameraInit.toDict()["attributes"] if cameraInit else {} cameraInitCopy = node_factory(cameraInit.toDict())
cameraInitCopy = node_factory("CameraInit", **attributes)
try: try:
self.setBuildingIntrinsics(True) self.setBuildingIntrinsics(True)

114
tests/test_compatibility.py Normal file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env python
# coding:utf-8
import tempfile
import os
import pytest
import meshroom.core
from meshroom.core import desc, registerNodeType, unregisterNodeType
from meshroom.core.exception import NodeUpgradeError
from meshroom.core.graph import Graph, loadGraph
from meshroom.core.node import CompatibilityNode, CompatibilityIssue, Node
class SampleNodeV1(desc.Node):
""" Version 1 Sample Node """
inputs = [
desc.File(name='input', label='Input', description='', value='', uid=[0],),
desc.StringParam(name='paramA', label='ParamA', description='', value='', uid=[]) # No impact on UID
]
outputs = [
desc.File(name='output', label='Output', description='', value=desc.Node.internalFolder, uid=[])
]
class SampleNodeV2(desc.Node):
""" Changes from V1:
* 'input' has been renamed to 'in'
"""
inputs = [
desc.File(name='in', label='Input', description='', value='', uid=[0],),
desc.StringParam(name='paramA', label='ParamA', description='', value='', uid=[]) # No impact on UID
]
outputs = [
desc.File(name='output', label='Output', description='', value=desc.Node.internalFolder, uid=[])
]
def test_unknown_node_type():
"""
Test compatibility behavior for unknown node type.
"""
registerNodeType(SampleNodeV1)
g = Graph('')
n = g.addNewNode("SampleNodeV1", input="/dev/null", paramA="foo")
graphFile = os.path.join(tempfile.mkdtemp(), "test_unknown_node_type.mg")
g.save(graphFile)
internalFolder = n.internalFolder
nodeName = n.name
unregisterNodeType(SampleNodeV1)
# reload file
g = loadGraph(graphFile)
os.remove(graphFile)
assert len(g.nodes) == 1
n = g.node(nodeName)
# SampleNodeV1 is now an unknown type
# check node instance type and compatibility issue type
assert isinstance(n, CompatibilityNode)
assert n.issue == CompatibilityIssue.UnknownNodeType
# check if attributes are properly restored
assert len(n.attributes) == 3
assert n.input.isInput
assert n.output.isOutput
# check if internal folder
assert n.internalFolder == internalFolder
# upgrade can't be perform on unknown node types
assert not n.canUpgrade
with pytest.raises(NodeUpgradeError):
g.upgradeNode(nodeName)
def test_description_conflict():
"""
Test compatibility behavior for conflicting node descriptions.
"""
registerNodeType(SampleNodeV1)
g = Graph('')
n = g.addNewNode("SampleNodeV1")
graphFile = os.path.join(tempfile.mkdtemp(), "test_description_conflict.mg")
g.save(graphFile)
internalFolder = n.internalFolder
nodeName = n.name
# replace SampleNodeV1 by SampleNodeV2
# 'SampleNodeV1' is still registered but implementation has changed
meshroom.core.nodesDesc[SampleNodeV1.__name__] = SampleNodeV2
# reload file
g = loadGraph(graphFile)
os.remove(graphFile)
assert len(g.nodes) == 1
n = g.node(nodeName)
# Node description clashes between what has been saved
assert isinstance(n, CompatibilityNode)
assert n.issue == CompatibilityIssue.DescriptionConflict
assert len(n.attributes) == 3
assert hasattr(n, "input")
assert not hasattr(n, "in")
assert n.internalFolder == internalFolder
# perform upgrade
g.upgradeNode(nodeName)
n = g.node(nodeName)
assert isinstance(n, Node)
assert not hasattr(n, "input")
assert hasattr(n, "in")
# check uid has changed (not the same set of attributes)
assert n.internalFolder != internalFolder