[core] Simplify invalidation status for all the attributes

The UID system based on a UID index is removed and replaced by a single
UID per node.

Attributes will be included in the UID computation if the `invalidate`
is set to `True` in their description. This replaces the `uid=[]` /
`uid=[0]` element of the description.
This commit is contained in:
Candice Bentéjac 2024-09-05 16:48:19 +02:00
parent 248c301c5a
commit 9973298746
4 changed files with 142 additions and 151 deletions

View file

@ -272,22 +272,21 @@ class Attribute(BaseObject):
def isInput(self): def isInput(self):
return not self._isOutput return not self._isOutput
def uid(self, uidIndex=-1): def uid(self):
""" """
Compute the UID for the attribute.
""" """
# 'uidIndex' should be in 'self.desc.uid' but in the case of linked attribute
# it will not be the case (so we cannot have an assert).
if self.isOutput: if self.isOutput:
if self.desc.isDynamicValue: if self.desc.isDynamicValue:
# If the attribute is a dynamic output, the UID is derived from the node UID. # If the attribute is a dynamic output, the UID is derived from the node UID.
# To guarantee that each output attribute receives a unique ID, we add the attribute name to it. # To guarantee that each output attribute receives a unique ID, we add the attribute name to it.
return hashValue((self.name, self.node._uids.get(uidIndex))) return hashValue((self.name, self.node._uid))
else: else:
# only dependent on the hash of its value without the cache folder # only dependent on the hash of its value without the cache folder
return hashValue(self._invalidationValue) return hashValue(self._invalidationValue)
if self.isLink: if self.isLink:
linkParam = self.getLinkParam(recursive=True) linkParam = self.getLinkParam(recursive=True)
return linkParam.uid(uidIndex) return linkParam.uid()
if isinstance(self._value, (list, tuple, set,)): if isinstance(self._value, (list, tuple, set,)):
# non-exclusive choice param # non-exclusive choice param
# hash of sorted values hashed # hash of sorted values hashed
@ -610,14 +609,14 @@ class ListAttribute(Attribute):
self.requestGraphUpdate() self.requestGraphUpdate()
self.valueChanged.emit() self.valueChanged.emit()
def uid(self, uidIndex): def uid(self):
if isinstance(self.value, ListModel): if isinstance(self.value, ListModel):
uids = [] uids = []
for value in self.value: for value in self.value:
if uidIndex in value.desc.uid: if value.desc.invalidate:
uids.append(value.uid(uidIndex)) uids.append(value.uid())
return hashValue(uids) return hashValue(uids)
return super(ListAttribute, self).uid(uidIndex) return super(ListAttribute, self).uid()
def _applyExpr(self): def _applyExpr(self):
if not self.node.graph: if not self.node.graph:
@ -747,11 +746,11 @@ class GroupAttribute(Attribute):
except KeyError: except KeyError:
return None return None
def uid(self, uidIndex): def uid(self):
uids = [] uids = []
for k, v in self._value.items(): for k, v in self._value.items():
if v.enabled and uidIndex in v.desc.uid: if v.enabled and v.desc.invalidate:
uids.append(v.uid(uidIndex)) uids.append(v.uid())
return hashValue(uids) return hashValue(uids)
def _applyExpr(self): def _applyExpr(self):

View file

