mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-07-23 19:47:39 +02:00
[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:
parent
248c301c5a
commit
9973298746
4 changed files with 142 additions and 151 deletions
|
@ -272,22 +272,21 @@ class Attribute(BaseObject):
|
|||
def isInput(self):
|
||||
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.desc.isDynamicValue:
|
||||
# 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.
|
||||
return hashValue((self.name, self.node._uids.get(uidIndex)))
|
||||
return hashValue((self.name, self.node._uid))
|
||||
else:
|
||||
# only dependent on the hash of its value without the cache folder
|
||||
return hashValue(self._invalidationValue)
|
||||
if self.isLink:
|
||||
linkParam = self.getLinkParam(recursive=True)
|
||||
return linkParam.uid(uidIndex)
|
||||
return linkParam.uid()
|
||||
if isinstance(self._value, (list, tuple, set,)):
|
||||
# non-exclusive choice param
|
||||
# hash of sorted values hashed
|
||||
|
@ -610,14 +609,14 @@ class ListAttribute(Attribute):
|
|||
self.requestGraphUpdate()
|
||||
self.valueChanged.emit()
|
||||
|
||||
def uid(self, uidIndex):
|
||||
def uid(self):
|
||||
if isinstance(self.value, ListModel):
|
||||
uids = []
|
||||
for value in self.value:
|
||||
if uidIndex in value.desc.uid:
|
||||
uids.append(value.uid(uidIndex))
|
||||
if value.desc.invalidate:
|
||||
uids.append(value.uid())
|
||||
return hashValue(uids)
|
||||
return super(ListAttribute, self).uid(uidIndex)
|
||||
return super(ListAttribute, self).uid()
|
||||
|
||||
def _applyExpr(self):
|
||||
if not self.node.graph:
|
||||
|
@ -747,11 +746,11 @@ class GroupAttribute(Attribute):
|
|||
except KeyError:
|
||||
return None
|
||||
|
||||
def uid(self, uidIndex):
|
||||
def uid(self):
|
||||
uids = []
|
||||
for k, v in self._value.items():
|
||||
if v.enabled and uidIndex in v.desc.uid:
|
||||
uids.append(v.uid(uidIndex))
|
||||
if v.enabled and v.desc.invalidate:
|
||||
uids.append(v.uid())
|
||||
return hashValue(uids)
|
||||
|
||||
def _applyExpr(self):
|
||||
|
|
|
@ -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):
|
||||
super(Attribute, self).__init__()
|
||||
self._name = name
|
||||
self._label = label
|
||||
self._description = description
|
||||
self._value = value
|
||||
self._uid = uid
|
||||
self._invalidate = invalidate
|
||||
self._group = group
|
||||
self._advanced = advanced
|
||||
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,
|
||||
# but an output value that is computed during the Node's process execution.
|
||||
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)
|
||||
advanced = Property(bool, lambda self: self._advanced, constant=True)
|
||||
enabled = Property(Variant, lambda self: self._enabled, constant=True)
|
||||
|
@ -114,7 +114,7 @@ class ListAttribute(Attribute):
|
|||
"""
|
||||
self._elementDesc = elementDesc
|
||||
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):
|
||||
# Import within the method to prevent cyclic dependencies
|
||||
|
@ -149,7 +149,7 @@ class ListAttribute(Attribute):
|
|||
return 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)
|
||||
|
||||
|
||||
|
@ -162,7 +162,7 @@ class GroupAttribute(Attribute):
|
|||
self._groupDesc = groupDesc
|
||||
self._joinChar = joinChar
|
||||
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):
|
||||
# Import within the method to prevent cyclic dependencies
|
||||
|
@ -239,14 +239,14 @@ class GroupAttribute(Attribute):
|
|||
|
||||
return matchCount > 0
|
||||
|
||||
def retrieveChildrenUids(self):
|
||||
allUids = []
|
||||
def retrieveChildrenInvalidations(self):
|
||||
allInvalidations = []
|
||||
for desc in self._groupDesc:
|
||||
allUids.extend(desc.uid)
|
||||
return allUids
|
||||
allInvalidations.append(desc.invalidate)
|
||||
return allInvalidations
|
||||
|
||||
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)
|
||||
brackets = Property(str, lambda self: self._brackets, constant=True)
|
||||
|
||||
|
@ -254,16 +254,16 @@ class GroupAttribute(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):
|
||||
super(Param, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled,
|
||||
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, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled,
|
||||
uidIgnoreValue=uidIgnoreValue, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed)
|
||||
|
||||
|
||||
class File(Attribute):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, name, label, description, value, uid, 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)
|
||||
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, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
|
||||
self._valueType = str
|
||||
|
||||
def validateValue(self, value):
|
||||
|
@ -284,8 +284,8 @@ class File(Attribute):
|
|||
class BoolParam(Param):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, name, label, description, value, uid, 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)
|
||||
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, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
|
||||
self._valueType = bool
|
||||
|
||||
def validateValue(self, value):
|
||||
|
@ -308,9 +308,9 @@ class BoolParam(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
|
||||
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)
|
||||
self._valueType = int
|
||||
|
||||
|
@ -334,9 +334,9 @@ class IntParam(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
|
||||
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)
|
||||
self._valueType = float
|
||||
|
||||
|
@ -358,8 +358,8 @@ class FloatParam(Param):
|
|||
class PushButtonParam(Param):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, name, label, description, uid, 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)
|
||||
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, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
|
||||
self._valueType = None
|
||||
|
||||
def getInstanceType(self):
|
||||
|
@ -377,10 +377,10 @@ class PushButtonParam(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):
|
||||
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)
|
||||
self._values = values
|
||||
self._exclusive = exclusive
|
||||
|
@ -446,8 +446,8 @@ class ChoiceParam(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):
|
||||
super(StringParam, self).__init__(name=name, label=label, description=description, value=value, uid=uid, group=group, advanced=advanced, semantic=semantic, enabled=enabled,
|
||||
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, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled,
|
||||
uidIgnoreValue=uidIgnoreValue, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed)
|
||||
self._valueType = str
|
||||
|
||||
|
@ -467,8 +467,8 @@ class StringParam(Param):
|
|||
class ColorParam(Param):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, name, label, description, value, uid, 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)
|
||||
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, invalidate=invalidate, group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed)
|
||||
self._valueType = str
|
||||
|
||||
def validateValue(self, value):
|
||||
|
@ -616,7 +616,7 @@ class StaticNodeSize(object):
|
|||
class Node(object):
|
||||
"""
|
||||
"""
|
||||
internalFolder = '{cache}/{nodeType}/{uid0}/'
|
||||
internalFolder = '{cache}/{nodeType}/{uid}/'
|
||||
cpu = Level.NORMAL
|
||||
gpu = Level.NONE
|
||||
ram = Level.NORMAL
|
||||
|
@ -631,7 +631,7 @@ class Node(object):
|
|||
"It is displayed in bold font in the invalidation/comment messages tooltip.",
|
||||
value="",
|
||||
semantic="multiline",
|
||||
uid=[0],
|
||||
invalidate=True,
|
||||
advanced=True,
|
||||
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.",
|
||||
value="",
|
||||
semantic="multiline",
|
||||
uid=[],
|
||||
invalidate=False,
|
||||
),
|
||||
StringParam(
|
||||
name="label",
|
||||
label="Node's Label",
|
||||
description="Customize the default label (to replace the technical name of the node instance).",
|
||||
value="",
|
||||
uid=[],
|
||||
invalidate=False,
|
||||
),
|
||||
ColorParam(
|
||||
name="color",
|
||||
label="Color",
|
||||
description="Custom color for the node (SVG name or hexadecimal code).",
|
||||
value="",
|
||||
uid=[],
|
||||
invalidate=False,
|
||||
)
|
||||
]
|
||||
inputs = []
|
||||
|
@ -780,7 +780,7 @@ class CommandLineNode(Node):
|
|||
finally:
|
||||
chunk.subprocess = None
|
||||
|
||||
#specific command line node for alicevision apps
|
||||
# Specific command line node for AliceVision apps
|
||||
class AVCommandLineNode(CommandLineNode):
|
||||
|
||||
cgroupParsed = False
|
||||
|
@ -805,7 +805,6 @@ class AVCommandLineNode(CommandLineNode):
|
|||
AVCommandLineNode.cgroupParsed = True
|
||||
|
||||
def buildCommandLine(self, chunk):
|
||||
|
||||
commandLineString = super(AVCommandLineNode, self).buildCommandLine(chunk)
|
||||
|
||||
return commandLineString + AVCommandLineNode.cmdMem + AVCommandLineNode.cmdCore
|
||||
|
|
|
@ -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.
|
||||
self.updateInternals()
|
||||
|
||||
# By this point, the graph has been fully loaded and an updateInternals has been triggered, so all the nodes'
|
||||
# links have been resolved and their UID computations are all complete.
|
||||
# By this point, the graph has been fully loaded and an updateInternals has been triggered, so all the
|
||||
# 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
|
||||
# that were computed.
|
||||
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])):
|
||||
node = self.node(nodeName)
|
||||
|
||||
savedUid = nodeData.get("uids", {}) # Node's UID from the graph file
|
||||
# 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.
|
||||
savedUid = {int(k): v for k, v in savedUid.items()}
|
||||
savedUid = nodeData.get("uid", None)
|
||||
graphUid = node._uid # Node's UID from the graph itself
|
||||
|
||||
graphUid = node._uids # Node's UID from the graph itself
|
||||
if savedUid != graphUid and graphUid is not None:
|
||||
# Different UIDs, remove the existing node from the graph and replace it with a CompatibilityNode
|
||||
logging.debug("UID conflict detected for {}".format(nodeName))
|
||||
|
@ -1382,7 +1378,7 @@ class Graph(BaseObject):
|
|||
del graph[nodeName]["internalInputs"]
|
||||
|
||||
del graph[nodeName]["outputs"]
|
||||
del graph[nodeName]["uids"]
|
||||
del graph[nodeName]["uid"]
|
||||
del graph[nodeName]["internalFolder"]
|
||||
del graph[nodeName]["parallelization"]
|
||||
|
||||
|
@ -1431,13 +1427,13 @@ class Graph(BaseObject):
|
|||
node.updateStatisticsFromCache()
|
||||
|
||||
def updateNodesPerUid(self):
|
||||
""" Update the duplicate nodes (sharing same uid) list of each node. """
|
||||
# First step is to construct a map uid/nodes
|
||||
""" Update the duplicate nodes (sharing same UID) list of each node. """
|
||||
# First step is to construct a map UID/nodes
|
||||
nodesPerUid = {}
|
||||
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:
|
||||
nodesPerUid.get(uid).append(node)
|
||||
# If it fails because the uid is not in the map, we add it
|
||||
|
|
|
@ -477,7 +477,7 @@ class BaseNode(BaseObject):
|
|||
# i.e: a.b, a[0], a[0].b.c[1]
|
||||
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.
|
||||
Any other keyword argument will be used to initialize this node's attributes.
|
||||
|
@ -502,13 +502,13 @@ class BaseNode(BaseObject):
|
|||
self.graph = None
|
||||
self.dirty = True # whether this node's outputs must be re-evaluated on next Graph update
|
||||
self._chunks = ListModel(parent=self)
|
||||
self._uids = uids if uids else {}
|
||||
self._uid = uid
|
||||
self._cmdVars = {}
|
||||
self._size = 0
|
||||
self._position = position or Position()
|
||||
self._attributes = 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._locked = False
|
||||
self._duplicates = ListModel(parent=self) # list of nodes with the same uid
|
||||
|
@ -699,56 +699,59 @@ class BaseNode(BaseObject):
|
|||
def toDict(self):
|
||||
pass
|
||||
|
||||
def _computeUids(self):
|
||||
""" Compute node UIDs by combining associated attributes' UIDs. """
|
||||
# Get all the attributes associated to a given UID index, specified in node descriptions with "uid=[index]"
|
||||
# For now, the only index that is used is "0", so there will be a single iteration of the loop below
|
||||
for uidIndex, associatedAttributes in self.attributesPerUid.items():
|
||||
# UID is computed by hashing the sorted list of tuple (name, value) of all attributes impacting this UID
|
||||
uidAttributes = []
|
||||
for a in associatedAttributes:
|
||||
if not a.enabled:
|
||||
continue # disabled params do not contribute to the uid
|
||||
dynamicOutputAttr = a.isLink and a.getLinkParam(recursive=True).desc.isDynamicValue
|
||||
# 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,
|
||||
# and the node status and the dynamic output values are not yet loaded,
|
||||
# so we should not read the attribute value.
|
||||
if not dynamicOutputAttr and a.value == a.uidIgnoreValue:
|
||||
continue # for non-dynamic attributes, check if the value should be ignored
|
||||
uidAttributes.append((a.getName(), a.uid(uidIndex)))
|
||||
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
|
||||
uidAttributes.append(self.nodeType)
|
||||
self._uids[uidIndex] = hashValue(uidAttributes)
|
||||
def _computeUid(self):
|
||||
""" Compute node UID by combining associated attributes' UIDs. """
|
||||
# If there is no invalidating attribute, then the computation of the UID should not go through as
|
||||
# it will only include the node type
|
||||
if not self.invalidatingAttributes:
|
||||
return
|
||||
|
||||
# UID is computed by hashing the sorted list of tuple (name, value) of all attributes impacting this UID
|
||||
uidAttributes = []
|
||||
for attr in self.invalidatingAttributes:
|
||||
if not attr.enabled:
|
||||
continue # Disabled params do not contribute to the uid
|
||||
dynamicOutputAttr = attr.isLink and attr.getLinkParam(recursive=True).desc.isDynamicValue
|
||||
# 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,
|
||||
# and the node status and the dynamic output values are not yet loaded,
|
||||
# so we should not read the attribute value.
|
||||
if not dynamicOutputAttr and attr.value == attr.uidIgnoreValue:
|
||||
continue # For non-dynamic attributes, check if the value should be ignored
|
||||
uidAttributes.append((attr.getName(), attr.uid()))
|
||||
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
|
||||
uidAttributes.append(self.nodeType)
|
||||
self._uid = hashValue(uidAttributes)
|
||||
|
||||
def _buildCmdVars(self):
|
||||
def _buildAttributeCmdVars(cmdVars, name, attr):
|
||||
if attr.enabled:
|
||||
group = attr.attributeDesc.group(attr.node) if isinstance(attr.attributeDesc.group, types.FunctionType) else attr.attributeDesc.group
|
||||
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)
|
||||
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
|
||||
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.
|
||||
# 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,
|
||||
# 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:
|
||||
cmdVars[group] = cmdVars.get(group, '') + ' ' + cmdVars[name]
|
||||
cmdVars[group] = cmdVars.get(group, "") + " " + cmdVars[name]
|
||||
elif isinstance(attr, GroupAttribute):
|
||||
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
|
||||
for v in attr._value:
|
||||
_buildAttributeCmdVars(cmdVars, v.name, v)
|
||||
|
||||
""" Generate command variables using input attributes and resolved output attributes names and values. """
|
||||
for uidIndex, value in self._uids.items():
|
||||
self._cmdVars['uid{}'.format(uidIndex)] = value
|
||||
self._cmdVars["uid"] = self._uid
|
||||
|
||||
# Evaluate input params
|
||||
for name, attr in self._attributes.objects.items():
|
||||
|
@ -768,12 +771,14 @@ class BaseNode(BaseObject):
|
|||
# Apply expressions for File attributes
|
||||
if attr.attributeDesc.isExpression:
|
||||
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:
|
||||
try:
|
||||
defaultValue = attr.defaultValue()
|
||||
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))
|
||||
if defaultValue is not None:
|
||||
try:
|
||||
|
@ -972,12 +977,13 @@ class BaseNode(BaseObject):
|
|||
folder = self.internalFolder
|
||||
except KeyError:
|
||||
folder = ''
|
||||
|
||||
# Update command variables / output attributes
|
||||
self._cmdVars = {
|
||||
'cache': cacheDir or self.graph.cacheDir,
|
||||
'nodeType': self.nodeType,
|
||||
}
|
||||
self._computeUids()
|
||||
self._computeUid()
|
||||
self._buildCmdVars()
|
||||
if self.nodeDesc:
|
||||
self.nodeDesc.postUpdate(self)
|
||||
|
@ -1237,16 +1243,15 @@ class BaseNode(BaseObject):
|
|||
self.setLocked(False)
|
||||
|
||||
def updateDuplicates(self, nodesPerUid):
|
||||
""" Update the list of duplicate nodes (sharing the same uid). """
|
||||
uid = self._uids.get(0)
|
||||
if not nodesPerUid or not uid:
|
||||
""" Update the list of duplicate nodes (sharing the same UID). """
|
||||
if not nodesPerUid or not self._uid:
|
||||
if len(self._duplicates) > 0:
|
||||
self._duplicates.clear()
|
||||
self._hasDuplicates = False
|
||||
self.hasDuplicatesChanged.emit()
|
||||
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,
|
||||
# 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.
|
||||
"""
|
||||
def __init__(self, nodeType, position=None, parent=None, uids=None, **kwargs):
|
||||
super(Node, self).__init__(nodeType, position, parent=parent, uids=uids, **kwargs)
|
||||
def __init__(self, nodeType, position=None, parent=None, uid=None, **kwargs):
|
||||
super(Node, self).__init__(nodeType, position, parent=parent, uid=uid, **kwargs)
|
||||
|
||||
if not self.nodeDesc:
|
||||
raise UnknownNodeTypeError(nodeType)
|
||||
|
@ -1401,19 +1406,18 @@ class Node(BaseNode):
|
|||
if attr.isOutput and attr.desc.semantic == "image":
|
||||
attr.enabledChanged.connect(self.outputAttrEnabledChanged)
|
||||
|
||||
# List attributes per uid
|
||||
# List attributes per UID
|
||||
for attr in self._attributes:
|
||||
if attr.isInput:
|
||||
for uidIndex in attr.attributeDesc.uid:
|
||||
self.attributesPerUid[uidIndex].add(attr)
|
||||
if attr.isInput and attr.attributeDesc.invalidate:
|
||||
self.invalidatingAttributes.add(attr)
|
||||
else:
|
||||
if attr.attributeDesc.uid:
|
||||
logging.error(f"Output Attribute should not contain a UID: '{nodeType}.{attr.name}'")
|
||||
if attr.attributeDesc.invalidate:
|
||||
logging.error(f"Output Attribute should not be invalidating: '{nodeType}.{attr.name}'")
|
||||
|
||||
# Add internal attributes with a UID to the list
|
||||
for attr in self._internalAttributes:
|
||||
for uidIndex in attr.attributeDesc.uid:
|
||||
self.attributesPerUid[uidIndex].add(attr)
|
||||
if attr.attributeDesc.invalidate:
|
||||
self.invalidatingAttributes.add(attr)
|
||||
|
||||
self.optionalCallOnDescriptor("onNodeCreated")
|
||||
|
||||
|
@ -1497,7 +1501,7 @@ class Node(BaseNode):
|
|||
'size': self.size,
|
||||
'split': self.nbParallelizationBlocks
|
||||
},
|
||||
'uids': self._uids,
|
||||
'uid': self._uid,
|
||||
'internalFolder': self._internalFolder,
|
||||
'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},
|
||||
|
@ -1539,7 +1543,7 @@ class CompatibilityIssue(Enum):
|
|||
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
|
||||
UidConflict = 4 # mismatch between computed UIDs and UIDs stored in serialized node data
|
||||
|
||||
|
||||
class CompatibilityNode(BaseNode):
|
||||
|
@ -1552,7 +1556,7 @@ class CompatibilityNode(BaseNode):
|
|||
super(CompatibilityNode, self).__init__(nodeType, position, parent)
|
||||
|
||||
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)
|
||||
self.nodeDict = copy.deepcopy(nodeDict)
|
||||
self.version = Version(self.nodeDict.get("version", None))
|
||||
|
@ -1561,30 +1565,26 @@ class CompatibilityNode(BaseNode):
|
|||
self._internalInputs = self.nodeDict.get("internalInputs", {})
|
||||
self.outputs = self.nodeDict.get("outputs", {})
|
||||
self._internalFolder = self.nodeDict.get("internalFolder", "")
|
||||
self._uids = self.nodeDict.get("uids", {})
|
||||
# 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()}
|
||||
self._uid = self.nodeDict.get("uid", None)
|
||||
|
||||
# restore parallelization settings
|
||||
# Restore parallelization settings
|
||||
self.parallelization = self.nodeDict.get("parallelization", {})
|
||||
self.splitCount = self.parallelization.get("split", 1)
|
||||
self.setSize(self.parallelization.get("size", 1))
|
||||
|
||||
# create input attributes
|
||||
# Create input attributes
|
||||
for attrName, value in self._inputs.items():
|
||||
self._addAttribute(attrName, value, isOutput=False)
|
||||
|
||||
# create outputs attributes
|
||||
# Create outputs attributes
|
||||
for attrName, value in self.outputs.items():
|
||||
self._addAttribute(attrName, value, isOutput=True)
|
||||
|
||||
# create internal attributes
|
||||
# Create internal attributes
|
||||
for attrName, value in self._internalInputs.items():
|
||||
self._addAttribute(attrName, value, isOutput=False, internalAttr=True)
|
||||
|
||||
# create NodeChunks matching serialized parallelization settings
|
||||
# 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)
|
||||
|
@ -1609,7 +1609,7 @@ class CompatibilityNode(BaseNode):
|
|||
params = {
|
||||
"name": attrName, "label": attrName,
|
||||
"description": "Incompatible parameter",
|
||||
"value": value, "uid": (),
|
||||
"value": value, "invalidate": False,
|
||||
"group": "incompatible"
|
||||
}
|
||||
if isinstance(value, bool):
|
||||
|
@ -1626,10 +1626,10 @@ class CompatibilityNode(BaseNode):
|
|||
# List/GroupAttribute: recursively build descriptions
|
||||
elif isinstance(value, (list, dict)):
|
||||
del params["value"]
|
||||
del params["uid"]
|
||||
del params["invalidate"]
|
||||
attrDesc = None
|
||||
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)
|
||||
attrDesc = desc.ListAttribute(elementDesc=eltDesc, **params)
|
||||
elif isinstance(value, dict):
|
||||
|
@ -1638,10 +1638,10 @@ class CompatibilityNode(BaseNode):
|
|||
eltDesc = CompatibilityNode.attributeDescFromValue(key, value, isOutput)
|
||||
groupDesc.append(eltDesc)
|
||||
attrDesc = desc.GroupAttribute(groupDesc=groupDesc, **params)
|
||||
# override empty default value with
|
||||
# Override empty default value with
|
||||
attrDesc._value = value
|
||||
return attrDesc
|
||||
# handle any other type of parameters as Strings
|
||||
# Handle any other type of parameters as Strings
|
||||
return desc.StringParam(**params)
|
||||
|
||||
@staticmethod
|
||||
|
@ -1760,7 +1760,7 @@ class CompatibilityNode(BaseNode):
|
|||
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")
|
||||
raise NodeUpgradeError(self.name, "No matching node type")
|
||||
|
||||
# inputs matching current type description
|
||||
commonInputs = []
|
||||
|
@ -1819,23 +1819,19 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
|
|||
"""
|
||||
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:
|
||||
nodeDict["inputs"] = nodeDict["attributes"]
|
||||
del nodeDict["attributes"]
|
||||
|
||||
# get node inputs/outputs
|
||||
# Get node inputs/outputs
|
||||
inputs = nodeDict.get("inputs", {})
|
||||
internalInputs = nodeDict.get("internalInputs", {})
|
||||
outputs = nodeDict.get("outputs", {})
|
||||
version = nodeDict.get("version", None)
|
||||
internalFolder = nodeDict.get("internalFolder", None)
|
||||
position = Position(*nodeDict.get("position", []))
|
||||
uids = nodeDict.get("uids", {})
|
||||
# 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()}
|
||||
uid = nodeDict.get("uid", None)
|
||||
|
||||
compatibilityIssue = None
|
||||
|
||||
|
@ -1843,21 +1839,22 @@ def nodeFactory(nodeDict, name=None, template=False, uidConflict=False):
|
|||
try:
|
||||
nodeDesc = meshroom.core.nodesDesc[nodeType]
|
||||
except KeyError:
|
||||
# unknown node type
|
||||
# Unknown node type
|
||||
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
|
||||
|
||||
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)
|
||||
# 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:
|
||||
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:
|
||||
# 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;
|
||||
# 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
|
||||
|
@ -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())):
|
||||
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
|
||||
# issue must be raised to warn the user, as this will automatically change the node's UID
|
||||
if not template:
|
||||
invalidatingIntInputs = []
|
||||
for attr in nodeDesc.internalInputs:
|
||||
if attr.uid == [0]:
|
||||
if attr.invalidate:
|
||||
invalidatingIntInputs.append(attr.name)
|
||||
for attr in invalidatingIntInputs:
|
||||
if attr not in internalInputs.keys():
|
||||
compatibilityIssue = CompatibilityIssue.DescriptionConflict
|
||||
break
|
||||
|
||||
# verify that all inputs match their descriptions
|
||||
# Verify that all inputs match their descriptions
|
||||
for attrName, value in inputs.items():
|
||||
if not CompatibilityNode.attributeDescFromName(nodeDesc.inputs, attrName, value):
|
||||
compatibilityIssue = CompatibilityIssue.DescriptionConflict
|
||||
break
|
||||
# verify that all internal inputs match their description
|
||||
# Verify that all internal inputs match their description
|
||||
for attrName, value in internalInputs.items():
|
||||
if not CompatibilityNode.attributeDescFromName(nodeDesc.internalInputs, attrName, value):
|
||||
compatibilityIssue = CompatibilityIssue.DescriptionConflict
|
||||
break
|
||||
# verify that all outputs match their descriptions
|
||||
# Verify that all outputs match their descriptions
|
||||
for attrName, value in outputs.items():
|
||||
if not CompatibilityNode.attributeDescFromName(nodeDesc.outputs, attrName, value):
|
||||
compatibilityIssue = CompatibilityIssue.DescriptionConflict
|
||||
break
|
||||
|
||||
if compatibilityIssue is None:
|
||||
node = Node(nodeType, position, uids=uids, **inputs, **internalInputs, **outputs)
|
||||
node = Node(nodeType, position, uid=uid, **inputs, **internalInputs, **outputs)
|
||||
else:
|
||||
logging.debug("Compatibility issue detected for node '{}': {}".format(name, compatibilityIssue.name))
|
||||
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
|
||||
# => automatically try to perform node upgrade
|
||||
if not internalFolder and nodeDesc:
|
||||
logging.warning("No serialized output data: performing automatic upgrade on '{}'".format(name))
|
||||
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()
|
||||
|
||||
return node
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue