[core] Node: Clean-up code

This commit is contained in:
Candice Bentéjac 2025-04-28 18:24:08 +02:00
parent 081d38f78d
commit 8300626ef5

View file

@ -56,6 +56,8 @@ class Status(Enum):
class ExecMode(Enum): class ExecMode(Enum):
"""
"""
NONE = auto() NONE = auto()
LOCAL = auto() LOCAL = auto()
EXTERN = auto() EXTERN = auto()
@ -66,7 +68,8 @@ class StatusData(BaseObject):
""" """
dateTimeFormatting = '%Y-%m-%d %H:%M:%S.%f' dateTimeFormatting = '%Y-%m-%d %H:%M:%S.%f'
def __init__(self, nodeName='', nodeType='', packageName='', packageVersion='', mrNodeType: MrNodeType = MrNodeType.NONE, parent: BaseObject = None): def __init__(self, nodeName='', nodeType='', packageName='', packageVersion='',
mrNodeType: MrNodeType = MrNodeType.NONE, parent: BaseObject = None):
super(StatusData, self).__init__(parent) super(StatusData, self).__init__(parent)
self.nodeName: str = nodeName self.nodeName: str = nodeName
@ -81,12 +84,13 @@ class StatusData(BaseObject):
self.resetDynamicValues() self.resetDynamicValues()
def setNode(self, node): def setNode(self, node):
""" Set the node information from one node instance.""" """ Set the node information from one node instance. """
self.nodeName = node.name self.nodeName = node.name
self.setNodeType(node) self.setNodeType(node)
def setNodeType(self, node): def setNodeType(self, node):
""" Set the node type and package information from the given node. """
Set the node type and package information from the given node.
We do not set the name in this method as it may vary if there are duplicates. We do not set the name in this method as it may vary if there are duplicates.
""" """
self.nodeType = node.nodeType self.nodeType = node.nodeType
@ -132,17 +136,21 @@ class StatusData(BaseObject):
# we don't want to modify the execMode set from the submit. # we don't want to modify the execMode set from the submit.
def initIsolatedCompute(self): def initIsolatedCompute(self):
''' When submitting a node, we reset the status information to ensure that we do not keep outdated information. """
''' When submitting a node, we reset the status information to ensure that we do not keep
outdated information.
"""
self.resetDynamicValues() self.resetDynamicValues()
self.initStartCompute() self.initStartCompute()
assert(self.mrNodeType == MrNodeType.NODE) assert self.mrNodeType == MrNodeType.NODE
self.sessionUid = None self.sessionUid = None
self.submitterSessionUid = meshroom.core.sessionUid self.submitterSessionUid = meshroom.core.sessionUid
def initExternSubmit(self): def initExternSubmit(self):
''' When submitting a node, we reset the status information to ensure that we do not keep outdated information. """
''' When submitting a node, we reset the status information to ensure that we do not keep
outdated information.
"""
self.resetDynamicValues() self.resetDynamicValues()
self.sessionUid = None self.sessionUid = None
self.submitterSessionUid = meshroom.core.sessionUid self.submitterSessionUid = meshroom.core.sessionUid
@ -150,8 +158,10 @@ class StatusData(BaseObject):
self.execMode = ExecMode.EXTERN self.execMode = ExecMode.EXTERN
def initLocalSubmit(self): def initLocalSubmit(self):
''' When submitting a node, we reset the status information to ensure that we do not keep outdated information. """
''' When submitting a node, we reset the status information to ensure that we do not keep
outdated information.
"""
self.resetDynamicValues() self.resetDynamicValues()
self.sessionUid = None self.sessionUid = None
self.submitterSessionUid = meshroom.core.sessionUid self.submitterSessionUid = meshroom.core.sessionUid
@ -181,29 +191,29 @@ class StatusData(BaseObject):
return d return d
def fromDict(self, d): def fromDict(self, d):
self.status = d.get('status', Status.NONE) self.status = d.get("status", Status.NONE)
if not isinstance(self.status, Status): if not isinstance(self.status, Status):
self.status = Status[self.status] self.status = Status[self.status]
self.execMode = d.get('execMode', ExecMode.NONE) self.execMode = d.get("execMode", ExecMode.NONE)
if not isinstance(self.execMode, ExecMode): if not isinstance(self.execMode, ExecMode):
self.execMode = ExecMode[self.execMode] self.execMode = ExecMode[self.execMode]
self.mrNodeType = d.get('mrNodeType', MrNodeType.NONE) self.mrNodeType = d.get("mrNodeType", MrNodeType.NONE)
if not isinstance(self.mrNodeType, MrNodeType): if not isinstance(self.mrNodeType, MrNodeType):
self.mrNodeType = MrNodeType[self.mrNodeType] self.mrNodeType = MrNodeType[self.mrNodeType]
self.nodeName = d.get('nodeName', '') self.nodeName = d.get("nodeName", "")
self.nodeType = d.get('nodeType', '') self.nodeType = d.get("nodeType", "")
self.packageName = d.get('packageName', '') self.packageName = d.get("packageName", "")
self.packageVersion = d.get('packageVersion', '') self.packageVersion = d.get("packageVersion", "")
self.graph = d.get('graph', '') self.graph = d.get("graph", "")
self.commandLine = d.get('commandLine', '') self.commandLine = d.get("commandLine", "")
self.env = d.get('env', '') self.env = d.get("env", "")
self.startDateTime = d.get('startDateTime', '') self.startDateTime = d.get("startDateTime", "")
self.endDateTime = d.get('endDateTime', '') self.endDateTime = d.get("endDateTime", "")
self.elapsedTime = d.get('elapsedTime', 0) self.elapsedTime = d.get("elapsedTime", 0)
self.hostname = d.get('hostname', '') self.hostname = d.get("hostname", "")
self.sessionUid = d.get('sessionUid', '') self.sessionUid = d.get("sessionUid", "")
self.submitterSessionUid = d.get('submitterSessionUid', '') self.submitterSessionUid = d.get("submitterSessionUid", "")
class LogManager: class LogManager:
@ -223,7 +233,8 @@ class LogManager:
for handler in self.logger.handlers[:]: for handler in self.logger.handlers[:]:
self.logger.removeHandler(handler) self.logger.removeHandler(handler)
handler = logging.FileHandler(self.chunk.logFile) handler = logging.FileHandler(self.chunk.logFile)
formatter = self.Formatter('[%(asctime)s.%(msecs)03d][%(levelname)s] %(message)s', self.dateTimeFormatting) formatter = self.Formatter('[%(asctime)s.%(msecs)03d][%(levelname)s] %(message)s',
self.dateTimeFormatting)
handler.setFormatter(formatter) handler.setFormatter(formatter)
self.logger.addHandler(handler) self.logger.addHandler(handler)
@ -284,15 +295,15 @@ class LogManager:
self.progressBar = False self.progressBar = False
def textToLevel(self, text): def textToLevel(self, text):
if text == 'critical': if text == "critical":
return logging.CRITICAL return logging.CRITICAL
elif text == 'error': elif text == "error":
return logging.ERROR return logging.ERROR
elif text == 'warning': elif text == "warning":
return logging.WARNING return logging.WARNING
elif text == 'info': elif text == "info":
return logging.INFO return logging.INFO
elif text == 'debug': elif text == "debug":
return logging.DEBUG return logging.DEBUG
else: else:
return logging.NOTSET return logging.NOTSET
@ -313,7 +324,8 @@ class NodeChunk(BaseObject):
self.node = node self.node = node
self.range = range self.range = range
self.logManager: LogManager = LogManager(self) self.logManager: LogManager = LogManager(self)
self._status: StatusData = StatusData(node.name, node.nodeType, node.packageName, node.packageVersion, node.getMrNodeType()) self._status: StatusData = StatusData(node.name, node.nodeType, node.packageName,
node.packageVersion, node.getMrNodeType())
self.statistics: stats.Statistics = stats.Statistics() self.statistics: stats.Statistics = stats.Statistics()
self.statusFileLastModTime = -1 self.statusFileLastModTime = -1
self.subprocess = None self.subprocess = None
@ -375,21 +387,24 @@ class NodeChunk(BaseObject):
if self.range.blockSize == 0: if self.range.blockSize == 0:
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "status") return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "status")
else: else:
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, str(self.index) + ".status") return os.path.join(self.node.graph.cacheDir, self.node.internalFolder,
str(self.index) + ".status")
@property @property
def statisticsFile(self): def statisticsFile(self):
if self.range.blockSize == 0: if self.range.blockSize == 0:
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "statistics") return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "statistics")
else: else:
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, str(self.index) + ".statistics") return os.path.join(self.node.graph.cacheDir, self.node.internalFolder,
str(self.index) + ".statistics")
@property @property
def logFile(self): def logFile(self):
if self.range.blockSize == 0: if self.range.blockSize == 0:
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "log") return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, "log")
else: else:
return os.path.join(self.node.graph.cacheDir, self.node.internalFolder, str(self.index) + ".log") return os.path.join(self.node.graph.cacheDir, self.node.internalFolder,
str(self.index) + ".log")
def saveStatusFile(self): def saveStatusFile(self):
""" """
@ -406,7 +421,8 @@ class NodeChunk(BaseObject):
renameWritingToFinalPath(statusFilepathWriting, statusFilepath) renameWritingToFinalPath(statusFilepathWriting, statusFilepath)
def upgradeStatusFile(self): def upgradeStatusFile(self):
""" Upgrade node status file based on the current status. """
Upgrade node status file based on the current status.
""" """
self.saveStatusFile() self.saveStatusFile()
self.statusChanged.emit() self.statusChanged.emit()
@ -468,7 +484,8 @@ class NodeChunk(BaseObject):
# Start the process environment for nodes running in isolation. # Start the process environment for nodes running in isolation.
# This only happens once, when the node has the SUBMITTED status. # This only happens once, when the node has the SUBMITTED status.
# The sub-process will go through this method again, but the node status will have been set to RUNNING. # The sub-process will go through this method again, but the node status will
# have been set to RUNNING.
if not inCurrentEnv and self.node.getMrNodeType() == MrNodeType.NODE: if not inCurrentEnv and self.node.getMrNodeType() == MrNodeType.NODE:
self._processInIsolatedEnvironment() self._processInIsolatedEnvironment()
return return
@ -541,12 +558,14 @@ class NodeChunk(BaseObject):
self.updateStatusFromCache() self.updateStatusFromCache()
if self._status.status != Status.RUNNING: if self._status.status != Status.RUNNING:
# When we stop the process of a node with multiple chunks, the Node function will call the stop function of each chunk. # When we stop the process of a node with multiple chunks, the Node function will call
# So, the chunck status could be SUBMITTED, RUNNING or ERROR. # the stop function of each chunk.
# So, the chunk status could be SUBMITTED, RUNNING or ERROR.
if self._status.status is Status.SUBMITTED: if self._status.status is Status.SUBMITTED:
self.upgradeStatusTo(Status.NONE) self.upgradeStatusTo(Status.NONE)
elif self._status.status in (Status.ERROR, Status.STOPPED, Status.KILLED, Status.SUCCESS, Status.NONE): elif self._status.status in (Status.ERROR, Status.STOPPED, Status.KILLED,
Status.SUCCESS, Status.NONE):
# Nothing to do, the computation is already stopped. # Nothing to do, the computation is already stopped.
pass pass
else: else:
@ -560,8 +579,10 @@ class NodeChunk(BaseObject):
self.upgradeStatusTo(Status.STOPPED) self.upgradeStatusTo(Status.STOPPED)
def isExtern(self): def isExtern(self):
""" The computation is managed externally by another instance of Meshroom. """
In the ambiguous case of an isolated environment, it is considered as local as we can stop it (if it is run from the current Meshroom instance). The computation is managed externally by another instance of Meshroom.
In the ambiguous case of an isolated environment, it is considered as local as we can stop
it (if it is run from the current Meshroom instance).
""" """
if self._status.execMode == ExecMode.EXTERN: if self._status.execMode == ExecMode.EXTERN:
return True return True
@ -603,7 +624,8 @@ 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: str, position: Position = None, parent: BaseObject = None, uid: str = None, **kwargs): def __init__(self, nodeType: str, position: Position = None, parent: BaseObject = None,
uid: str = 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.
@ -656,7 +678,8 @@ class BaseNode(BaseObject):
raise e raise e
def getMrNodeType(self): def getMrNodeType(self):
# In compatibility mode, we may or may not have access to the nodeDesc and its information about the node type. # In compatibility mode, we may or may not have access to the nodeDesc and its information
# about the node type.
if self.nodeDesc is None: if self.nodeDesc is None:
return MrNodeType.NONE return MrNodeType.NONE
return self.nodeDesc.getMrNodeType() return self.nodeDesc.getMrNodeType()
@ -826,22 +849,25 @@ class BaseNode(BaseObject):
return os.path.join(self.graph.cacheDir, self.internalFolder, 'values') return os.path.join(self.graph.cacheDir, self.internalFolder, 'values')
def getInputNodes(self, recursive, dependenciesOnly): def getInputNodes(self, recursive, dependenciesOnly):
return self.graph.getInputNodes(self, recursive=recursive, dependenciesOnly=dependenciesOnly) return self.graph.getInputNodes(self, recursive=recursive,
dependenciesOnly=dependenciesOnly)
def getOutputNodes(self, recursive, dependenciesOnly): def getOutputNodes(self, recursive, dependenciesOnly):
return self.graph.getOutputNodes(self, recursive=recursive, dependenciesOnly=dependenciesOnly) return self.graph.getOutputNodes(self, recursive=recursive,
dependenciesOnly=dependenciesOnly)
def toDict(self): def toDict(self):
pass pass
def _computeUid(self): def _computeUid(self):
""" Compute node UID by combining associated attributes' UIDs. """ """ 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 # If there is no invalidating attribute, then the computation of the UID should not
# it will only include the node type # go through as it will only include the node type
if not self.invalidatingAttributes: if not self.invalidatingAttributes:
return 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 attr in self.invalidatingAttributes: for attr in self.invalidatingAttributes:
if not attr.enabled: if not attr.enabled:
@ -862,6 +888,10 @@ class BaseNode(BaseObject):
self._uid = hashValue(uidAttributes) self._uid = hashValue(uidAttributes)
def _buildCmdVars(self): def _buildCmdVars(self):
"""
Generate command variables using input attributes and resolved output attributes
names and values.
"""
def _buildAttributeCmdVars(cmdVars, name, attr): def _buildAttributeCmdVars(cmdVars, name, attr):
if attr.enabled: if attr.enabled:
group = attr.attributeDesc.group(attr.node) \ group = attr.attributeDesc.group(attr.node) \
@ -886,7 +916,6 @@ class BaseNode(BaseObject):
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. """
self._cmdVars["uid"] = self._uid self._cmdVars["uid"] = self._uid
self._cmdVars["nodeCacheFolder"] = self.internalFolder self._cmdVars["nodeCacheFolder"] = self.internalFolder
self._cmdVars["nodeSourceCodeFolder"] = self.sourceCodeFolder self._cmdVars["nodeSourceCodeFolder"] = self.sourceCodeFolder
@ -901,8 +930,9 @@ class BaseNode(BaseObject):
cmdVarsNoCache = self._cmdVars.copy() cmdVarsNoCache = self._cmdVars.copy()
cmdVarsNoCache["cache"] = "" cmdVarsNoCache["cache"] = ""
# Use "self._internalFolder" instead of "self.internalFolder" because we do not want it to be # Use "self._internalFolder" instead of "self.internalFolder" because we do not want it to
# resolved with the {cache} information ("self.internalFolder" resolves "self._internalFolder") # be resolved with the {cache} information ("self.internalFolder" resolves
# "self._internalFolder")
cmdVarsNoCache["nodeCacheFolder"] = self._internalFolder.format(**cmdVarsNoCache) cmdVarsNoCache["nodeCacheFolder"] = self._internalFolder.format(**cmdVarsNoCache)
# Evaluate output params # Evaluate output params
@ -919,8 +949,8 @@ class BaseNode(BaseObject):
try: try:
defaultValue = attr.defaultValue() defaultValue = attr.defaultValue()
except AttributeError: except AttributeError:
# If we load an old scene, the lambda associated to the 'value' could try to access other # If we load an old scene, the lambda associated to the 'value' could try to
# params that could not exist yet # access other params that could not exist yet
logging.warning('Invalid lambda evaluation for "{nodeName}.{attrName}"'. logging.warning('Invalid lambda evaluation for "{nodeName}.{attrName}"'.
format(nodeName=self.name, attrName=attr.name)) format(nodeName=self.name, attrName=attr.name))
if defaultValue is not None: if defaultValue is not None:
@ -945,8 +975,8 @@ class BaseNode(BaseObject):
self._cmdVars[name + 'Value'] = attr.getValueStr(withQuotes=False) self._cmdVars[name + 'Value'] = attr.getValueStr(withQuotes=False)
if v: if v:
self._cmdVars[attr.attributeDesc.group] = self._cmdVars.get(attr.attributeDesc.group, '') + \ self._cmdVars[attr.attributeDesc.group] = \
' ' + self._cmdVars[name] self._cmdVars.get(attr.attributeDesc.group, '') + ' ' + self._cmdVars[name]
@property @property
def isParallelized(self): def isParallelized(self):
@ -958,7 +988,7 @@ class BaseNode(BaseObject):
def hasStatus(self, status: Status): def hasStatus(self, status: Status):
if not self._chunks: if not self._chunks:
return (status == Status.INPUT) return status == Status.INPUT
for chunk in self._chunks: for chunk in self._chunks:
if chunk.status.status != status: if chunk.status.status != status:
return False return False
@ -973,7 +1003,8 @@ class BaseNode(BaseObject):
""" Return True if this node type is computable, False otherwise. """ Return True if this node type is computable, False otherwise.
A computable node type can be in a context that does not allow computation. A computable node type can be in a context that does not allow computation.
""" """
# Ambiguous case for NONE, which could be used for compatibility nodes if we don't have any information about the node descriptor. # Ambiguous case for NONE, which could be used for compatibility nodes if we don't have
# any information about the node descriptor.
return self.getMrNodeType() != MrNodeType.INPUT return self.getMrNodeType() != MrNodeType.INPUT
def clearData(self): def clearData(self):
@ -984,9 +1015,11 @@ class BaseNode(BaseObject):
try: try:
shutil.rmtree(self.internalFolder) shutil.rmtree(self.internalFolder)
except Exception as e: except Exception as e:
# We could get some "Device or resource busy" on .nfs file while removing the folder on linux network. # We could get some "Device or resource busy" on .nfs file while removing the folder
# On windows, some output files may be open for visualization and the removal will fail. # on Linux network.
# On both cases, we can ignore it. # On Windows, some output files may be open for visualization and the removal will
# fail.
# In both cases, we can ignore it.
logging.warning(f"Failed to remove internal folder: '{self.internalFolder}'. Error: {e}.") logging.warning(f"Failed to remove internal folder: '{self.internalFolder}'. Error: {e}.")
self.updateStatusFromCache() self.updateStatusFromCache()
@ -1011,7 +1044,10 @@ class BaseNode(BaseObject):
@Slot(result=bool) @Slot(result=bool)
def isSubmittedOrRunning(self): def isSubmittedOrRunning(self):
""" Return True if all chunks are at least submitted and there is one running chunk, False otherwise. """ """
Return True if all chunks are at least submitted and there is one running chunk,
False otherwise.
"""
if not self.isAlreadySubmittedOrFinished(): if not self.isAlreadySubmittedOrFinished():
return False return False
for chunk in self._chunks: for chunk in self._chunks:
@ -1026,7 +1062,10 @@ class BaseNode(BaseObject):
@Slot(result=bool) @Slot(result=bool)
def isFinishedOrRunning(self): def isFinishedOrRunning(self):
""" Return True if all chunks of this Node is either finished or running, False otherwise. """ """
Return True if all chunks of this Node is either finished or running, False
otherwise.
"""
return all(chunk.isFinishedOrRunning() for chunk in self._chunks) return all(chunk.isFinishedOrRunning() for chunk in self._chunks)
@Slot(result=bool) @Slot(result=bool)
@ -1038,12 +1077,15 @@ class BaseNode(BaseObject):
return [ch for ch in self._chunks if ch.isAlreadySubmitted()] return [ch for ch in self._chunks if ch.isAlreadySubmitted()]
def isExtern(self): def isExtern(self):
""" Return True if at least one chunk of this Node has an external execution mode, False otherwise. """
Return True if at least one chunk of this Node has an external execution mode,
False otherwise.
It is not enough to check whether the first chunk's execution mode is external, because computations It is not enough to check whether the first chunk's execution mode is external,
may have been started locally, interrupted, and restarted externally. In that case, if the first because computations may have been started locally, interrupted, and restarted externally.
chunk has completed locally before the computations were interrupted, its execution mode will always In that case, if the first chunk has completed locally before the computations were
be local, even if computations resume externally. interrupted, its execution mode will always be local, even if computations resume
externally.
""" """
if len(self._chunks) == 0: if len(self._chunks) == 0:
return False return False
@ -1051,8 +1093,9 @@ class BaseNode(BaseObject):
@Slot() @Slot()
def clearSubmittedChunks(self): def clearSubmittedChunks(self):
""" Reset all submitted chunks to Status.NONE. This method should be used to clear inconsistent status """
if a computation failed without informing the graph. Reset all submitted chunks to Status.NONE. This method should be used to clear
inconsistent status if a computation failed without informing the graph.
Warnings: Warnings:
This must be used with caution. This could lead to inconsistent node status This must be used with caution. This could lead to inconsistent node status
@ -1069,9 +1112,7 @@ class BaseNode(BaseObject):
chunk.upgradeStatusTo(Status.NONE, ExecMode.NONE) chunk.upgradeStatusTo(Status.NONE, ExecMode.NONE)
def upgradeStatusTo(self, newStatus): def upgradeStatusTo(self, newStatus):
""" """ Upgrade node to the given status and save it on disk. """
Upgrade node to the given status and save it on disk.
"""
for chunk in self._chunks: for chunk in self._chunks:
chunk.upgradeStatusTo(newStatus) chunk.upgradeStatusTo(newStatus)
@ -1083,7 +1124,7 @@ class BaseNode(BaseObject):
pass pass
def _getAttributeChangedCallback(self, attr: Attribute) -> Optional[Callable]: def _getAttributeChangedCallback(self, attr: Attribute) -> Optional[Callable]:
"""Get the node descriptor-defined value changed callback associated to `attr` if any.""" """ Get the node descriptor-defined value changed callback associated to `attr` if any. """
# Callbacks cannot be defined on nested attributes. # Callbacks cannot be defined on nested attributes.
if attr.root is not None: if attr.root is not None:
@ -1097,7 +1138,8 @@ class BaseNode(BaseObject):
def _onAttributeChanged(self, attr: Attribute): def _onAttributeChanged(self, attr: Attribute):
""" """
When an attribute value has changed, a specific function can be defined in the descriptor and be called. When an attribute value has changed, a specific function can be defined in the descriptor
and be called.
Args: Args:
attr: The Attribute that has changed. attr: The Attribute that has changed.
@ -1132,7 +1174,9 @@ class BaseNode(BaseObject):
edge.dst.valueChanged.emit() edge.dst.valueChanged.emit()
def onAttributeClicked(self, attr): def onAttributeClicked(self, attr):
""" When an attribute is clicked, a specific function can be defined in the descriptor and be called. """
When an attribute is clicked, a specific function can be defined in the descriptor
and be called.
Args: Args:
attr (Attribute): attribute that has been clicked attr (Attribute): attribute that has been clicked
@ -1230,7 +1274,8 @@ class BaseNode(BaseObject):
chunk.process(forceCompute, inCurrentEnv) chunk.process(forceCompute, inCurrentEnv)
def postprocess(self): def postprocess(self):
# Invoke the post process on Client Node to execute after the processing on the node is completed # Invoke the post process on Client Node to execute after the processing on the
# node is completed
self.nodeDesc.postprocess(self) self.nodeDesc.postprocess(self)
def updateOutputAttr(self): def updateOutputAttr(self):
@ -1546,8 +1591,8 @@ class BaseNode(BaseObject):
def hasImageOutputAttribute(self) -> bool: def hasImageOutputAttribute(self) -> bool:
""" """
Return True if at least one attribute has the 'image' semantic (and can thus be loaded in the 2D Viewer), Return True if at least one attribute has the 'image' semantic (and can thus be loaded in
False otherwise. the 2D Viewer), False otherwise.
""" """
for attr in self._attributes: for attr in self._attributes:
if not attr.enabled or not attr.isOutput: if not attr.enabled or not attr.isOutput:
@ -1558,8 +1603,8 @@ class BaseNode(BaseObject):
def hasSequenceOutputAttribute(self) -> bool: def hasSequenceOutputAttribute(self) -> bool:
""" """
Return True if at least one attribute has the 'sequence' semantic (and can thus be loaded in the 2D Viewer), Return True if at least one attribute has the 'sequence' semantic (and can thus be loaded in
False otherwise. the 2D Viewer), False otherwise.
""" """
for attr in self._attributes: for attr in self._attributes:
if not attr.enabled or not attr.isOutput: if not attr.enabled or not attr.isOutput:
@ -1570,7 +1615,8 @@ class BaseNode(BaseObject):
def has3DOutputAttribute(self): def has3DOutputAttribute(self):
""" """
Return True if at least one attribute is a File that can be loaded in the 3D Viewer, False otherwise. Return True if at least one attribute is a File that can be loaded in the 3D Viewer,
False otherwise.
""" """
# List of supported extensions, taken from Viewer3DSettings # List of supported extensions, taken from Viewer3DSettings
supportedExts = ['.obj', '.stl', '.fbx', '.gltf', '.abc', '.ply'] supportedExts = ['.obj', '.stl', '.fbx', '.gltf', '.abc', '.ply']
@ -1654,14 +1700,16 @@ class Node(BaseNode):
self._sourceCodeFolder = self.nodeDesc.sourceCodeFolder self._sourceCodeFolder = self.nodeDesc.sourceCodeFolder
for attrDesc in self.nodeDesc.inputs: for attrDesc in self.nodeDesc.inputs:
self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=False, node=self)) self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
isOutput=False, node=self))
for attrDesc in self.nodeDesc.outputs: for attrDesc in self.nodeDesc.outputs:
self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=True, node=self)) self._attributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
isOutput=True, node=self))
for attrDesc in self.nodeDesc.internalInputs: for attrDesc in self.nodeDesc.internalInputs:
self._internalAttributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None), isOutput=False, self._internalAttributes.add(attributeFactory(attrDesc, kwargs.get(attrDesc.name, None),
node=self)) isOutput=False, node=self))
# Declare events for specific output attributes # Declare events for specific output attributes
for attr in self._attributes: for attr in self._attributes:
@ -1891,7 +1939,8 @@ class CompatibilityNode(BaseNode):
@staticmethod @staticmethod
def attributeDescFromName(refAttributes, name, value, strict=True): def attributeDescFromName(refAttributes, name, value, strict=True):
""" """
Try to find a matching attribute description in refAttributes for given attribute 'name' and 'value'. Try to find a matching attribute description in refAttributes for given attribute
'name' and 'value'.
Args: Args:
refAttributes ([desc.Attribute]): reference Attributes to look for a description refAttributes ([desc.Attribute]): reference Attributes to look for a description