@ -15,14 +15,14 @@ class Attribute(BaseObject):
""" """
""" """
def __init__(self, name, label, description, value, advanced, semantic, uid, group, enabled, uidIgnoreValue=None, def __init__(self, name, label, description, value, advanced, semantic, invalidate, group, enabled, uidIgnoreValue=None,
validValue=True, errorMessage="", visible=True, exposed=False): validValue=True, errorMessage="", visible=True, exposed=False):
super(Attribute, self).__init__() super(Attribute, self).__init__()
self._name = name self._name = name
self._label = label self._label = label
self._description = description self._description = description
self._value = value self._value = value
self._uid = uid self._invalidate = invalidate
self._group = group self._group = group
self._advanced = advanced self._advanced = advanced
self._enabled = enabled self._enabled = enabled
@ -84,7 +84,7 @@ class Attribute(BaseObject):
# The default value of the attribute's descriptor is None, so it's not an input value, # The default value of the attribute's descriptor is None, so it's not an input value,
# but an output value that is computed during the Node's process execution. # but an output value that is computed during the Node's process execution.
isDynamicValue = Property(bool, lambda self: self._isDynamicValue, constant=True) isDynamicValue = Property(bool, lambda self: self._isDynamicValue, constant=True)
uid = Property(Variant, lambda self: self._uid, constant=True) invalidate = Property(Variant, lambda self: self._invalidate, constant=True)
group = Property(str, lambda self: self._group, constant=True) group = Property(str, lambda self: self._group, constant=True)
advanced = Property(bool, lambda self: self._advanced, constant=True) advanced = Property(bool, lambda self: self._advanced, constant=True)
enabled = Property(Variant, lambda self: self._enabled, constant=True) enabled = Property(Variant, lambda self: self._enabled, constant=True)
@ -114,7 +114,7 @@ class ListAttribute(Attribute):
""" """
self._elementDesc = elementDesc self._elementDesc = elementDesc
self._joinChar = joinChar self._joinChar = joinChar
super(ListAttribute, self).__init__(name=name, label=label, description=description, value=[], uid=(), group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) super(ListAttribute, self).__init__(name=name, label=label, description=description, value=[], invalidate=False, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
def getInstanceType(self): def getInstanceType(self):
# Import within the method to prevent cyclic dependencies # Import within the method to prevent cyclic dependencies
@ -149,7 +149,7 @@ class ListAttribute(Attribute):
return True return True
elementDesc = Property(Attribute, lambda self: self._elementDesc, constant=True) elementDesc = Property(Attribute, lambda self: self._elementDesc, constant=True)
uid = Property(Variant, lambda self: self.elementDesc.uid, constant=True) invalidate = Property(Variant, lambda self: self.elementDesc.invalidate, constant=True)
joinChar = Property(str, lambda self: self._joinChar, constant=True) joinChar = Property(str, lambda self: self._joinChar, constant=True)
@ -162,7 +162,7 @@ class GroupAttribute(Attribute):
self._groupDesc = groupDesc self._groupDesc = groupDesc
self._joinChar = joinChar self._joinChar = joinChar
self._brackets = brackets self._brackets = brackets
super(GroupAttribute, self).__init__(name=name, label=label, description=description, value={}, uid=(), group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) super(GroupAttribute, self).__init__(name=name, label=label, description=description, value={}, invalidate=False, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
def getInstanceType(self): def getInstanceType(self):
# Import within the method to prevent cyclic dependencies # Import within the method to prevent cyclic dependencies
@ -239,14 +239,14 @@ class GroupAttribute(Attribute):
return matchCount > 0 return matchCount > 0
def retrieveChildrenUids(self): def retrieveChildrenInvalidations(self):
allUids = [] allInvalidations = []
for desc in self._groupDesc: for desc in self._groupDesc:
allUids.extend(desc.uid) allInvalidations.append(desc.invalidate)
return allUids return allInvalidations
groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True) groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True)
uid = Property(Variant, retrieveChildrenUids, constant=True) invalidate = Property(Variant, retrieveChildrenInvalidations, constant=True)
joinChar = Property(str, lambda self: self._joinChar, constant=True) joinChar = Property(str, lambda self: self._joinChar, constant=True)
brackets = Property(str, lambda self: self._brackets, constant=True) brackets = Property(str, lambda self: self._brackets, constant=True)
@ -254,16 +254,16 @@ class GroupAttribute(Attribute):
class Param(Attribute): class Param(Attribute):
""" """
""" """
def __init__(self, name, label, description, value, uid, group, advanced, semantic, enabled, uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False): def __init__(self, name, label, description, value, invalidate, group, advanced, semantic, enabled, uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False):
super(Param, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, super(Param, self).__init__(name=name, label=label, description=description, value=value, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled,
uidIgnoreValue=uidIgnoreValue, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) uidIgnoreValue=uidIgnoreValue, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed)
class File(Attribute): class File(Attribute):
""" """
""" """
def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, visible=True, exposed=True): def __init__(self, name, label, description, value, invalidate, group='allParams', advanced=False, semantic='', enabled=True, visible=True, exposed=True):
super(File, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) super(File, self).__init__(name=name, label=label, description=description, value=value, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
self._valueType = str self._valueType = str
def validateValue(self, value): def validateValue(self, value):
@ -284,8 +284,8 @@ class File(Attribute):
class BoolParam(Param): class BoolParam(Param):
""" """
""" """
def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, visible=True, exposed=False): def __init__(self, name, label, description, value, invalidate, group='allParams', advanced=False, semantic='', enabled=True, visible=True, exposed=False):
super(BoolParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) super(BoolParam, self).__init__(name=name, label=label, description=description, value=value, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
self._valueType = bool self._valueType = bool
def validateValue(self, value): def validateValue(self, value):
@ -308,9 +308,9 @@ class BoolParam(Param):
class IntParam(Param): class IntParam(Param):
""" """
""" """
def __init__(self, name, label, description, value, range, uid, group='allParams', advanced=False, semantic='', enabled=True, validValue=True, errorMessage="", visible=True, exposed=False): def __init__(self, name, label, description, value, range, invalidate, group='allParams', advanced=False, semantic='', enabled=True, validValue=True, errorMessage="", visible=True, exposed=False):
self._range = range self._range = range
super(IntParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, super(IntParam, self).__init__(name=name, label=label, description=description, value=value, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled,
validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed)
self._valueType = int self._valueType = int
@ -334,9 +334,9 @@ class IntParam(Param):
class FloatParam(Param): class FloatParam(Param):
""" """
""" """
def __init__(self, name, label, description, value, range, uid, group='allParams', advanced=False, semantic='', enabled=True, validValue=True, errorMessage="", visible=True, exposed=False): def __init__(self, name, label, description, value, range, invalidate, group='allParams', advanced=False, semantic='', enabled=True, validValue=True, errorMessage="", visible=True, exposed=False):
self._range = range self._range = range
super(FloatParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, super(FloatParam, self).__init__(name=name, label=label, description=description, value=value, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled,
validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed)
self._valueType = float self._valueType = float
@ -358,8 +358,8 @@ class FloatParam(Param):
class PushButtonParam(Param): class PushButtonParam(Param):
""" """
""" """
def __init__(self, name, label, description, uid, group='allParams', advanced=False, semantic='', enabled=True, visible=True, exposed=False): def __init__(self, name, label, description, invalidate, group='allParams', advanced=False, semantic='', enabled=True, visible=True, exposed=False):
super(PushButtonParam, self).__init__(name=name, label=label, description=description, value=None, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) super(PushButtonParam, self).__init__(name=name, label=label, description=description, value=None, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
self._valueType = None self._valueType = None
def getInstanceType(self): def getInstanceType(self):
@ -377,10 +377,10 @@ class PushButtonParam(Param):
class ChoiceParam(Param): class ChoiceParam(Param):
""" """
""" """
def __init__(self, name, label, description, value, values, exclusive, uid, group='allParams', joinChar=' ', advanced=False, semantic='', def __init__(self, name, label, description, value, values, exclusive, invalidate, group='allParams', joinChar=' ', advanced=False, semantic='',
enabled=True, validValue=True, errorMessage="", visible=True, exposed=False): enabled=True, validValue=True, errorMessage="", visible=True, exposed=False):
assert values assert values
super(ChoiceParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, super(ChoiceParam, self).__init__(name=name, label=label, description=description, value=value, invalidate=invalidate, group=group, advanced=advanced,
semantic=semantic, enabled=enabled, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) semantic=semantic, enabled=enabled, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed)
self._values = values self._values = values
self._exclusive = exclusive self._exclusive = exclusive
@ -446,8 +446,8 @@ class ChoiceParam(Param):
class StringParam(Param): class StringParam(Param):
""" """
""" """
def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False): def __init__(self, name, label, description, value, invalidate, group='allParams', advanced=False, semantic='', enabled=True, uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False):
super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, super(StringParam, self).__init__(name=name, label=label, description=description, value=value, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled,
uidIgnoreValue=uidIgnoreValue, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) uidIgnoreValue=uidIgnoreValue, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed)
self._valueType = str self._valueType = str
@ -467,8 +467,8 @@ class StringParam(Param):
class ColorParam(Param): class ColorParam(Param):
""" """
""" """
def __init__(self, name, label, description, value, uid, group='allParams', advanced=False, semantic='', enabled=True, visible=True, exposed=False): def __init__(self, name, label, description, value, invalidate, group='allParams', advanced=False, semantic='', enabled=True, visible=True, exposed=False):
super(ColorParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) super(ColorParam, self).__init__(name=name, label=label, description=description, value=value, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
self._valueType = str self._valueType = str
def validateValue(self, value): def validateValue(self, value):
@ -616,7 +616,7 @@ class StaticNodeSize(object):
class Node(object): class Node(object):
""" """
""" """
internalFolder = '{cache}/{nodeType}/{uid0}/' internalFolder = '{cache}/{nodeType}/{uid}/'
cpu = Level.NORMAL cpu = Level.NORMAL
gpu = Level.NONE gpu = Level.NONE
ram = Level.NORMAL ram = Level.NORMAL
@ -631,7 +631,7 @@ class Node(object):
"It is displayed in bold font in the invalidation/comment messages tooltip.", "It is displayed in bold font in the invalidation/comment messages tooltip.",
value="", value="",
semantic="multiline", semantic="multiline",
uid=[0], invalidate=True,
advanced=True, advanced=True,
uidIgnoreValue="", # If the invalidation string is empty, it does not participate to the node's UID uidIgnoreValue="", # If the invalidation string is empty, it does not participate to the node's UID
), ),
@ -642,21 +642,21 @@ class Node(object):
"It is displayed in regular font in the invalidation/comment messages tooltip.", "It is displayed in regular font in the invalidation/comment messages tooltip.",
value="", value="",
semantic="multiline", semantic="multiline",
uid=[], invalidate=False,
), ),
StringParam( StringParam(
name="label", name="label",
label="Node's Label", label="Node's Label",
description="Customize the default label (to replace the technical name of the node instance).", description="Customize the default label (to replace the technical name of the node instance).",
value="", value="",
uid=[], invalidate=False,
), ),
ColorParam( ColorParam(
name="color", name="color",
label="Color", label="Color",
description="Custom color for the node (SVG name or hexadecimal code).", description="Custom color for the node (SVG name or hexadecimal code).",
value="", value="",
uid=[], invalidate=False,
) )
] ]
inputs = [] inputs = []
@ -780,7 +780,7 @@ class CommandLineNode(Node):
finally: finally:
chunk.subprocess = None chunk.subprocess = None
#specific command line node for alicevision apps # Specific command line node for AliceVision apps
class AVCommandLineNode(CommandLineNode): class AVCommandLineNode(CommandLineNode):
cgroupParsed = False cgroupParsed = False
@ -805,7 +805,6 @@ class AVCommandLineNode(CommandLineNode):
AVCommandLineNode.cgroupParsed = True AVCommandLineNode.cgroupParsed = True
def buildCommandLine(self, chunk): def buildCommandLine(self, chunk):
commandLineString = super(AVCommandLineNode, self).buildCommandLine(chunk) commandLineString = super(AVCommandLineNode, self).buildCommandLine(chunk)
return commandLineString + AVCommandLineNode.cmdMem + AVCommandLineNode.cmdCore return commandLineString + AVCommandLineNode.cmdMem + AVCommandLineNode.cmdCore

View file

@ -315,8 +315,8 @@ class Graph(BaseObject):
# If no filepath is being set but the graph is not a template, trigger an updateInternals either way. # If no filepath is being set but the graph is not a template, trigger an updateInternals either way.
self.updateInternals() self.updateInternals()
# By this point, the graph has been fully loaded and an updateInternals has been triggered, so all the nodes' # By this point, the graph has been fully loaded and an updateInternals has been triggered, so all the
# links have been resolved and their UID computations are all complete. # nodes' links have been resolved and their UID computations are all complete.
# It is now possible to check whether the UIDs stored in the graph file for each node correspond to the ones # It is now possible to check whether the UIDs stored in the graph file for each node correspond to the ones
# that were computed. # that were computed.
if not isTemplate: # UIDs are not stored in templates if not isTemplate: # UIDs are not stored in templates
@ -342,13 +342,9 @@ class Graph(BaseObject):
for nodeName, nodeData in sorted(data.items(), key=lambda x: self.getNodeIndexFromName(x[0])): for nodeName, nodeData in sorted(data.items(), key=lambda x: self.getNodeIndexFromName(x[0])):
node = self.node(nodeName) node = self.node(nodeName)
savedUid = nodeData.get("uids", {}) # Node's UID from the graph file savedUid = nodeData.get("uid", None)
# JSON enfore keys to be strings, see graphUid = node._uid # Node's UID from the graph itself
# https://docs.python.org/3.8/library/json.html#json.dump
# We know our keys are integers, so we convert them back to int.
savedUid = {int(k): v for k, v in savedUid.items()}
graphUid = node._uids # Node's UID from the graph itself
if savedUid != graphUid and graphUid is not None: if savedUid != graphUid and graphUid is not None:
# Different UIDs, remove the existing node from the graph and replace it with a CompatibilityNode # Different UIDs, remove the existing node from the graph and replace it with a CompatibilityNode
logging.debug("UID conflict detected for {}".format(nodeName)) logging.debug("UID conflict detected for {}".format(nodeName))
@ -1382,7 +1378,7 @@ class Graph(BaseObject):
del graph[nodeName]["internalInputs"] del graph[nodeName]["internalInputs"]
del graph[nodeName]["outputs"] del graph[nodeName]["outputs"]
del graph[nodeName]["uids"] del graph[nodeName]["uid"]
del graph[nodeName]["internalFolder"] del graph[nodeName]["internalFolder"]
del graph[nodeName]["parallelization"] del graph[nodeName]["parallelization"]
@ -1431,13 +1427,13 @@ class Graph(BaseObject):
node.updateStatisticsFromCache() node.updateStatisticsFromCache()
def updateNodesPerUid(self): def updateNodesPerUid(self):
""" Update the duplicate nodes (sharing same uid) list of each node. """ """ Update the duplicate nodes (sharing same UID) list of each node. """
# First step is to construct a map uid/nodes # First step is to construct a map UID/nodes
nodesPerUid = {} nodesPerUid = {}
for node in self.nodes: for node in self.nodes:
uid = node._uids.get(0) uid = node._uid
# We try to add the node to the list corresponding to this uid # We try to add the node to the list corresponding to this UID
try: try:
nodesPerUid.get(uid).append(node) nodesPerUid.get(uid).append(node)
# If it fails because the uid is not in the map, we add it # If it fails because the uid is not in the map, we add it

View file

@ -477,7 +477,7 @@ class BaseNode(BaseObject):
# 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, nodeType, position=None, parent=None, uids=None, **kwargs): def __init__(self, nodeType, position=None, parent=None, uid=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.
@ -502,13 +502,13 @@ class BaseNode(BaseObject):
self.graph = None self.graph = None
self.dirty = True # whether this node's outputs must be re-evaluated on next Graph update self.dirty = True # whether this node's outputs must be re-evaluated on next Graph update
self._chunks = ListModel(parent=self) self._chunks = ListModel(parent=self)
self._uids = uids if uids else {} self._uid = uid
self._cmdVars = {} self._cmdVars = {}
self._size = 0 self._size = 0
self._position = position or Position() self._position = position or Position()
self._attributes = DictModel(keyAttrName='name', parent=self) self._attributes = DictModel(keyAttrName='name', parent=self)
self._internalAttributes = DictModel(keyAttrName='name', parent=self) self._internalAttributes = DictModel(keyAttrName='name', parent=self)
self.attributesPerUid = defaultdict(set) self.invalidatingAttributes = set()
self._alive = True # for QML side to know if the node can be used or is going to be deleted self._alive = True # for QML side to know if the node can be used or is going to be deleted
self._locked = False self._locked = False
self._duplicates = ListModel(parent=self) # list of nodes with the same uid self._duplicates = ListModel(parent=self) # list of nodes with the same uid
@ -699,56 +699,59 @@ class BaseNode(BaseObject):
def toDict(self): def toDict(self):
pass pass
def _computeUids(self): def _computeUid(self):
""" Compute node UIDs by combining associated attributes' UIDs. """ """ Compute node UID by combining associated attributes' UIDs. """
# Get all the attributes associated to a given UID index, specified in node descriptions with "uid=[index]" # If there is no invalidating attribute, then the computation of the UID should not go through as
# For now, the only index that is used is "0", so there will be a single iteration of the loop below # it will only include the node type
for uidIndex, associatedAttributes in self.attributesPerUid.items(): if not self.invalidatingAttributes:
return
# UID is computed by hashing the sorted list of tuple (name, value) of all attributes impacting this UID # UID is computed by hashing the sorted list of tuple (name, value) of all attributes impacting this UID
uidAttributes = [] uidAttributes = []
for a in associatedAttributes: for attr in self.invalidatingAttributes:
if not a.enabled: if not attr.enabled:
continue # disabled params do not contribute to the uid continue # Disabled params do not contribute to the uid
dynamicOutputAttr = a.isLink and a.getLinkParam(recursive=True).desc.isDynamicValue dynamicOutputAttr = attr.isLink and attr.getLinkParam(recursive=True).desc.isDynamicValue
# For dynamic output attributes, the UID does not depend on the attribute value. # For dynamic output attributes, the UID does not depend on the attribute value.
# In particular, when loading a project file, the UIDs are updated first, # In particular, when loading a project file, the UIDs are updated first,
# and the node status and the dynamic output values are not yet loaded, # and the node status and the dynamic output values are not yet loaded,
# so we should not read the attribute value. # so we should not read the attribute value.
if not dynamicOutputAttr and a.value == a.uidIgnoreValue: if not dynamicOutputAttr and attr.value == attr.uidIgnoreValue:
continue # for non-dynamic attributes, check if the value should be ignored continue # For non-dynamic attributes, check if the value should be ignored
uidAttributes.append((a.getName(), a.uid(uidIndex))) uidAttributes.append((attr.getName(), attr.uid()))
uidAttributes.sort() uidAttributes.sort()
# Adding the node type prevents ending up with two identical UIDs for different node types that have the exact same list of attributes
# Adding the node type prevents ending up with two identical UIDs for different node types
# that have the exact same list of attributes
uidAttributes.append(self.nodeType) uidAttributes.append(self.nodeType)
self._uids[uidIndex] = hashValue(uidAttributes) self._uid = hashValue(uidAttributes)
def _buildCmdVars(self): def _buildCmdVars(self):
def _buildAttributeCmdVars(cmdVars, name, attr): def _buildAttributeCmdVars(cmdVars, name, attr):
if attr.enabled: if attr.enabled:
group = attr.attributeDesc.group(attr.node) if isinstance(attr.attributeDesc.group, types.FunctionType) else attr.attributeDesc.group group = attr.attributeDesc.group(attr.node) if isinstance(attr.attributeDesc.group, types.FunctionType) else attr.attributeDesc.group
if group is not None: if group is not None:
# if there is a valid command line "group" # If there is a valid command line "group"
v = attr.getValueStr(withQuotes=True) v = attr.getValueStr(withQuotes=True)
cmdVars[name] = '--{name} {value}'.format(name=name, value=v) cmdVars[name] = "--{name} {value}".format(name=name, value=v)
# xxValue is exposed without quotes to allow to compose expressions # xxValue is exposed without quotes to allow to compose expressions
cmdVars[name + 'Value'] = attr.getValueStr(withQuotes=False) cmdVars[name + "Value"] = attr.getValueStr(withQuotes=False)
# List elements may give a fully empty string and will not be sent to the command line. # List elements may give a fully empty string and will not be sent to the command line.
# String attributes will return only quotes if it is empty and thus will be send to the command line. # String attributes will return only quotes if it is empty and thus will be send to the command line.
# But a List of string containing 1 element, # But a List of string containing 1 element,
# and this element is an empty string will also return quotes and will be send to the command line. # and this element is an empty string will also return quotes and will be sent to the command line.
if v: if v:
cmdVars[group] = cmdVars.get(group, '') + ' ' + cmdVars[name] cmdVars[group] = cmdVars.get(group, "") + " " + cmdVars[name]
elif isinstance(attr, GroupAttribute): elif isinstance(attr, GroupAttribute):
assert isinstance(attr.value, DictModel) assert isinstance(attr.value, DictModel)
# if the GroupAttribute is not set in a single command line argument, # If the GroupAttribute is not set in a single command line argument,
# the sub-attributes may need to be exposed individually # the sub-attributes may need to be exposed individually
for v in attr._value: for v in attr._value:
_buildAttributeCmdVars(cmdVars, v.name, v) _buildAttributeCmdVars(cmdVars, v.name, v)
""" Generate command variables using input attributes and resolved output attributes names and values. """ """ Generate command variables using input attributes and resolved output attributes names and values. """
for uidIndex, value in self._uids.items(): self._cmdVars["uid"] = self._uid
self._cmdVars['uid{}'.format(uidIndex)] = value
# Evaluate input params # Evaluate input params
for name, attr in self._attributes.objects.items(): for name, attr in self._attributes.objects.items():
@ -768,12 +771,14 @@ class BaseNode(BaseObject):
# Apply expressions for File attributes # Apply expressions for File attributes
if attr.attributeDesc.isExpression: if attr.attributeDesc.isExpression:
defaultValue = "" defaultValue = ""
# Do not evaluate expression for disabled attributes (the expression may refer to other attributes that are not defined) # Do not evaluate expression for disabled attributes
# (the expression may refer to other attributes that are not defined)
if attr.enabled: if attr.enabled:
try: try:
defaultValue = attr.defaultValue() defaultValue = attr.defaultValue()
except AttributeError as e: except AttributeError as e:
# If we load an old scene, the lambda associated to the 'value' could try to access other params that could not exist yet # If we load an old scene, the lambda associated to the 'value' could try to access other
# params that could not exist yet
logging.warning('Invalid lambda evaluation for "{nodeName}.{attrName}"'.format(nodeName=self.name, attrName=attr.name)) logging.warning('Invalid lambda evaluation for "{nodeName}.{attrName}"'.format(nodeName=self.name, attrName=attr.name))
if defaultValue is not None: if defaultValue is not None:
try: try:
@ -972,12 +977,13 @@ class BaseNode(BaseObject):
folder = self.internalFolder folder = self.internalFolder
except KeyError: except KeyError:
folder = '' folder = ''
# Update command variables / output attributes # Update command variables / output attributes
self._cmdVars = { self._cmdVars = {
'cache': cacheDir or self.graph.cacheDir, 'cache': cacheDir or self.graph.cacheDir,
'nodeType': self.nodeType, 'nodeType': self.nodeType,
} }
self._computeUids() self._computeUid()
self._buildCmdVars() self._buildCmdVars()
if self.nodeDesc: if self.nodeDesc:
self.nodeDesc.postUpdate(self) self.nodeDesc.postUpdate(self)
@ -1237,16 +1243,15 @@ class BaseNode(BaseObject):
self.setLocked(False) self.setLocked(False)
def updateDuplicates(self, nodesPerUid): def updateDuplicates(self, nodesPerUid):
""" Update the list of duplicate nodes (sharing the same uid). """ """ Update the list of duplicate nodes (sharing the same UID). """
uid = self._uids.get(0) if not nodesPerUid or not self._uid:
if not nodesPerUid or not uid:
if len(self._duplicates) > 0: if len(self._duplicates) > 0:
self._duplicates.clear() self._duplicates.clear()
self._hasDuplicates = False self._hasDuplicates = False
self.hasDuplicatesChanged.emit() self.hasDuplicatesChanged.emit()
return return
newList = [node for node in nodesPerUid.get(uid) if node != self] newList = [node for node in nodesPerUid.get(self._uid) if node != self]
# If number of elements in both lists are identical, # If number of elements in both lists are identical,
# we must check if their content is the same # we must check if their content is the same
@ -1377,8 +1382,8 @@ class Node(BaseNode):
""" """
A standard Graph node based on a node type. A standard Graph node based on a node type.
""" """
def __init__(self, nodeType, position=None, parent=None, uids=None, **kwargs): def __init__(self, nodeType, position=None, parent=None, uid=None, **kwargs):
super(Node, self).__init__(nodeType, position, parent=parent, uids=uids, **kwargs) super(Node, self).__init__(nodeType, position, parent=parent, uid=uid, **kwargs)
if not self.nodeDesc: if not self.nodeDesc:
raise UnknownNodeTypeError(nodeType) raise UnknownNodeTypeError(nodeType)
@ -1401,19 +1406,18 @@ class Node(BaseNode):
if attr.isOutput and attr.desc.semantic == "image": if attr.isOutput and attr.desc.semantic == "image":
attr.enabledChanged.connect(self.outputAttrEnabledChanged) attr.enabledChanged.connect(self.outputAttrEnabledChanged)
# List attributes per uid # List attributes per UID
for attr in self._attributes: for attr in self._attributes:
if attr.isInput: if attr.isInput and attr.attributeDesc.invalidate:
for uidIndex in attr.attributeDesc.uid: self.invalidatingAttributes.add(attr)
self.attributesPerUid[uidIndex].add(attr)
else: else:
if attr.attributeDesc.uid: if attr.attributeDesc.invalidate:
logging.error(f"Output Attribute should not contain a UID: '{nodeType}.{attr.name}'") logging.error(f"Output Attribute should not be invalidating: '{nodeType}.{attr.name}'")
# Add internal attributes with a UID to the list # Add internal attributes with a UID to the list
for attr in self._internalAttributes: for attr in self._internalAttributes:
for uidIndex in attr.attributeDesc.uid: if attr.attributeDesc.invalidate:
self.attributesPerUid[uidIndex].add(attr) self.invalidatingAttributes.add(attr)
self.optionalCallOnDescriptor("onNodeCreated") self.optionalCallOnDescriptor("onNodeCreated")
@ -1497,7 +1501,7 @@ class Node(BaseNode):
'size': self.size, 'size': self.size,
'split': self.nbParallelizationBlocks 'split': self.nbParallelizationBlocks
}, },
'uids': self._uids, 'uid': self._uid,
'internalFolder': self._internalFolder, 'internalFolder': self._internalFolder,
'inputs': {k: v for k, v in inputs.items() if v is not None}, # filter empty values 'inputs': {k: v for k, v in inputs.items() if v is not None}, # filter empty values
'internalInputs': {k: v for k, v in internalInputs.items() if v is not None}, 'internalInputs': {k: v for k, v in internalInputs.items() if v is not None},
@ -1539,7 +1543,7 @@ class CompatibilityIssue(Enum):
UnknownNodeType = 1 # the node type has no corresponding description class UnknownNodeType = 1 # the node type has no corresponding description class
VersionConflict = 2 # mismatch between node's description version and serialized node data VersionConflict = 2 # mismatch between node's description version and serialized node data
DescriptionConflict = 3 # mismatch between node's description attributes 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 UidConflict = 4 # mismatch between computed UIDs and UIDs stored in serialized node data
class CompatibilityNode(BaseNode): class CompatibilityNode(BaseNode):
@ -1552,7 +1556,7 @@ class CompatibilityNode(BaseNode):
super(CompatibilityNode, self).__init__(nodeType, position, parent) super(CompatibilityNode, self).__init__(nodeType, position, parent)
self.issue = issue self.issue = issue
# make a deepcopy of nodeDict to handle CompatibilityNode duplication # Make a deepcopy of nodeDict to handle CompatibilityNode duplication
# and be able to change modified inputs (see CompatibilityNode.toDict) # and be able to change modified inputs (see CompatibilityNode.toDict)
self.nodeDict = copy.deepcopy(nodeDict) self.nodeDict = copy.deepcopy(nodeDict)
self.version = Version(self.nodeDict.get("version", None)) self.version = Version(self.nodeDict.get("version", None))
@ -1561,30 +1565,26 @@ class CompatibilityNode(BaseNode):
self._internalInputs = self.nodeDict.get("internalInputs", {}) self._internalInputs = self.nodeDict.get("internalInputs", {})
self.outputs = self.nodeDict.get("outputs", {}) self.outputs = self.nodeDict.get("outputs", {})
self._internalFolder = self.nodeDict.get("internalFolder", "") self._internalFolder = self.nodeDict.get("internalFolder", "")
self._uids = self.nodeDict.get("uids", {}) self._uid = self.nodeDict.get("uid", None)
# JSON enfore keys to be strings, see
# https://docs.python.org/3.8/library/json.html#json.dump
# We know our keys are integers, so we convert them back to int.
self._uids = {int(k): v for k, v in self._uids.items()}
# restore parallelization settings # Restore parallelization settings
self.parallelization = self.nodeDict.get("parallelization", {}) self.parallelization = self.nodeDict.get("parallelization", {})
self.splitCount = self.parallelization.get("split", 1) self.splitCount = self.parallelization.get("split", 1)
self.setSize(self.parallelization.get("size", 1)) self.setSize(self.parallelization.get("size", 1))
# create input attributes # Create input attributes
for attrName, value in self._inputs.items(): for attrName, value in self._inputs.items():
self._addAttribute(attrName, value, isOutput=False) self._addAttribute(attrName, value, isOutput=False)
# create outputs attributes # Create outputs attributes
for attrName, value in self.outputs.items(): for attrName, value in self.outputs.items():
self._addAttribute(attrName, value, isOutput=True) self._addAttribute(attrName, value, isOutput=True)
# create internal attributes # Create internal attributes
for attrName, value in self._internalInputs.items(): for attrName, value in self._internalInputs.items():
self._addAttribute(attrName, value, isOutput=False, internalAttr=True) self._addAttribute(attrName, value, isOutput=False, internalAttr=True)
# create NodeChunks matching serialized parallelization settings # Create NodeChunks matching serialized parallelization settings
self._chunks.setObjectList([ self._chunks.setObjectList([
NodeChunk(self, desc.Range(i, blockSize=self.parallelization.get("blockSize", 0))) NodeChunk(self, desc.Range(i, blockSize=self.parallelization.get("blockSize", 0)))
for i in range(self.splitCount) for i in range(self.splitCount)
@ -1609,7 +1609,7 @@ class CompatibilityNode(BaseNode):
params = { params = {
"name": attrName, "label": attrName, "name": attrName, "label": attrName,
"description": "Incompatible parameter", "description": "Incompatible parameter",
"value": value, "uid": (), "value": value, "invalidate": False,
"group": "incompatible" "group": "incompatible"
} }
if isinstance(value, bool): if isinstance(value, bool):
@ -1626,10 +1626,10 @@ class CompatibilityNode(BaseNode):
# List/GroupAttribute: recursively build descriptions # List/GroupAttribute: recursively build descriptions
elif isinstance(value, (list, dict)): elif isinstance(value, (list, dict)):
del params["value"] del params["value"]
del params["uid"] del params["invalidate"]
attrDesc = None attrDesc = None
if isinstance(value, list): if isinstance(value, list):
elt = value[0] if value else "" # fallback: empty string value if list is empty elt = value[0] if value else "" # Fallback: empty string value if list is empty
eltDesc = CompatibilityNode.attributeDescFromValue("element", elt, isOutput) eltDesc = CompatibilityNode.attributeDescFromValue("element", elt, isOutput)
attrDesc = desc.ListAttribute(elementDesc=eltDesc, **params) attrDesc = desc.ListAttribute(elementDesc=eltDesc, **params)
elif isinstance(value, dict): elif isinstance(value, dict):
@ -1638,10 +1638,10 @@ class CompatibilityNode(BaseNode):
eltDesc = CompatibilityNode.attributeDescFromValue(key, value, isOutput) eltDesc = CompatibilityNode.attributeDescFromValue(key, value, isOutput)
groupDesc.append(eltDesc) groupDesc.append(eltDesc)
attrDesc = desc.GroupAttribute(groupDesc=groupDesc, **params) attrDesc = desc.GroupAttribute(groupDesc=groupDesc, **params)
# override empty default value with # Override empty default value with
attrDesc._value = value attrDesc._value = value
return attrDesc return attrDesc
# handle any other type of parameters as Strings # Handle any other type of parameters as Strings
return desc.StringParam(**params) return desc.StringParam(**params)
@staticmethod @staticmethod
@ -1760,7 +1760,7 @@ class CompatibilityNode(BaseNode):
Return a new Node instance based on original node type with common inputs initialized. Return a new Node instance based on original node type with common inputs initialized.
""" """
if not self.canUpgrade: if not self.canUpgrade:
raise NodeUpgradeError(self.name, "no matching node type") raise NodeUpgradeError(self.name, "No matching node type")
# inputs matching current type description # inputs matching current type description
commonInputs = [] commonInputs = []
@ -1819,23 +1819,19 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
""" """
nodeType = nodeDict["nodeType"] nodeType = nodeDict["nodeType"]
# retro-compatibility: inputs were previously saved as "attributes" # Retro-compatibility: inputs were previously saved as "attributes"
if "inputs" not in nodeDict and "attributes" in nodeDict: if "inputs" not in nodeDict and "attributes" in nodeDict:
nodeDict["inputs"] = nodeDict["attributes"] nodeDict["inputs"] = nodeDict["attributes"]
del nodeDict["attributes"] del nodeDict["attributes"]
# get node inputs/outputs # Get node inputs/outputs
inputs = nodeDict.get("inputs", {}) inputs = nodeDict.get("inputs", {})
internalInputs = nodeDict.get("internalInputs", {}) internalInputs = nodeDict.get("internalInputs", {})
outputs = nodeDict.get("outputs", {}) outputs = nodeDict.get("outputs", {})
version = nodeDict.get("version", None) version = nodeDict.get("version", None)
internalFolder = nodeDict.get("internalFolder", None) internalFolder = nodeDict.get("internalFolder", None)
position = Position(*nodeDict.get("position", [])) position = Position(*nodeDict.get("position", []))
uids = nodeDict.get("uids", {}) uid = nodeDict.get("uid", None)
# JSON enfore keys to be strings, see
# https://docs.python.org/3.8/library/json.html#json.dump
# We know our keys are integers, so we convert them back to int.
uids = {int(k): v for k, v in uids.items()}
compatibilityIssue = None compatibilityIssue = None
@ -1843,21 +1839,22 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
try: try:
nodeDesc = meshroom.core.nodesDesc[nodeType] nodeDesc = meshroom.core.nodesDesc[nodeType]
except KeyError: except KeyError:
# unknown node type # Unknown node type
compatibilityIssue = CompatibilityIssue.UnknownNodeType compatibilityIssue = CompatibilityIssue.UnknownNodeType
if uidConflict: # Unknown node type should take precedence over UID conflict, as it cannot be resolved
if uidConflict and nodeDesc:
compatibilityIssue = CompatibilityIssue.UidConflict compatibilityIssue = CompatibilityIssue.UidConflict
if nodeDesc and not uidConflict: # if uidConflict, there is no need to look for another compatibility issue if nodeDesc and not uidConflict: # if uidConflict, there is no need to look for another compatibility issue
# compare serialized node version with current node version # Compare serialized node version with current node version
currentNodeVersion = meshroom.core.nodeVersion(nodeDesc) currentNodeVersion = meshroom.core.nodeVersion(nodeDesc)
# if both versions are available, check for incompatibility in major version # If both versions are available, check for incompatibility in major version
if version and currentNodeVersion and Version(version).major != Version(currentNodeVersion).major: if version and currentNodeVersion and Version(version).major != Version(currentNodeVersion).major:
compatibilityIssue = CompatibilityIssue.VersionConflict compatibilityIssue = CompatibilityIssue.VersionConflict
# in other cases, check attributes compatibility between serialized node and its description # In other cases, check attributes compatibility between serialized node and its description
else: else:
# check that the node has the exact same set of inputs/outputs as its description, except # Check that the node has the exact same set of inputs/outputs as its description, except
# if the node is described in a template file, in which only non-default parameters are saved; # if the node is described in a template file, in which only non-default parameters are saved;
# do not perform that check for internal attributes because there is no point in # do not perform that check for internal attributes because there is no point in
# raising compatibility issues if their number differs: in that case, it is only useful # raising compatibility issues if their number differs: in that case, it is only useful
@ -1866,47 +1863,47 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
sorted([attr.name for attr in nodeDesc.outputs if not attr.isDynamicValue]) != sorted(outputs.keys())): sorted([attr.name for attr in nodeDesc.outputs if not attr.isDynamicValue]) != sorted(outputs.keys())):
compatibilityIssue = CompatibilityIssue.DescriptionConflict compatibilityIssue = CompatibilityIssue.DescriptionConflict
# check whether there are any internal attributes that are invalidating in the node description: if there # Check whether there are any internal attributes that are invalidating in the node description: if there
# are, then check that these internal attributes are part of nodeDict; if they are not, a compatibility # are, then check that these internal attributes are part of nodeDict; if they are not, a compatibility
# issue must be raised to warn the user, as this will automatically change the node's UID # issue must be raised to warn the user, as this will automatically change the node's UID
if not template: if not template:
invalidatingIntInputs = [] invalidatingIntInputs = []
for attr in nodeDesc.internalInputs: for attr in nodeDesc.internalInputs:
if attr.uid == [0]: if attr.invalidate:
invalidatingIntInputs.append(attr.name) invalidatingIntInputs.append(attr.name)
for attr in invalidatingIntInputs: for attr in invalidatingIntInputs:
if attr not in internalInputs.keys(): if attr not in internalInputs.keys():
compatibilityIssue = CompatibilityIssue.DescriptionConflict compatibilityIssue = CompatibilityIssue.DescriptionConflict
break break
# verify that all inputs match their descriptions # Verify that all inputs match their descriptions
for attrName, value in inputs.items(): for attrName, value in inputs.items():
if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value): if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value):
compatibilityIssue = CompatibilityIssue.DescriptionConflict compatibilityIssue = CompatibilityIssue.DescriptionConflict
break break
# verify that all internal inputs match their description # Verify that all internal inputs match their description
for attrName, value in internalInputs.items(): for attrName, value in internalInputs.items():
if not CompatibilityNode.attributeDescFromName(nodeDesc.internalInputs, attrName, value): if not CompatibilityNode.attributeDescFromName(nodeDesc.internalInputs, attrName, value):
compatibilityIssue = CompatibilityIssue.DescriptionConflict compatibilityIssue = CompatibilityIssue.DescriptionConflict
break break
# verify that all outputs match their descriptions # Verify that all outputs match their descriptions
for attrName, value in outputs.items(): for attrName, value in outputs.items():
if not CompatibilityNode.attributeDescFromName(nodeDesc.outputs, attrName, value): if not CompatibilityNode.attributeDescFromName(nodeDesc.outputs, attrName, value):
compatibilityIssue = CompatibilityIssue.DescriptionConflict compatibilityIssue = CompatibilityIssue.DescriptionConflict
break break
if compatibilityIssue is None: if compatibilityIssue is None:
node = Node(nodeType, position, uids=uids, **inputs, **internalInputs, **outputs) node = Node(nodeType, position, uid=uid, **inputs, **internalInputs, **outputs)
else: else:
logging.debug("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name)) logging.debug("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name))
node = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue) node = CompatibilityNode(nodeType, nodeDict, position, compatibilityIssue)
# retro-compatibility: no internal folder saved # Retro-compatibility: no internal folder saved
# can't spawn meaningful CompatibilityNode with precomputed outputs # can't spawn meaningful CompatibilityNode with precomputed outputs
# => automatically try to perform node upgrade # => automatically try to perform node upgrade
if not internalFolder and nodeDesc: if not internalFolder and nodeDesc:
logging.warning("No serialized output data: performing automatic upgrade on '{}'".format(name)) logging.warning("No serialized output data: performing automatic upgrade on '{}'".format(name))
node = node.upgrade() node = node.upgrade()
elif template: # if the node comes from a template file and there is a conflict, it should be upgraded anyway elif template: # If the node comes from a template file and there is a conflict, it should be upgraded anyway
node = node.upgrade() node = node.upgrade()
return node return node