diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 113cab206b..a83ccfd807 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -40,7 +40,7 @@ jobs: - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --select=E9,F63,F7,F82,F401 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest @@ -77,7 +77,7 @@ jobs: - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --select=E9,F63,F7,F82,F401 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest diff --git a/bin/meshroom_compute b/bin/meshroom_compute index 2cbfe25e8b..e714957402 100755 --- a/bin/meshroom_compute +++ b/bin/meshroom_compute @@ -16,7 +16,7 @@ meshroom.setupEnvironment() import meshroom.core import meshroom.core.graph -from meshroom.core.node import Status, ExecMode +from meshroom.core.node import Status parser = argparse.ArgumentParser(description='Execute a Graph of processes.') diff --git a/meshroom/__init__.py b/meshroom/__init__.py index d487f22d67..10cc1fe948 100644 --- a/meshroom/__init__.py +++ b/meshroom/__init__.py @@ -4,6 +4,7 @@ import os import sys + class VersionStatus(Enum): release = 1 develop = 2 @@ -42,16 +43,18 @@ class VersionStatus(Enum): useMultiChunks = util.strtobool(os.environ.get("MESHROOM_USE_MULTI_CHUNKS", "True")) -# Logging +# Logging def addTraceLevel(): """ From https://stackoverflow.com/a/35804945 """ levelName, methodName, levelNum = 'TRACE', 'trace', logging.DEBUG - 5 - if hasattr(logging, levelName) or hasattr(logging, methodName)or hasattr(logging.getLoggerClass(), methodName): - return + if hasattr(logging, levelName) or hasattr(logging, methodName) or hasattr(logging.getLoggerClass(), methodName): + return + def logForLevel(self, message, *args, **kwargs): if self.isEnabledFor(levelNum): self._log(levelNum, message, args, **kwargs) + def logToRoot(message, *args, **kwargs): logging.log(levelNum, message, *args, **kwargs) @@ -76,7 +79,8 @@ def setupEnvironment(backend=Backend.STANDALONE): """ Setup environment for Meshroom to work in a prebuilt, standalone configuration. - Use 'MESHROOM_INSTALL_DIR' to simulate standalone configuration with a path to a Meshroom installation folder. + Use 'MESHROOM_INSTALL_DIR' to simulate standalone configuration with a path to a Meshroom + installation folder. # Meshroom standalone structure diff --git a/meshroom/core/__init__.py b/meshroom/core/__init__.py index 3a472fedbe..871568dd19 100644 --- a/meshroom/core/__init__.py +++ b/meshroom/core/__init__.py @@ -22,7 +22,7 @@ from meshroom.core.submitter import BaseSubmitter from meshroom.env import EnvVar, meshroomFolder from . import desc -from .desc import MrNodeType +from .desc import MrNodeType # Not used here but simplifies imports for files that need it # Setup logging logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=logging.INFO) @@ -81,7 +81,8 @@ def loadClasses(folder: str, packageName: str, classType: type) -> list[type]: except Exception as e: tb = traceback.extract_tb(e.__traceback__) last_call = tb[-1] - logging.warning(f' * Failed to load package "{packageName}" from folder "{resolvedFolder}" ({type(e).__name__}): {str(e)}\n' + logging.warning(f' * Failed to load package "{packageName}" from folder ' + f'"{resolvedFolder}" ({type(e).__name__}): {str(e)}\n' # filename:lineNumber functionName f'{last_call.filename}:{last_call.lineno} {last_call.name}\n' # line of code with the error @@ -105,7 +106,8 @@ def loadClasses(folder: str, packageName: str, classType: type) -> list[type]: isPackage = hasattr(pluginMod, "__path__") # Sub-folders/Packages should not raise a warning if not isPackage: - logging.warning(f"No class defined in plugin: {package.__name__}.{pluginName} ('{pluginMod.__file__}')") + logging.warning(f"No class defined in plugin: " + f"{package.__name__}.{pluginName} ('{pluginMod.__file__}')") for p in plugins: p.packageName = packageName @@ -114,8 +116,8 @@ def loadClasses(folder: str, packageName: str, classType: type) -> list[type]: if classType == desc.BaseNode: nodePlugin = NodePlugin(p) if nodePlugin.errors: - errors.append(" * {}: The following parameters do not have valid " \ - "default values/ranges: {}".format(pluginName, ", ".join(nodePlugin.errors))) + errors.append(f" * {pluginName}: The following parameters do not have " + f"valid default values/ranges: {', '.join(nodePlugin.errors)}") classes.append(nodePlugin) else: classes.append(p) @@ -438,7 +440,7 @@ def initPlugins(): plugin.processEnv = processEnvFactory(f, plugin.configEnv) # Rez plugins (with a RezProcessEnv) - rezPlugins = initRezPlugins() + initRezPlugins() def initRezPlugins(): @@ -452,6 +454,7 @@ def initRezPlugins(): # Set the ProcessEnv for Rez plugins if plugins: for plugin in plugins: - plugin.processEnv = processEnvFactory(path, plugin.configEnv, envType="rez", uri=name) + plugin.processEnv = processEnvFactory(path, plugin.configEnv, envType="rez", + uri=name) return rezPlugins diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 703b8aaed4..247130e370 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -10,10 +10,6 @@ from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot from meshroom.core import desc, hashValue -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from meshroom.core.graph import Edge def attributeFactory(description: str, value, isOutput: bool, node, root=None, parent=None): """ @@ -75,21 +71,21 @@ def __init__(self, node, attributeDesc: desc.Attribute, isOutput: bool, root=Non self._isOutput: bool = isOutput self._enabled: bool = True self._invalidate = False if self._isOutput else attributeDesc.invalidate - self._invalidationValue = "" # invalidation value for output attributes + self._invalidationValue = "" # invalidation value for output attributes self._value = None self._initValue() def _getFullName(self) -> str: - """ + """ Get the attribute name following the path from the node to the attribute. - Return: nodeName.groupName.subGroupName.name + Return: nodeName.groupName.subGroupName.name """ return f'{self.node.name}.{self._getRootName()}' def _getRootName(self) -> str: - """ + """ Get the attribute name following the path from the root attribute. - Return: groupName.subGroupName.name + Return: groupName.subGroupName.name """ if isinstance(self.root, ListAttribute): return f'{self.root.rootName}[{self.root.index(self)}]' @@ -98,8 +94,8 @@ def _getRootName(self) -> str: return self._desc.name def asLinkExpr(self) -> str: - """ - Return the link expression for this Attribute + """ + Return the link expression for this Attribute """ return "{" + self._getFullName() + "}" @@ -150,7 +146,7 @@ def _getValue(self): def _setValue(self, value): """ - Set the attribute value from a given value, a given function or a given attribute. + Set the attribute value from a given value, a given function or a given attribute. """ if self._value == value: return @@ -220,7 +216,8 @@ def getDefaultValue(self): return self._desc.value(self) except Exception as e: if not self.node.isCompatibilityNode: - logging.warning(f"Failed to evaluate 'defaultValue' (node lambda) for attribute '{self.fullName}': {e}") + logging.warning(f"Failed to evaluate 'defaultValue' (node lambda) " + f"for attribute '{self.fullName}': {e}") return None # Need to force a copy, for the case where the value is a list # (avoid reference to the desc value) @@ -284,13 +281,14 @@ def _isValid(self): return self._desc.validValue(self.node) except Exception as e: if not self.node.isCompatibilityNode: - logging.warning(f"Failed to evaluate 'isValid' (node lambda) for attribute '{self.fullName}': {e}") + logging.warning(f"Failed to evaluate 'isValid' (node lambda) for " + f"attribute '{self.fullName}': {e}") return True return True def _is2dDisplayable(self) -> bool: - """ - Return True if the current attribute is considered as a displayable 2d file + """ + Return True if the current attribute is considered as a displayable 2d file """ if not self._desc.semantic: return False @@ -304,7 +302,8 @@ def _is3dDisplayable(self) -> bool: if self._desc.semantic == "3d": return True # If the attribute is a File attribute, it is an instance of str and can be iterated over - hasSupportedExt = isinstance(self.value, str) and any(ext in self.value for ext in Attribute.VALID_3D_EXTENSIONS) + hasSupportedExt = isinstance(self.value, str) and any(ext in self.value for + ext in Attribute.VALID_3D_EXTENSIONS) if hasSupportedExt: return True return False @@ -358,14 +357,14 @@ def _setEnabled(self, v): self.enabledChanged.emit() def _isLink(self) -> bool: - """ - Whether the attribute is a link to another attribute. + """ + Whether the attribute is a link to another attribute. """ return self.node.graph and self.isInput and self.node.graph._edges and \ self in self.node.graph._edges.keys() def _getInputLink(self, recursive=False) -> "Attribute": - """ + """ Return the direct upstream connected attribute. :param recursive: recursive call, return the root attribute """ @@ -377,7 +376,7 @@ def _getInputLink(self, recursive=False) -> "Attribute": return linkAttribute def _getOutputLinks(self) -> list["Attribute"]: - """ + """ Return the list of direct downstream connected attributes. """ # Safety check to avoid evaluation errors @@ -386,16 +385,16 @@ def _getOutputLinks(self) -> list["Attribute"]: return [edge.dst for edge in self.node.graph.edges.values() if edge.src == self] def _getAllInputLinks(self) -> list["Attribute"]: - """ + """ Return the list of upstream connected attributes for the attribute or any of its elements. """ inputLink = self._getInputLink() - if inputLink is None: + if inputLink is None: return [] return [inputLink] def _getAllOutputLinks(self) -> list["Attribute"]: - """ + """ Return the list of downstream connected attributes for the attribute or any of its elements. """ return self._getOutputLinks() @@ -407,7 +406,8 @@ def _hasAnyInputLinks(self) -> bool: # Safety check to avoid evaluation errors if not self.node.graph or not self.node.graph.edges: return False - return next((edge for edge in self.node.graph.edges.values() if edge.dst == self), None) is not None + return next((edge for edge in self.node.graph.edges.values() + if edge.dst == self), None) is not None def _hasAnyOutputLinks(self) -> bool: """ @@ -416,7 +416,8 @@ def _hasAnyOutputLinks(self) -> bool: # Safety check to avoid evaluation errors if not self.node.graph or not self.node.graph.edges: return False - return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None + return next((edge for edge in self.node.graph.edges.values() + if edge.src == self), None) is not None # Slots @@ -428,7 +429,7 @@ def _onValueChanged(self): def matchText(self, text: str) -> bool: return self.label.lower().find(text.lower()) > -1 - # Properties and signals + # Properties and signals # The node that contains this attribute. node = Property(BaseObject, lambda self: self._node(), constant=True) @@ -453,7 +454,8 @@ def matchText(self, text: str) -> bool: # Whether the attribute is a node output attribute. isOutput = Property(bool, lambda self: self._isOutput, constant=True) # Whether the attribute is a read-only attribute. - isReadOnly = Property(bool, lambda self: not self._isOutput and self.node.isCompatibilityNode, constant=True) + isReadOnly = Property(bool, lambda self: not self._isOutput and self.node.isCompatibilityNode, + constant=True) # Whether changing this attribute invalidates cached results. invalidate = Property(bool, lambda self: self._invalidate, constant=True) # Whether this attribute is enabled. @@ -466,14 +468,15 @@ def matchText(self, text: str) -> bool: evalValue = Property(Variant, _getEvalValue, notify=valueChanged) # Whether the attribute value is the default value. - isDefault = Property(bool, lambda self: self.value == self.getDefaultValue(), notify=valueChanged) + isDefault = Property(bool, lambda self: self.value == self.getDefaultValue(), + notify=valueChanged) # Whether the attribute value is valid. isValid = Property(bool, _isValid, notify=valueChanged) # Whether the attribute value is displayable in 2d. is2dDisplayable = Property(bool, _is2dDisplayable, constant=True) # Whether the attribute value is displayable in 3d. is3dDisplayable = Property(bool, _is3dDisplayable, constant=True) - + # Attribute link properties and signals inputLinksChanged = Signal() outputLinksChanged = Signal() @@ -481,7 +484,8 @@ def matchText(self, text: str) -> bool: # Whether the attribute is a link to another attribute. isLink = Property(bool, _isLink, notify=inputLinksChanged) # The upstream connected root attribute. - inputRootLink = Property(Variant, lambda self: self._getInputLink(recursive=True), notify=inputLinksChanged) + inputRootLink = Property(Variant, lambda self: self._getInputLink(recursive=True), + notify=inputLinksChanged) # The upstream connected attribute. inputLink = Property(BaseObject, _getInputLink, notify=inputLinksChanged) # The list of downstream connected attributes. @@ -497,7 +501,7 @@ def matchText(self, text: str) -> bool: def raiseIfLink(func): - """ + """ If Attribute instance is a link, raise a RuntimeError. """ def wrapper(attr, *args, **kwargs): @@ -545,12 +549,12 @@ def validateValue(self, value): if isinstance(value, str): value = value.split(',') if not isinstance(value, Iterable): - raise ValueError("Non exclusive ChoiceParam value should be iterable (param:{}, value:{}, type:{})". - format(self.name, value, type(value))) + raise ValueError(f"Non exclusive ChoiceParam value should be iterable " + f"(param:{self.name}, value:{value}, type:{type(value)})") return [self._conformValue(v) for v in value] def _conformValue(self, val): - """ + """ Conform 'val' to the correct type and check for its validity """ return self._desc.conformValue(val) @@ -596,7 +600,7 @@ def __iter__(self): return iter(self.value) def at(self, idx): - """ + """ Returns child attribute at index 'idx'. """ # Implement 'at' rather than '__getitem__' @@ -700,9 +704,9 @@ def getValueStr(self, withQuotes=True) -> str: assert isinstance(self.value, ListModel) if self._desc.joinChar == ' ': return self._desc.joinChar.join([v.getValueStr(withQuotes=withQuotes) - for v in self.value]) + for v in self.value]) v = self._desc.joinChar.join([v.getValueStr(withQuotes=False) - for v in self.value]) + for v in self.value]) if withQuotes and v: return f'"{v}"' return v @@ -746,23 +750,25 @@ def updateInternals(self): # Override def _getAllInputLinks(self) -> list["Attribute"]: - """ - Return the list of upstream connected attributes for the attribute or any of its elements." + """ + Return the list of upstream connected attributes for the attribute or any of its elements. """ # Safety check to avoid evaluation errors if not self.node.graph or not self.node.graph.edges: return [] - return [edge.src for edge in self.node.graph.edges.values() if edge.dst == self or edge.dst in self._value] + return [edge.src for edge in self.node.graph.edges.values() + if edge.dst == self or edge.dst in self._value] # Override def _getAllOutputLinks(self) -> list["Attribute"]: - """ - Return the list of downstream connected attributes for the attribute or any of its elements." + """ + Return the list of downstream connected attributes for the attribute or any of its elements. """ # Safety check to avoid evaluation errors if not self.node.graph or not self.node.graph.edges: return [] - return [edge.dst for edge in self.node.graph.edges.values() if edge.src == self or edge.src in self._value] + return [edge.dst for edge in self.node.graph.edges.values() + if edge.src == self or edge.src in self._value] # Override def _hasAnyInputLinks(self) -> bool: @@ -770,7 +776,8 @@ def _hasAnyInputLinks(self) -> bool: Whether the attribute or any of its elements is a link to another attribute. """ return super()._hasAnyInputLinks() or \ - any(attribute.hasAnyInputLinks for attribute in self._value if hasattr(attribute, 'hasAnyInputLinks')) + any(attribute.hasAnyInputLinks for attribute in self._value + if hasattr(attribute, 'hasAnyInputLinks')) # Override def _hasAnyOutputLinks(self) -> bool: @@ -778,8 +785,8 @@ def _hasAnyOutputLinks(self) -> bool: Whether the attribute or any of its elements is linked by another attribute. """ return super()._hasAnyOutputLinks() or \ - any(attribute.hasAnyOutputLinks for attribute in self._value if hasattr(attribute, 'hasAnyOutputLinks')) - + any(attribute.hasAnyOutputLinks for attribute in self._value + if hasattr(attribute, 'hasAnyOutputLinks')) # Override value property setter value = Property(Variant, Attribute._getValue, _setValue, notify=Attribute.valueChanged) diff --git a/meshroom/core/desc/attribute.py b/meshroom/core/desc/attribute.py index a88e3abece..84783b3ed5 100644 --- a/meshroom/core/desc/attribute.py +++ b/meshroom/core/desc/attribute.py @@ -414,11 +414,11 @@ class ChoiceParam(Param): When using `exclusive=True`, the value is a single element of the list of possible values. When using `exclusive=False`, the value is a list of elements of the list of possible values. - - Despite this being the standard behavior, ChoiceParam also supports custom value: it is possible to set any value, - even outside list of possible values. - The list of possible values on a ChoiceParam instance can be overriden at runtime. + Despite this being the standard behavior, ChoiceParam also supports custom value: it is possible + to set any value, even outside the list of possible values. + + The list of possible values on a ChoiceParam instance can be overriden at runtime. If those changes needs to be persisted, `saveValuesOverride` should be set to True. """ @@ -426,14 +426,15 @@ class ChoiceParam(Param): _OVERRIDE_SERIALIZATION_KEY_VALUE = "__ChoiceParam_value__" _OVERRIDE_SERIALIZATION_KEY_VALUES = "__ChoiceParam_values__" - def __init__(self, name: str, label: str, description: str, value, values, exclusive=True, saveValuesOverride=False, - group="allParams", joinChar=" ", advanced=False, enabled=True, invalidate=True, semantic="", - validValue=True, errorMessage="", + def __init__(self, name: str, label: str, description: str, value, values, exclusive=True, + saveValuesOverride=False, group="allParams", joinChar=" ", advanced=False, + enabled=True, invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False): - super(ChoiceParam, self).__init__(name=name, label=label, description=description, value=value, - group=group, advanced=advanced, enabled=enabled, invalidate=invalidate, - semantic=semantic, validValue=validValue, errorMessage=errorMessage, + super(ChoiceParam, self).__init__(name=name, label=label, description=description, + value=value, group=group, advanced=advanced, + enabled=enabled, invalidate=invalidate, semantic=semantic, + validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) self._values = values self._saveValuesOverride = saveValuesOverride @@ -475,8 +476,8 @@ def validateValue(self, value): value = value.split(',') if not isinstance(value, Iterable): - raise ValueError("Non-exclusive ChoiceParam value should be iterable (param: {}, value: {}, type: {}).". - format(self.name, value, type(value))) + raise ValueError(f"Non-exclusive ChoiceParam value should be iterable " + f"(param: {self.name}, value: {value}, type: {type(value)}).") return [self.conformValue(v) for v in value] @@ -485,14 +486,14 @@ def checkValueTypes(self): if not isinstance(self._values, list): return self.name - # If the choices are not exclusive, check that 'value' is a list, and check that it does not contain values that - # are not available + # If the choices are not exclusive, check that 'value' is a list, and check that it + # does not contain values that are not available elif not self.exclusive and (not isinstance(self._value, list) or not all(val in self._values for val in self._value)): return self.name - # If the choices are exclusive, the value should NOT be a list but it can contain any value that is not in the - # list of possible ones + # If the choices are exclusive, the value should NOT be a list but it can contain + # any value that is not in the list of possible ones elif self.exclusive and isinstance(self._value, list): return self.name @@ -506,21 +507,24 @@ def checkValueTypes(self): class StringParam(Param): """ """ - def __init__(self, name, label, description, value, group="allParams", advanced=False, enabled=True, - invalidate=True, semantic="", uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, - exposed=False): - super(StringParam, self).__init__(name=name, label=label, description=description, value=value, - group=group, advanced=advanced, enabled=enabled, invalidate=invalidate, - semantic=semantic, uidIgnoreValue=uidIgnoreValue, validValue=validValue, - errorMessage=errorMessage, visible=visible, exposed=exposed) + def __init__(self, name, label, description, value, group="allParams", advanced=False, + enabled=True, invalidate=True, semantic="", uidIgnoreValue=None, validValue=True, + errorMessage="", visible=True, exposed=False): + super(StringParam, self).__init__(name=name, label=label, description=description, + value=value, group=group, advanced=advanced, + enabled=enabled, invalidate=invalidate, semantic=semantic, + uidIgnoreValue=uidIgnoreValue, validValue=validValue, + errorMessage=errorMessage, visible=visible, + exposed=exposed) self._valueType = str def validateValue(self, value): if value is None: return value if not isinstance(value, str): - raise ValueError("StringParam value should be a string (param:{}, value:{}, type:{})". - format(self.name, value, type(value))) + raise ValueError(f"StringParam value should be a string " + f"(param:{self.name}, value:{value}, type:{type(value)})") + return value def checkValueTypes(self): @@ -532,10 +536,11 @@ def checkValueTypes(self): class ColorParam(Param): """ """ - def __init__(self, name, label, description, value, group="allParams", advanced=False, enabled=True, - invalidate=True, semantic="", visible=True, exposed=False): - super(ColorParam, self).__init__(name=name, label=label, description=description, value=value, - group=group, advanced=advanced, enabled=enabled, invalidate=invalidate, + def __init__(self, name, label, description, value, group="allParams", advanced=False, + enabled=True, invalidate=True, semantic="", visible=True, exposed=False): + super(ColorParam, self).__init__(name=name, label=label, description=description, + value=value, group=group, advanced=advanced, + enabled=enabled, invalidate=invalidate, semantic=semantic, visible=visible, exposed=exposed) self._valueType = str @@ -543,7 +548,7 @@ def validateValue(self, value): if value is None: return value if not isinstance(value, str) or len(value.split(" ")) > 1: - raise ValueError('ColorParam value should be a string containing either an SVG name or an hexadecimal ' - 'color code (param: {}, value: {}, type: {})'.format(self.name, value, type(value))) + raise ValueError(f"ColorParam value should be a string containing either an SVG " + f"name or an hexadecimal color code (param: {self.name}, " + f"value: {value}, type: {type(value)})") return value - diff --git a/meshroom/core/desc/node.py b/meshroom/core/desc/node.py index 5bad8fba7a..7f3148b759 100644 --- a/meshroom/core/desc/node.py +++ b/meshroom/core/desc/node.py @@ -2,7 +2,6 @@ from inspect import getfile from pathlib import Path import logging -import os import psutil import shlex import shutil @@ -150,7 +149,7 @@ def postprocess(self, node): def process(self, node): raise NotImplementedError(f'No process implementation on node: "{node.name}"') - + def processChunk(self, chunk): if self.parallelization is None: self.process(chunk.node) diff --git a/meshroom/core/exception.py b/meshroom/core/exception.py index 4443a8962f..01c5018d8b 100644 --- a/meshroom/core/exception.py +++ b/meshroom/core/exception.py @@ -14,7 +14,7 @@ class GraphException(MeshroomException): class GraphCompatibilityError(GraphException): """ Raised when node compatibility issues occur when loading a graph. - + Args: filepath: The path to the file that caused the error. issues: A dictionnary of node names and their respective compatibility issues. diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index c02329a1d4..204d44696e 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -238,7 +238,7 @@ def fileFeatures(self): def isLoading(self): """ Return True if the graph is currently being loaded. """ return self._loading - + @property def isSaving(self): """ Return True if the graph is currently being saved. """ @@ -310,18 +310,18 @@ def _deserialize(self, graphData: dict): # Create graph edges by resolving attributes expressions self._applyExpr() - - # Templates are specific: they contain only the minimal amount of + + # Templates are specific: they contain only the minimal amount of # serialized data to describe the graph structure. # They are not meant to be computed: therefore, we can early return here, # as uid conflict evaluation is only meaningful for nodes with computed data. if isTemplate: return - # 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. + # 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. self._evaluateUidConflicts(graphContent) def _normalizeGraphContent(self, graphData: dict, fileVersion: Version) -> dict: @@ -358,21 +358,25 @@ def _deserializeNode(self, nodeData: dict, nodeName: str, fromGraph: "Graph"): self._addNode(node, nodeName) return node - def _getNodeTypeVersionFromHeader(self, nodeType: str, default: Optional[str] = None) -> Optional[str]: + def _getNodeTypeVersionFromHeader(self, nodeType: str, + default: Optional[str] = None) -> Optional[str]: nodeVersions = self.header.get(GraphIO.Keys.NodesVersions, {}) return nodeVersions.get(nodeType, default) def _evaluateUidConflicts(self, graphContent: dict): """ - Compare the computed UIDs of all the nodes in the graph with the UIDs serialized in `graphContent`. If there - are mismatches, the nodes with the unexpected UID are replaced with "UidConflict" compatibility nodes. + Compare the computed UIDs of all the nodes in the graph with the UIDs serialized + in `graphContent`. If there are mismatches, the nodes with the unexpected UID are + replaced with "UidConflict" compatibility nodes. Args: graphContent: The serialized Graph content. """ def _serializedNodeUidMatchesComputedUid(nodeData: dict, node: BaseNode) -> bool: - """Returns whether the serialized UID matches the one computed in the `node` instance.""" + """ + Returns whether the serialized UID matches the one computed in the `node` instance. + """ if isinstance(node, CompatibilityNode): return True serializedUid = nodeData.get("uid", None) @@ -390,24 +394,25 @@ def _serializedNodeUidMatchesComputedUid(nodeData: dict, node: BaseNode) -> bool logging.warning("UID Compatibility issues found: recreating conflicting nodes as CompatibilityNodes.") - # A uid conflict is contagious: if a node has a uid conflict, all of its downstream nodes may be - # impacted as well, as the uid flows through connections. - # Therefore, we deal with conflicting uid nodes by depth: replacing a node with a CompatibilityNode restores - # the serialized uid, which might solve "false-positives" downstream conflicts as well. + # A uid conflict is contagious: if a node has a uid conflict, all of its downstream + # nodes may be impacted as well, as the uid flows through connections. + # Therefore, we deal with conflicting uid nodes by depth: replacing a node with a + # CompatibilityNode restores the serialized uid, which might solve "false-positives" + # downstream conflicts as well. nodesSortedByDepth = sorted(uidConflictingNodes, key=lambda node: node.minDepth) for node in nodesSortedByDepth: nodeData = graphContent[node.name] - # Evaluate if the node uid is still conflicting at this point, or if it has been resolved by an - # upstream node replacement. + # Evaluate if the node uid is still conflicting at this point, or if it has been + # resolved by an upstream node replacement. if _serializedNodeUidMatchesComputedUid(nodeData, node): continue expectedUid = node._uid - compatibilityNode = nodeFactory(graphContent[node.name], node.name, expectedUid=expectedUid) + compatibilityNode = nodeFactory(graphContent[node.name], node.name, + expectedUid=expectedUid) # This operation will trigger a graph update that will recompute the uids of all nodes, # allowing the iterative resolution of uid conflicts. self.replaceNode(node.name, compatibilityNode) - def importGraphContentFromFile(self, filepath: PathLike) -> list[Node]: """Import the content (nodes and edges) of another Graph file into this Graph instance. @@ -425,8 +430,8 @@ def importGraphContent(self, graph: "Graph") -> list[Node]: """ Import the content (node and edges) of another `graph` into this Graph instance. - Nodes are imported with their original names if possible, otherwise a new unique name is generated - from their node type. + Nodes are imported with their original names if possible, otherwise a new unique + name is generated from their node type. Args: graph: The graph to import. @@ -509,12 +514,13 @@ def copyNode(self, srcNode, withEdges=False): Returns: Node, dict: the created node instance, - a dictionary of linked attributes with their original value (empty if withEdges is True) + a dictionary of linked attributes with their original value + (empty if withEdges is True) """ with GraphModification(self): # create a new node of the same type and with the same attributes values - # keep links as-is so that CompatibilityNodes attributes can be created with correct automatic description - # (File params for link expressions) + # keep links as-is so that CompatibilityNodes attributes can be created with + # correct automatic description (File params for link expressions) node = nodeFactory(srcNode.toDict(), srcNode.nodeType) # use nodeType as name # skip edges: filter out attributes which are links by resetting default values skippedEdges = {} @@ -597,8 +603,8 @@ def removeNode(self, nodeName): {dstAttr.fullName, srcAttr.fullName} - a dictionary containing the outgoing edges removed by this operation: {dstAttr.fullName, srcAttr.fullName} - - a dictionary containing the values, indices and keys of attributes that were connected to a ListAttribute - prior to the removal of all edges: + - a dictionary containing the values, indices and keys of attributes that were + connected to a ListAttribute prior to the removal of all edges: {dstAttr.fullName, (dstAttr.root.fullName, dstAttr.index, dstAttr.value)} """ node = self.node(nodeName) @@ -609,10 +615,10 @@ def removeNode(self, nodeName): # Remove all edges arriving to and starting from this node with GraphModification(self): # Two iterations over the outgoing edges are necessary: - # - the first one is used to collect all the information about the edges while they are all there - # (overall context) - # - once we have collected all the information, the edges (and perhaps the entries in ListAttributes) can - # actually be removed + # - the first one is used to collect all the information about the edges while they + # are all there (overall context) + # - once we have collected all the information, the edges (and perhaps the entries + # in ListAttributes) can actually be removed for edge in self.nodeOutEdges(node): outEdges[edge.dst.fullName] = edge.src.fullName @@ -625,7 +631,8 @@ def removeNode(self, nodeName): for edge in self.nodeOutEdges(node): self.removeEdge(edge.dst) - # Remove the corresponding attributes from the ListAttributes instead of just emptying their values + # Remove the corresponding attributes from the ListAttributes instead of + # just emptying their values if isinstance(edge.dst.root, ListAttribute): index = edge.dst.root.index(edge.dst) edge.dst.root.remove(index) @@ -648,7 +655,8 @@ def addNewNode( Args: nodeType: the node type name. - name: if specified, the desired name for this node. If not unique, will be prefixed (_N). + name: if specified, the desired name for this node. If not unique, + will be prefixed (_N). position: the position of the node. **kwargs: keyword arguments to initialize the created node's attributes. @@ -664,18 +672,22 @@ def addNewNode( return node def _triggerNodeCreatedCallback(self, nodes: Iterable[Node]): - """Trigger the `onNodeCreated` node descriptor callback for each node instance in `nodes`.""" + """ + Trigger the `onNodeCreated` node descriptor callback for each node instance in `nodes`. + """ with GraphModification(self): for node in nodes: if node.nodeDesc: node.nodeDesc.onNodeCreated(node) def _createUniqueNodeName(self, inputName: str, existingNames: Optional[set[str]] = None): - """Create a unique node name based on the input name. + """ + Create a unique node name based on the input name. Args: inputName: The desired node name. - existingNames: (optional) If specified, consider this set for uniqueness check, instead of the list of nodes. + existingNames: (optional) If specified, consider this set for uniqueness check, + instead of the list of nodes. """ existingNodeNames = existingNames or set(self._nodes.objects.keys()) @@ -701,8 +713,8 @@ def upgradeNode(self, nodeName) -> Node: {dstAttr.fullName, srcAttr.fullName} - a dictionary containing the outgoing edges removed by this operation: {dstAttr.fullName, srcAttr.fullName} - - a dictionary containing the values, indices and keys of attributes that were connected to a ListAttribute - prior to the removal of all edges: + - a dictionary containing the values, indices and keys of attributes that were connected + to a ListAttribute prior to the removal of all edges: {dstAttr.fullName, (dstAttr.root.fullName, dstAttr.index, dstAttr.value)} """ node = self.node(nodeName) @@ -714,7 +726,8 @@ def upgradeNode(self, nodeName) -> Node: @changeTopology def replaceNode(self, nodeName: str, newNode: BaseNode): - """Replace the node idenfitied by `nodeName` with `newNode`, while restoring compatible edges. + """ + Replace the node idenfitied by `nodeName` with `newNode`, while restoring compatible edges. Args: nodeName: The name of the Node to replace. @@ -724,15 +737,15 @@ def replaceNode(self, nodeName: str, newNode: BaseNode): _, outEdges, outListAttributes = self.removeNode(nodeName) self.addNode(newNode, nodeName) self._restoreOutEdges(outEdges, outListAttributes) - + def _restoreOutEdges(self, outEdges: dict[str, str], outListAttributes): """Restore output edges that were removed during a call to "removeNode". - + Args: outEdges: a dictionary containing the outgoing edges removed by a call to "removeNode". {dstAttr.fullName, srcAttr.fullName} - outListAttributes: a dictionary containing the values, indices and keys of attributes that were connected - to a ListAttribute prior to the removal of all edges. + outListAttributes: a dictionary containing the values, indices and keys of attributes + that were connected to a ListAttribute prior to the removal of all edges. {dstAttr.fullName, (dstAttr.root.fullName, dstAttr.index, dstAttr.value)} """ def _recreateTargetListAttributeChildren(listAttrName: str, index: int, value: Any): @@ -745,14 +758,17 @@ def _recreateTargetListAttributeChildren(listAttrName: str, index: int, value: A listAttr.insert(index, value) for dstName, srcName in outEdges.items(): - # Re-create the entries in ListAttributes that were completely removed during the call to "removeNode" + # Re-create the entries in ListAttributes that were completely removed during the call + # to "removeNode" if dstName in outListAttributes: _recreateTargetListAttributeChildren(*outListAttributes[dstName]) try: srcAttr = self.attribute(srcName) dstAttr = self.attribute(dstName) if srcAttr is None or dstAttr is None: - logging.warning(f"Failed to restore edge {srcName}{' (missing)' if srcAttr is None else ''} -> {dstName}{' (missing)' if dstAttr is None else ''}") + logging.warning(f"Failed to restore edge {srcName}" + f"{' (missing)' if srcAttr is None else ''} -> " + f"{dstName}{' (missing)' if dstAttr is None else ''}") continue self.addEdge(srcAttr, dstAttr) except (KeyError, ValueError) as e: @@ -767,9 +783,9 @@ def upgradeAllNodes(self): def reloadNodePlugins(self, nodeTypes: list[str]): """ - Replace all the node instances of "nodeTypes" in the current graph with new node instances of the - same type. If the description of the nodes has changed, the reloaded nodes will reflect theses - changes. If "nodeTypes" is empty, then the function returns immediately. + Replace all the node instances of "nodeTypes" in the current graph with new node instances + of the same type. If the description of the nodes has changed, the reloaded nodes will + reflect theses changes. If "nodeTypes" is empty, then the function returns immediately. Args: nodeTypes: the list of node types that will be reloaded. @@ -846,7 +862,8 @@ def nodesOfType(self, nodeType, sortedByIndex=True): Args: nodeType (str): the node type name to consider. - sortedByIndex (bool): whether to sort the nodes by their index (see Graph.sortNodesByIndex) + sortedByIndex (bool): whether to sort the nodes by their index + (see Graph.sortNodesByIndex) Returns: list[Node]: the list of nodes matching the given nodeType. """ @@ -858,7 +875,8 @@ def findInitNodes(self): Returns: list[Node]: the list of Init nodes (nodes inheriting from InitNode) """ - nodes = [n for n in self._nodes.values() if isinstance(n.nodeDesc, meshroom.core.desc.InitNode)] + nodes = [n for n in self._nodes.values() + if isinstance(n.nodeDesc, meshroom.core.desc.InitNode)] return nodes def findNodeCandidates(self, nodeNameExpr: str) -> list[Node]: @@ -873,7 +891,8 @@ def findNode(self, nodeExpr: str) -> Node: for c in candidates: if c.name == nodeExpr: return c - raise KeyError(f'Multiple node candidates for "{nodeExpr}": {str([c.name for c in candidates])}') + raise KeyError(f'Multiple node candidates for "{nodeExpr}": ' + f'{str([c.name for c in candidates])}') return candidates[0] def findNodes(self, nodesExpr): @@ -929,7 +948,8 @@ def getDepth(self, node, minimal=False): Args: node (Node): the node to consider. - minimal (bool): whether to return the minimal depth instead of the maximal one (default). + minimal (bool): whether to return the minimal depth instead of the maximal + one (default). Returns: int: the node's depth in this Graph. """ @@ -939,7 +959,8 @@ def getDepth(self, node, minimal=False): return minDepth if minimal else maxDepth def getInputEdges(self, node, dependenciesOnly): - return {edge for edge in self.getEdges(dependenciesOnly=dependenciesOnly) if edge.dst.node is node} + return {edge for edge in self.getEdges(dependenciesOnly=dependenciesOnly) + if edge.dst.node is node} def _getInputEdgesPerNode(self, dependenciesOnly): nodeEdges = defaultdict(set) @@ -970,8 +991,8 @@ def dfs(self, visitor, startNodes=None, longestPathFirst=False): if longestPathFirst and visitor.reverse: # Because we have no knowledge of the node's count between a node and its leaves, # it is not possible to handle this case at the moment - raise NotImplementedError("Graph.dfs(): longestPathFirst=True and visitor.reverse=True are not " - "compatible yet.") + raise NotImplementedError("Graph.dfs(): longestPathFirst=True and visitor.reverse=True " + "are not compatible yet.") nodes = startNodes or (self.getRootNodes(visitor.dependenciesOnly) if visitor.reverse else self.getLeafNodes(visitor.dependenciesOnly)) @@ -1000,13 +1021,15 @@ def _dfsVisit(self, u, visitor, colors, nodeChildren, longestPathFirst): children = nodeChildren[u] if longestPathFirst: assert not self.dirtyTopology - children = sorted(children, reverse=True, key=lambda item: self._nodesMinMaxDepths[item][1]) + children = sorted(children, reverse=True, + key=lambda item: self._nodesMinMaxDepths[item][1]) for v in children: visitor.examineEdge((u, v), self) if colors[v] == WHITE: visitor.treeEdge((u, v), self) # (u,v) is a tree edge - self.dfsVisit(v, visitor, colors, nodeChildren, longestPathFirst) # TODO: avoid recursion + # TODO: avoid recursion + self.dfsVisit(v, visitor, colors, nodeChildren, longestPathFirst) elif colors[v] == GRAY: # (u,v) is a back edge visitor.backEdge((u, v), self) @@ -1017,7 +1040,8 @@ def _dfsVisit(self, u, visitor, colors, nodeChildren, longestPathFirst): colors[u] = BLACK visitor.finishVertex(u, self) - def dfsOnFinish(self, startNodes=None, longestPathFirst=False, reverse=False, dependenciesOnly=False): + def dfsOnFinish(self, startNodes=None, longestPathFirst=False, reverse=False, + dependenciesOnly=False): """ Return the node chain from startNodes to the graph roots/leaves. Order is defined by the visit and finishVertex event. @@ -1040,7 +1064,8 @@ def dfsOnFinish(self, startNodes=None, longestPathFirst=False, reverse=False, de self.dfs(visitor=visitor, startNodes=startNodes, longestPathFirst=longestPathFirst) return nodes, edges - def dfsOnDiscover(self, startNodes=None, filterTypes=None, longestPathFirst=False, reverse=False, dependenciesOnly=False): + def dfsOnDiscover(self, startNodes=None, filterTypes=None, longestPathFirst=False, + reverse=False, dependenciesOnly=False): """ Return the node chain from startNodes to the graph roots/leaves. Order is defined by the visit and discoverVertex event. @@ -1163,7 +1188,8 @@ def finishEdge(edge, graph): depthMin = inputDepths[0] + 1 else: depthMin = min(currentDepths[0], inputDepths[0] + 1) - self._nodesMinMaxDepths[currentVertex] = (depthMin, max(currentDepths[1], inputDepths[1] + 1)) + self._nodesMinMaxDepths[currentVertex] = (depthMin, max(currentDepths[1], + inputDepths[1] + 1)) # update computability if currentVertex.hasStatus(Status.SUCCESS): @@ -1218,8 +1244,8 @@ def finishVertex(vertex, graph): def flowEdges(self, startNodes=None, dependenciesOnly=True): """ - Return as few edges as possible, such that if there is a directed path from one vertex to another in the - original graph, there is also such a path in the reduction. + Return as few edges as possible, such that if there is a directed path from one vertex + to another in the original graph, there is also such a path in the reduction. :param startNodes: :return: the remaining edges after a transitive reduction of the graph. @@ -1252,24 +1278,27 @@ def getEdges(self, dependenciesOnly=False): def getInputNodes(self, node, recursive, dependenciesOnly): """ Return either the first level input nodes of a node or the whole chain. """ if not recursive: - return {edge.src.node for edge in self.getEdges(dependenciesOnly) if edge.dst.node is node} + return {edge.src.node for edge in self.getEdges(dependenciesOnly) + if edge.dst.node is node} - inputNodes, edges = self.dfsOnDiscover(startNodes=[node], filterTypes=None, reverse=False) + inputNodes, _ = self.dfsOnDiscover(startNodes=[node], filterTypes=None, reverse=False) return inputNodes[1:] # exclude current node def getOutputNodes(self, node, recursive, dependenciesOnly): """ Return either the first level output nodes of a node or the whole chain. """ if not recursive: - return {edge.dst.node for edge in self.getEdges(dependenciesOnly) if edge.src.node is node} + return {edge.dst.node for edge in self.getEdges(dependenciesOnly) + if edge.src.node is node} - outputNodes, edges = self.dfsOnDiscover(startNodes=[node], filterTypes=None, reverse=True) + outputNodes, _ = self.dfsOnDiscover(startNodes=[node], filterTypes=None, reverse=True) return outputNodes[1:] # exclude current node @Slot(Node, result=int) def canSubmitOrCompute(self, startNode): """ Check if a node can be submitted/computed. - It does not depend on the topology of the graph and is based on the node status and its dependencies. + It does not depend on the topology of the graph and is based on the node status and its + dependencies. Returns: int: 0 = cannot be submitted or computed / @@ -1411,7 +1440,8 @@ def _setFilepath(self, filepath): # * cache folder is located next to the graph file # * graph name if the basename of the graph file self.name = os.path.splitext(os.path.basename(filepath))[0] - self.cacheDir = os.path.join(os.path.abspath(os.path.dirname(filepath)), meshroom.core.cacheFolderName) + self.cacheDir = os.path.join(os.path.abspath(os.path.dirname(filepath)), + meshroom.core.cacheFolderName) self.filepathChanged.emit() def _unsetFilepath(self): @@ -1421,7 +1451,7 @@ def _unsetFilepath(self): self.filepathChanged.emit() def updateInternals(self, startNodes=None, force=False): - nodes, edges = self.dfsOnFinish(startNodes=startNodes) + nodes, _ = self.dfsOnFinish(startNodes=startNodes) for node in nodes: if node.dirty or force: node.updateInternals() @@ -1487,7 +1517,7 @@ def markNodesDirty(self, fromNode): See Also: Graph.update, Graph.updateInternals, Graph.updateStatusFromCache """ - nodes, edges = self.dfsOnDiscover(startNodes=[fromNode], reverse=True) + nodes, _ = self.dfsOnDiscover(startNodes=[fromNode], reverse=True) for node in nodes: node.dirty = True @@ -1529,7 +1559,9 @@ def getChunksByStatus(self, status): return chunks def getChunks(self, nodes=None): - """ Returns the list of NodeChunks for the given list of nodes (for all nodes if nodes is None) """ + """ + Returns the list of NodeChunks for the given list of nodes (for all nodes if nodes is None) + """ chunks = [] for node in nodes or self.nodes: chunks += [chunk for chunk in node.chunks] @@ -1591,14 +1623,17 @@ def setVerbose(self, v): filepathChanged = Signal() filepath = Property(str, lambda self: self._filepath, notify=filepathChanged) isSaving = Property(bool, isSaving.fget, constant=True) - fileReleaseVersion = Property(str, lambda self: self.header.get(GraphIO.Keys.ReleaseVersion, "0.0"), + fileReleaseVersion = Property(str, + lambda self: self.header.get(GraphIO.Keys.ReleaseVersion, "0.0"), notify=filepathChanged) - fileDateVersion = Property(float, fileDateVersion.fget, fileDateVersion.fset, notify=filepathChanged) + fileDateVersion = Property(float, fileDateVersion.fget, fileDateVersion.fset, + notify=filepathChanged) cacheDirChanged = Signal() cacheDir = Property(str, cacheDir.fget, cacheDir.fset, notify=cacheDirChanged) updated = Signal() canComputeLeavesChanged = Signal() - canComputeLeaves = Property(bool, lambda self: self._canComputeLeaves, notify=canComputeLeavesChanged) + canComputeLeaves = Property(bool, lambda self: self._canComputeLeaves, + notify=canComputeLeavesChanged) def loadGraph(filepath, strictCompatibility: bool = False) -> Graph: @@ -1607,20 +1642,23 @@ def loadGraph(filepath, strictCompatibility: bool = False) -> Graph: Args: filepath: The path to the Meshroom Graph file. - strictCompatibility: If True, raise a GraphCompatibilityError if the loaded Graph has node compatibility issues. + strictCompatibility: If True, raise a GraphCompatibilityError if the loaded Graph + has node compatibility issues. Returns: Graph: The loaded Graph instance. Raises: - GraphCompatibilityError: If the Graph has node compatibility issues and `strictCompatibility` is True. + GraphCompatibilityError: If the Graph has node compatibility issues and + `strictCompatibility` is True. """ graph = Graph("") graph.load(filepath) compatibilityIssues = len(graph.compatibilityNodes) > 0 if compatibilityIssues and strictCompatibility: - raise GraphCompatibilityError(filepath, {n.name: str(n.issue) for n in graph.compatibilityNodes}) + raise GraphCompatibilityError(filepath, + {n.name: str(n.issue) for n in graph.compatibilityNodes}) graph.update() return graph @@ -1639,9 +1677,9 @@ def executeGraph(graph, toNodes=None, forceCompute=False, forceStatus=False): """ """ if forceCompute: - nodes, edges = graph.dfsOnFinish(startNodes=toNodes) + nodes, _ = graph.dfsOnFinish(startNodes=toNodes) else: - nodes, edges = graph.dfsToProcess(startNodes=toNodes) + nodes, _ = graph.dfsToProcess(startNodes=toNodes) chunksInConflict = getAlreadySubmittedChunks(nodes) if chunksInConflict: diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 10b9721eb1..ca0eb942e2 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -172,7 +172,7 @@ def initLocalSubmit(self): def initEndCompute(self): self.sessionUid = meshroom.core.sessionUid self.endDateTime = datetime.datetime.now().strftime(self.dateTimeFormatting) - if self._startTime != None: + if self._startTime is not None: self.elapsedTime = time.time() - self._startTime @property @@ -359,7 +359,7 @@ def name(self): return f"{self.node.name}({self.index})" else: return self.node.name - + @property def logManager(self): if self._logManager is None: @@ -394,11 +394,13 @@ def updateStatusFromCache(self): try: with open(statusFile) as jsonFile: statusData = json.load(jsonFile) - # logging.debug(f"updateStatusFromCache({self.node.name}): From status {self.status.status} to {statusData['status']}") + logging.debug(f"updateStatusFromCache({self.node.name}): From status " + f"{self.status.status} to {statusData['status']}") self._status.fromDict(statusData) self.statusFileLastModTime = os.path.getmtime(statusFile) except Exception as e: - logging.debug(f"updateStatusFromCache({self.node.name}): Error while loading status file {statusFile}: {e}") + logging.debug(f"updateStatusFromCache({self.node.name}): " + f"Error while loading status file {statusFile}: {e}") self.statusFileLastModTime = -1 self._status.reset() self._status.setNodeType(self.node) @@ -453,7 +455,8 @@ def upgradeStatusFile(self): def upgradeStatusTo(self, newStatus, execMode=None): if newStatus.value < self._status.status.value: - logging.warning(f"Downgrade status on node '{self.name}' from {self._status.status} to {newStatus}") + logging.warning(f"Downgrade status on node '{self.name}' " + f"from {self._status.status} to {newStatus}") if execMode is not None: self._status.execMode = execMode @@ -521,7 +524,7 @@ def process(self, forceCompute=False, inCurrentEnv=False): executionStatus = None self.statThread = stats.StatisticsThread(self) self.statThread.start() - + try: self.node.nodeDesc.processChunk(self) # NOTE: this assumes saving the output attributes for each chunk @@ -549,7 +552,6 @@ def process(self, forceCompute=False, inCurrentEnv=False): self.statistics = stats.Statistics() del runningProcesses[self.name] - def _processInIsolatedEnvironment(self): """ Process this node chunk in the isolated environment defined in the environment @@ -594,7 +596,8 @@ def stopProcess(self): # Nothing to do, the computation is already stopped. pass else: - logging.debug(f"Cannot stop process: node is not running (status is: {self._status.status}).") + logging.debug(f"Cannot stop process: node is not running (status is: " + f"{self._status.status}).") return self.node.nodeDesc.stopProcess(self) @@ -613,7 +616,8 @@ def isExtern(self): return True elif self._status.execMode == ExecMode.LOCAL: if self._status.status in (Status.SUBMITTED, Status.RUNNING): - return meshroom.core.sessionUid not in (self._status.submitterSessionUid, self._status.sessionUid) + return meshroom.core.sessionUid not in (self._status.submitterSessionUid, + self._status.sessionUid) return False return False @@ -737,7 +741,7 @@ def getNodeLogLevel(self): if self.hasInternalAttribute("nodeDefaultLogLevel"): return self.internalAttribute("nodeDefaultLogLevel").value.strip() return "info" - + def getColor(self): """ Returns: @@ -781,7 +785,7 @@ def getDocumentation(self): return self.nodeDesc.documentation else: return self.nodeDesc.__doc__ - + def getNodeInfos(self): if not self.nodeDesc: return [] @@ -808,7 +812,7 @@ def getNodeInfos(self): for key, value in additionalNodeInfos: infos[key] = value return [{"key": k, "value": v} for k, v in infos.items()] - + @property def packageFullName(self): return '-'.join([self.packageName, self.packageVersion]) @@ -970,10 +974,12 @@ def _buildAttributeCmdVars(cmdVars, name, attr): # xxValue is exposed without quotes to allow to compose expressions 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 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. + # But a List of string containing 1 element, 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] elif isinstance(attr, GroupAttribute): @@ -1027,13 +1033,13 @@ def _buildAttributeCmdVars(cmdVars, name, attr): except KeyError as e: logging.warning('Invalid expression with missing key on "{nodeName}.{attrName}" with ' 'value "{defaultValue}".\nError: {err}'. - format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, - err=str(e))) + format(nodeName=self.name, attrName=attr.name, + defaultValue=defaultValue, err=str(e))) except ValueError as e: logging.warning('Invalid expression value on "{nodeName}.{attrName}" with value ' '"{defaultValue}".\nError: {err}'. - format(nodeName=self.name, attrName=attr.name, defaultValue=defaultValue, - err=str(e))) + format(nodeName=self.name, attrName=attr.name, + defaultValue=defaultValue, err=str(e))) v = attr.getValueStr(withQuotes=True) @@ -1161,7 +1167,7 @@ def isExtern(self): @Slot() def clearSubmittedChunks(self): """ - Reset all submitted chunks to Status.NONE. This method should be used to clear + 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: @@ -1314,7 +1320,7 @@ def updateStatusFromCache(self): """ Update node status based on status file content/existence. """ - s = self.globalStatus + # s = self.globalStatus for chunk in self._chunks: chunk.updateStatusFromCache() # logging.warning(f"updateStatusFromCache: {self.name}, status: {s} => {self.globalStatus}") @@ -1456,7 +1462,7 @@ def getGlobalStatus(self): return Status.INPUT if not self._chunks: return Status.NONE - if len( self._chunks) == 1: + if len(self._chunks) == 1: return self._chunks[0].status.status chunksStatus = [chunk.status.status for chunk in self._chunks] @@ -1637,12 +1643,16 @@ def initFromThisSession(self) -> bool: if len(self._chunks) == 0: return False for chunk in self._chunks: - if meshroom.core.sessionUid not in (chunk.status.sessionUid, chunk.status.submitterSessionUid): + if meshroom.core.sessionUid not in (chunk.status.sessionUid, + chunk.status.submitterSessionUid): return False return True def isMainNode(self) -> bool: - """ In case of a node with duplicates, we check that the node is the one driving the computation. """ + """ + In case of a node with duplicates, we check that the node is the one driving the + computation. + """ if len(self._chunks) == 0: return True firstChunk = self._chunks.at(0) @@ -1706,8 +1716,8 @@ def has3DOutputAttribute(self): Return True if at least one attribute is a File that can be loaded in the 3D Viewer, False otherwise. """ - - return next((attr for attr in self._attributes if attr.enabled and attr.isOutput and attr.is3dDisplayable), None) is not None + return next((attr for attr in self._attributes if attr.enabled and + attr.isOutput and attr.is3dDisplayable), None) is not None name = Property(str, getName, constant=True) defaultLabel = Property(str, getDefaultLabel, constant=True) @@ -1736,9 +1746,11 @@ def has3DOutputAttribute(self): sizeChanged = Signal() size = Property(int, getSize, notify=sizeChanged) globalStatusChanged = Signal() - globalStatus = Property(str, lambda self: self.getGlobalStatus().name, notify=globalStatusChanged) + globalStatus = Property(str, lambda self: self.getGlobalStatus().name, + notify=globalStatusChanged) fusedStatus = Property(StatusData, getFusedStatus, notify=globalStatusChanged) - elapsedTime = Property(float, lambda self: self.getFusedStatus().elapsedTime, notify=globalStatusChanged) + elapsedTime = Property(float, lambda self: self.getFusedStatus().elapsedTime, + notify=globalStatusChanged) recursiveElapsedTime = Property(float, lambda self: self.getRecursiveFusedStatus().elapsedTime, notify=globalStatusChanged) # isCompatibilityNode: need lambda to evaluate the virtual function @@ -1848,8 +1860,10 @@ def upgradeInternalAttributeValues(self, values): pass def toDict(self): - inputs = {k: v.getSerializedValue() for k, v in self._attributes.objects.items() if v.isInput} - internalInputs = {k: v.getSerializedValue() for k, v in self._internalAttributes.objects.items()} + inputs = {k: v.getSerializedValue() for k, v in self._attributes.objects.items() + if v.isInput} + internalInputs = {k: v.getSerializedValue() for k, v + in self._internalAttributes.objects.items()} outputs = ({k: v.getSerializedValue() for k, v in self._attributes.objects.items() if v.isOutput and not v.desc.isDynamicValue}) @@ -1908,11 +1922,13 @@ class CompatibilityIssue(Enum): class CompatibilityNode(BaseNode): """ - Fallback BaseNode subclass to instantiate Nodes having compatibility issues with current type description. + Fallback BaseNode subclass to instantiate Nodes having compatibility issues with current type + description. CompatibilityNode creates an 'empty-shell' exposing the deserialized node as-is, with all its inputs and precomputed outputs. """ - def __init__(self, nodeType, nodeDict, position=None, issue=CompatibilityIssue.UnknownIssue, parent=None): + def __init__(self, nodeType, nodeDict, position=None, issue=CompatibilityIssue.UnknownIssue, + parent=None): super().__init__(nodeType, position, parent) self.issue = issue @@ -2029,10 +2045,12 @@ def attributeDescFromName(refAttributes, name, value, strict=True): refAttributes ([desc.Attribute]): reference Attributes to look for a description name (str): attribute's name value: attribute's value - strict: strict test for the match (for instance, regarding a group with some parameter changes) + strict: strict test for the match (for instance, regarding a group with some + parameter changes) Returns: - desc.Attribute: an attribute description from refAttributes if a match is found, None otherwise. + desc.Attribute: an attribute description from refAttributes if a match is found, + None otherwise. """ # from original node description based on attribute's name attrDesc = next((d for d in refAttributes if d.name == name), None) @@ -2147,7 +2165,8 @@ def upgrade(self): commonInternalAttributes = [] for attrName, value in self._internalInputs.items(): - if self.attributeDescFromName(self.nodeDesc.internalInputs, attrName, value, strict=False): + if self.attributeDescFromName(self.nodeDesc.internalInputs, attrName, value, + strict=False): # store internal attributes that could be used during node upgrade commonInternalAttributes.append(attrName) @@ -2177,4 +2196,3 @@ def upgrade(self): compatibilityIssue = Property(int, lambda self: self.issue.value, constant=True) canUpgrade = Property(bool, canUpgrade.fget, constant=True) issueDetails = Property(str, issueDetails.fget, constant=True) - diff --git a/meshroom/core/plugins.py b/meshroom/core/plugins.py index 1e94b5bf54..06a8b68ada 100644 --- a/meshroom/core/plugins.py +++ b/meshroom/core/plugins.py @@ -63,8 +63,8 @@ class ProcessEnv(BaseObject): Args: folder: the source folder for the process. - configEnv: the dictionary containing the environment variables defined in a configuration file - for the process to run. + configEnv: the dictionary containing the environment variables defined in a configuration + file for the process to run. envType: (optional) the type of process environment. uri: (optional) the Unique Resource Identifier to activate the environment. """ @@ -112,7 +112,7 @@ def __init__(self, folder: str, configEnv: dict[str: str]): self.libPaths: list = [str(Path(folder, "lib")), str(Path(folder, "lib64")), str(Path(venvFolder, "lib")), str(Path(venvFolder, "lib64"))] self.pythonPaths: list = [str(Path(folder)), str(Path(venvFolder))] + \ - self.binPaths + envLibPaths + venvLibPaths + self.binPaths + envLibPaths + venvLibPaths if sys.platform == "win32": # For Windows platforms, try and include the content of the virtual env if it exists @@ -151,9 +151,9 @@ def __init__(self, folder: str, configEnv: dict[str: str]): class RezProcessEnv(ProcessEnv): """ """ - + REZ_DELIMITER_PATTERN = re.compile(r"-|==|>=|>|<=|<") - + def __init__(self, folder: str, configEnv: dict[str: str], uri: str = ""): if not uri: raise RuntimeError("Missing name of the Rez environment needs to be provided.") @@ -406,7 +406,8 @@ def loadConfig(self): except IOError as err: logging.error(f"Error while accessing the configuration file for {self.name}: {err}") - # If both dictionaries have identical keys, os.environ overwrites existing values from _configEnv + # If both dictionaries have identical keys, os.environ overwrites existing values + # from _configEnv self._configFullEnv = self._configEnv | os.environ def containsNodePlugin(self, name: str) -> bool: @@ -456,12 +457,13 @@ def __init__(self, nodeDesc: desc.Node, plugin: Plugin = None): def reload(self) -> bool: """ - Reload the node plugin and update its status accordingly. If the timestamp of the node plugin's - path has not changed since the last time the plugin has been loaded, then nothing will happen. + Reload the node plugin and update its status accordingly. If the timestamp of the node + plugin's path has not changed since the last time the plugin has been loaded, then nothing + will happen. Returns: - bool: True if the node plugin has successfully been reloaded (i.e. there was no error, and - some changes were made since its last loading), False otherwise. + bool: True if the node plugin has successfully been reloaded (i.e. there was no error, + and some changes were made since its last loading), False otherwise. """ timestamp = 0.0 try: @@ -548,6 +550,7 @@ def configFullEnv(self) -> dict[str: str]: return self.plugin.configFullEnv return {} + class NodePluginManager(BaseObject): """ Manager for all the loaded Plugin objects as well as the registered NodePlugin objects. @@ -633,10 +636,10 @@ def removePlugin(self, plugin: Plugin, unregisterNodePlugins: bool = True): Args: plugin: the Plugin to remove from the list of loaded plugins. - unregisterNodePlugins: True if all the nodes from the plugin should be unregistered (if they - are registered) at the same time as the plugin is unloaded. Otherwise, - the registered NodePlugins will remain while the Plugin itself will - be unloaded. + unregisterNodePlugins: True if all the nodes from the plugin should be unregistered + (if they are registered) at the same time as the plugin is + unloaded. Otherwise, the registered NodePlugins will remain + while the Plugin itself will be unloaded. """ if self.getPlugin(plugin.name): if unregisterNodePlugins: diff --git a/meshroom/core/test.py b/meshroom/core/test.py index bf783a2113..025e78b480 100644 --- a/meshroom/core/test.py +++ b/meshroom/core/test.py @@ -8,8 +8,12 @@ import json + def checkTemplateVersions(path: str, nodesAlreadyLoaded: bool = False) -> bool: - """ Check whether there is a compatibility issue with the nodes saved in the template provided with "path". """ + """ + Check whether there is a compatibility issue with the nodes saved in the template provided + with "path". + """ if not nodesAlreadyLoaded: meshroom.core.initNodes() diff --git a/meshroom/core/utils.py b/meshroom/core/utils.py index 99b466810f..0d2f0b6ca0 100644 --- a/meshroom/core/utils.py +++ b/meshroom/core/utils.py @@ -1,15 +1,18 @@ -COLORSPACES = ["AUTO", "sRGB", "rec709", "Linear", "ACES2065-1", "ACEScg", "Linear ARRI Wide Gamut 3", - "ARRI LogC3 (EI800)", "Linear ARRI Wide Gamut 4", "ARRI LogC4", "Linear BMD WideGamut Gen5", - "BMDFilm WideGamut Gen5", "CanonLog2 CinemaGamut D55", "CanonLog3 CinemaGamut D55", - "Linear CinemaGamut D55", "Linear V-Gamut", "V-Log V-Gamut", "Linear REDWideGamutRGB", - "Log3G10 REDWideGamutRGB", "Linear Venice S-Gamut3.Cine", "S-Log3 Venice S-Gamut3.Cine", "no_conversion"] +COLORSPACES = ["AUTO", "sRGB", "rec709", "Linear", "ACES2065-1", "ACEScg", + "Linear ARRI Wide Gamut 3", "ARRI LogC3 (EI800)", + "Linear ARRI Wide Gamut 4", "ARRI LogC4", "Linear BMD WideGamut Gen5", + "BMDFilm WideGamut Gen5", "CanonLog2 CinemaGamut D55", + "CanonLog3 CinemaGamut D55", "Linear CinemaGamut D55", "Linear V-Gamut", + "V-Log V-Gamut", "Linear REDWideGamutRGB", "Log3G10 REDWideGamutRGB", + "Linear Venice S-Gamut3.Cine", "S-Log3 Venice S-Gamut3.Cine", "no_conversion"] -DESCRIBER_TYPES = ["sift", "sift_float", "sift_upright", "dspsift", "akaze", "akaze_liop", "akaze_mldb", "cctag3", - "cctag4", "sift_ocv", "akaze_ocv", "tag16h5", "survey", "unknown"] +DESCRIBER_TYPES = ["sift", "sift_float", "sift_upright", "dspsift", "akaze", "akaze_liop", + "akaze_mldb", "cctag3", "cctag4", "sift_ocv", "akaze_ocv", "tag16h5", + "survey", "unknown"] EXR_STORAGE_DATA_TYPE = ["float", "half", "halfFinite", "auto"] -RAW_COLOR_INTERPRETATION = ["None", "LibRawNoWhiteBalancing", "LibRawWhiteBalancing", "DCPLinearProcessing", - "DCPMetadata", "Auto"] +RAW_COLOR_INTERPRETATION = ["None", "LibRawNoWhiteBalancing", "LibRawWhiteBalancing", + "DCPLinearProcessing", "DCPMetadata", "Auto"] VERBOSE_LEVEL = ["fatal", "error", "warning", "info", "debug", "trace"] diff --git a/meshroom/env.py b/meshroom/env.py index 56ff893f9b..b611f4dd72 100644 --- a/meshroom/env.py +++ b/meshroom/env.py @@ -17,6 +17,7 @@ meshroomFolder = os.path.dirname(__file__) + @dataclass class VarDefinition: """Environment variable definition.""" diff --git a/meshroom/submitters/simpleFarmSubmitter.py b/meshroom/submitters/simpleFarmSubmitter.py index b522b076e2..18b84ffa51 100644 --- a/meshroom/submitters/simpleFarmSubmitter.py +++ b/meshroom/submitters/simpleFarmSubmitter.py @@ -7,7 +7,6 @@ import re import simpleFarm -from meshroom.core.desc import Level from meshroom.core.submitter import BaseSubmitter currentDir = os.path.dirname(os.path.realpath(__file__)) diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 325c5df406..379f0d501a 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -6,7 +6,7 @@ from PySide6 import __version__ as PySideVersion from PySide6 import QtCore -from PySide6.QtCore import QObject, QUrl, QJsonValue, qInstallMessageHandler, QtMsgType, QSettings +from PySide6.QtCore import QUrl, QJsonValue, qInstallMessageHandler, QtMsgType, QSettings from PySide6.QtGui import QIcon from PySide6.QtQml import QQmlDebuggingEnabler from PySide6.QtQuickControls2 import QQuickStyle @@ -76,6 +76,7 @@ def handler(cls, messageType, context, message): return MessageHandler.logFunctions[messageType](message) + def createMeshroomParser(args): # Create the main parser with a description @@ -239,9 +240,9 @@ def __init__(self, inputArgs): # Initialize the list of recent project files self._recentProjectFiles = self._getRecentProjectFilesFromSettings() - # Flag set to True if, for all the project files in the list, thumbnails have been retrieved when they - # are available. If set to False, then all the paths in the list are accurate, but some thumbnails might - # be retrievable + # Flag set to True if, for all the project files in the list, thumbnails have been + # retrieved when they are available. If set to False, then all the paths in the list + # are accurate, but some thumbnails might be retrievable self._updatedRecentProjectFilesThumbnails = True # Register components for QML before instantiating the engine @@ -267,7 +268,9 @@ def __init__(self, inputArgs): # instantiate Reconstruction object self._undoStack = commands.UndoStack(self) self._taskManager = TaskManager(self) - self._activeProject = Reconstruction(undoStack=self._undoStack, taskManager=self._taskManager, defaultPipeline=args.pipeline, parent=self) + self._activeProject = Reconstruction(undoStack=self._undoStack, + taskManager=self._taskManager, + defaultPipeline=args.pipeline, parent=self) self._activeProject.setSubmitLabel(args.submitLabel) self.engine.rootContext().setContextProperty("_reconstruction", self._activeProject) @@ -277,15 +280,18 @@ def __init__(self, inputArgs): # => expose them as context properties instead self.engine.rootContext().setContextProperty("Filepath", FilepathHelper(parent=self)) self.engine.rootContext().setContextProperty("Scene3DHelper", Scene3DHelper(parent=self)) - self.engine.rootContext().setContextProperty("Transformations3DHelper", Transformations3DHelper(parent=self)) + self.engine.rootContext().setContextProperty("Transformations3DHelper", + Transformations3DHelper(parent=self)) self.engine.rootContext().setContextProperty("Clipboard", ClipboardHelper(parent=self)) self.engine.rootContext().setContextProperty("ThumbnailCache", ThumbnailCache(parent=self)) # additional context properties self._messageController = MessageController(parent=self) self.engine.rootContext().setContextProperty("_messageController", self._messageController) - self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self)) - self.engine.rootContext().setContextProperty("ScriptEditorManager", ScriptEditorManager(parent=self)) + self.engine.rootContext().setContextProperty("_PaletteManager", + PaletteManager(self.engine, parent=self)) + self.engine.rootContext().setContextProperty("ScriptEditorManager", + ScriptEditorManager(parent=self)) self.engine.rootContext().setContextProperty("MeshroomApp", self) # request any potential computation to stop on exit @@ -356,21 +362,23 @@ def _pipelineTemplateNames(self): def reloadTemplateList(self): meshroom.core.initPipelines() self.pipelineTemplateFilesChanged.emit() - + @Slot() def forceUIUpdate(self): - """ Force UI to process pending events - Necessary when we want to update the UI while a trigger is still running (e.g. reloadPlugins) + """ + Force UI to process pending events. + Necessary when we want to update the UI while a trigger is still running + (e.g. reloadPlugins). """ self.processEvents() - + def showMessage(self, message, status=None, duration=5000): self._messageController.sendMessage(message, status, duration) def _retrieveThumbnailPath(self, filepath: str) -> str: """ - Given the path of a project file, load this file and try to retrieve the path to its thumbnail, i.e. its - first viewpoint image. + Given the path of a project file, load this file and try to retrieve the path + to its thumbnail, i.e. its first viewpoint image. Args: filepath: the path of the project file to retrieve the thumbnail from @@ -402,12 +410,13 @@ def _retrieveThumbnailPath(self, filepath: str) -> str: def _getRecentProjectFilesFromSettings(self) -> list[dict[str, str]]: """ - Read the list of recent project files from the QSettings, retrieve their filepath, and if it exists, their - thumbnail. + Read the list of recent project files from the QSettings, retrieve their filepath, + and if it exists, their thumbnail. Returns: - The list containing dictionaries of the form {"path": "/path/to/project/file", "thumbnail": - "/path/to/thumbnail"} based on the recent projects stored in the QSettings. + The list containing dictionaries of the form {"path": "/path/to/project/file", + "thumbnail": "/path/to/thumbnail"} based on the recent projects stored in the + QSettings. """ projects = [] settings = QSettings() @@ -426,9 +435,9 @@ def _getRecentProjectFilesFromSettings(self) -> list[dict[str, str]]: @Slot() def updateRecentProjectFilesThumbnails(self) -> None: """ - If there are thumbnails that may be retrievable (meaning the list of projects has been updated minimally), - update the list of recent project files by reading the QSettings and retrieving the thumbnails if they are - available. + If there are thumbnails that may be retrievable (meaning the list of projects has been + updated minimally), update the list of recent project files by reading the QSettings and + retrieving the thumbnails if they are available. """ if not self._updatedRecentProjectFilesThumbnails: self._updateRecentProjectFilesThumbnails() @@ -444,11 +453,11 @@ def _updateRecentProjectFilesThumbnails(self) -> None: def addRecentProjectFile(self, projectFile) -> None: """ Add a project file to the list of recent project files. - The function ensures that the file is not present more than once in the list and trims it so it - never exceeds a set number of projects. + The function ensures that the file is not present more than once in the list and trims + it so it never exceeds a set number of projects. QSettings are updated accordingly. - The update of the list of recent projects files is minimal: the filepath is added, but there is no - attempt to retrieve its corresponding thumbnail. + The update of the list of recent projects files is minimal: the filepath is added, but + there is no attempt to retrieve its corresponding thumbnail. Args: projectFile (str or QUrl): path to the project file to add to the list @@ -502,7 +511,8 @@ def addRecentProjectFile(self, projectFile) -> None: def removeRecentProjectFile(self, projectFile) -> None: """ Remove a given project file from the list of recent project files. - If the provided filepath is not already present in the list of recent project files, nothing is done. + If the provided filepath is not already present in the list of recent project files, + nothing is done. Otherwise, it is effectively removed and the QSettings are updated accordingly. """ if not isinstance(projectFile, (QUrl, str)): @@ -708,7 +718,7 @@ def _getEnvironmentVariableValue(self, key: str, defaultValue: bool) -> bool: val = os.environ.get(key, defaultValue) # os.environ.get returns a string if the key exists, no matter its value, and converting a # string to a bool always evaluates to "True" - if val != True and str(val).lower() in ("0", "false", "off"): + if not val and str(val).lower() in ("0", "false", "off"): return False return True @@ -720,9 +730,13 @@ def _getEnvironmentVariableValue(self, key: str, defaultValue: bool) -> bool: pipelineTemplateFilesChanged = Signal() recentProjectFilesChanged = Signal() recentImportedImagesFoldersChanged = Signal() - pipelineTemplateFiles = Property("QVariantList", _pipelineTemplateFiles, notify=pipelineTemplateFilesChanged) - pipelineTemplateNames = Property("QVariantList", _pipelineTemplateNames, notify=pipelineTemplateFilesChanged) - recentProjectFiles = Property("QVariantList", lambda self: self._recentProjectFiles, notify=recentProjectFilesChanged) - recentImportedImagesFolders = Property("QVariantList", _recentImportedImagesFolders, notify=recentImportedImagesFoldersChanged) + pipelineTemplateFiles = Property("QVariantList", _pipelineTemplateFiles, + notify=pipelineTemplateFilesChanged) + pipelineTemplateNames = Property("QVariantList", _pipelineTemplateNames, + notify=pipelineTemplateFilesChanged) + recentProjectFiles = Property("QVariantList", lambda self: self._recentProjectFiles, + notify=recentProjectFilesChanged) + recentImportedImagesFolders = Property("QVariantList", _recentImportedImagesFolders, + notify=recentImportedImagesFoldersChanged) default8bitViewerEnabled = Property(bool, _default8bitViewerEnabled, constant=True) defaultSequencePlayerEnabled = Property(bool, _defaultSequencePlayerEnabled, constant=True) diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index 74a9b16678..ed102b5b49 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -26,7 +26,8 @@ def redo(self): try: self.redoImpl() except Exception: - logging.error(f"Error while redoing command '{self.text()}': \n{traceback.format_exc()}") + logging.error(f"Error while redoing command '{self.text()}': " + f"\n{traceback.format_exc()}") def undo(self): if not self._enabled: @@ -34,7 +35,8 @@ def undo(self): try: self.undoImpl() except Exception: - logging.error(f"Error while undoing command '{self.text()}': \n{traceback.format_exc()}") + logging.error(f"Error while undoing command '{self.text()}': " + f"\n{traceback.format_exc()}") def redoImpl(self): # type: () -> bool @@ -64,7 +66,8 @@ def tryAndPush(self, command): try: res = command.redoImpl() except Exception as e: - logging.error(f"Error while trying command '{command.text()}': \n{traceback.format_exc()}") + logging.error(f"Error while trying command '{command.text()}': " + f"\n{traceback.format_exc()}") res = False if res is not False: command.setEnabled(False) @@ -114,9 +117,11 @@ def unlock(self): index = Property(int, QUndoStack.index, notify=_indexChanged) isUndoableIndexChanged = Signal() - isUndoableIndex = Property(bool, lambda self: self.index > self._undoableIndex, notify=isUndoableIndexChanged) + isUndoableIndex = Property(bool, lambda self: self.index > self._undoableIndex, + notify=isUndoableIndexChanged) lockedRedoChanged = Signal() - lockedRedo = Property(bool, lambda self: self._lockedRedo, setLockedRedo, notify=lockedRedoChanged) + lockedRedo = Property(bool, lambda self: self._lockedRedo, setLockedRedo, + notify=lockedRedoChanged) class GraphCommand(UndoCommand): @@ -139,7 +144,7 @@ def __init__(self, graph, nodeType, position, parent=None, **kwargs): elif isinstance(value, list): for idx, v in enumerate(value): if isinstance(v, Attribute): - value[idx] = v.asLinkExpr() + value[idx] = v.asLinkExpr() def redoImpl(self): node = self.graph.addNewNode(self.nodeType, position=self.position, **self.kwargs) @@ -161,7 +166,8 @@ def __init__(self, graph, node, parent=None): self.outListAttributes = {} # maps attribute's key with a tuple containing the name of the list it is connected to and its value def redoImpl(self): - # keep outEdges (inEdges are serialized in nodeDict so unneeded here) and outListAttributes to be able to recreate the deleted elements in ListAttributes + # keep outEdges (inEdges are serialized in nodeDict so unneeded here) and outListAttributes + # to be able to recreate the deleted elements in ListAttributes _, self.outEdges, self.outListAttributes = self.graph.removeNode(self.nodeName) return True @@ -169,7 +175,7 @@ def undoImpl(self): with GraphModification(self.graph): node = nodeFactory(self.nodeDict, self.nodeName) self.graph.addNode(node, self.nodeName) - assert (node.getName() == self.nodeName) + assert node.getName() == self.nodeName self.graph._restoreOutEdges(self.outEdges, self.outListAttributes) @@ -183,10 +189,11 @@ def __init__(self, graph, srcNodes, parent=None): self.setText("Duplicate Nodes") def redoImpl(self): - srcNodes = [ self.graph.node(i) for i in self.srcNodeNames ] + srcNodes = [self.graph.node(i) for i in self.srcNodeNames] # flatten the list of duplicated nodes to avoid lists within the list - duplicates = [ n for nodes in list(self.graph.duplicateNodes(srcNodes).values()) for n in nodes ] - self.duplicates = [ n.name for n in duplicates ] + duplicates = [n for nodes in list(self.graph.duplicateNodes(srcNodes).values()) + for n in nodes] + self.duplicates = [n.name for n in duplicates] return duplicates def undoImpl(self): @@ -209,11 +216,12 @@ def redoImpl(self): graph = Graph("") try: graph._deserialize(self.data) - except: + except Exception: return False boundingBoxCenter = self._boundingBoxCenter(graph.nodes) - offset = Position(self.position.x - boundingBoxCenter.x, self.position.y - boundingBoxCenter.y) + offset = Position(self.position.x - boundingBoxCenter.x, + self.position.y - boundingBoxCenter.y) for node in graph.nodes: node.position = Position(node.position.x + offset.x, node.position.y + offset.y) @@ -221,7 +229,8 @@ def redoImpl(self): nodes = self.graph.importGraphContent(graph) self.nodeNames = [node.name for node in nodes] - self.setText(f"Paste Node{'s' if len(self.nodeNames) > 1 else ''} ({', '.join(self.nodeNames)})") + self.setText(f"Paste Node{'s' if len(self.nodeNames) > 1 else ''} " + f"({', '.join(self.nodeNames)})") return nodes def undoImpl(self): @@ -230,7 +239,7 @@ def undoImpl(self): def _boundingBox(self, nodes) -> tuple[int, int, int, int]: if not nodes: - return (0, 0, 0 , 0) + return (0, 0, 0, 0) minX = maxX = nodes[0].x minY = maxY = nodes[0].y @@ -247,6 +256,7 @@ def _boundingBoxCenter(self, nodes): minX, minY, maxX, maxY = self._boundingBox(nodes) return Position((minX + maxX) / 2, (minY + maxY) / 2) + class ImportProjectCommand(GraphCommand): """ Handle the import of a project into a Graph. @@ -271,9 +281,11 @@ def redoImpl(self): for node in importedNodes: self.importedNames.append(node.name) if self.position is not None: - self.graph.node(node.name).position = Position(node.x + self.position.x, node.y + self.position.y) + self.graph.node(node.name).position = Position(node.x + self.position.x, + node.y + self.position.y) else: - self.graph.node(node.name).position = Position(node.x, node.y + lowestY + self.yOffset) + self.graph.node(node.name).position = Position(node.x, + node.y + lowestY + self.yOffset) return importedNodes @@ -315,7 +327,8 @@ def __init__(self, graph, src, dst, parent=None): self.setText(f"Connect '{self.srcAttr}'->'{self.dstAttr}'") if src.baseType != dst.baseType: - raise ValueError(f"Attribute types are not compatible and cannot be connected: '{self.srcAttr}'({src.baseType})->'{self.dstAttr}'({dst.baseType})") + raise ValueError(f"Attribute types are not compatible and cannot be connected: " + f"'{self.srcAttr}'({src.baseType})->'{self.dstAttr}'({dst.baseType})") def redoImpl(self): self.graph.addEdge(self.graph.attribute(self.srcAttr), self.graph.attribute(self.dstAttr)) @@ -390,8 +403,10 @@ class RemoveImagesCommand(GraphCommand): def __init__(self, graph, cameraInitNodes, parent=None): super().__init__(graph, parent) self.cameraInits = cameraInitNodes - self.viewpoints = { cameraInit.name: cameraInit.attribute("viewpoints").getSerializedValue() for cameraInit in self.cameraInits } - self.intrinsics = { cameraInit.name: cameraInit.attribute("intrinsics").getSerializedValue() for cameraInit in self.cameraInits } + self.viewpoints = {cameraInit.name: cameraInit.attribute("viewpoints").getSerializedValue() + for cameraInit in self.cameraInits} + self.intrinsics = {cameraInit.name: cameraInit.attribute("intrinsics").getSerializedValue() + for cameraInit in self.cameraInits} self.title = f"Remove{' ' if len(self.cameraInits) == 1 else ' All '}Images" self.setText(self.title) @@ -478,8 +493,9 @@ def undoImpl(self): @contextmanager def GroupedGraphModification(graph, undoStack, title, disableUpdates=True): - """ A context manager that creates a macro command disabling (if not already) graph update by default - and resetting its status after nested block execution. + """ + A context manager that creates a macro command disabling (if not already) graph update + by default and resetting its status after nested block execution. Args: graph (Graph): the Graph that will be modified diff --git a/meshroom/ui/components/edge.py b/meshroom/ui/components/edge.py index 53b5678106..3483f0fffe 100755 --- a/meshroom/ui/components/edge.py +++ b/meshroom/ui/components/edge.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import Signal, Property, QPointF, Qt, QObject, Slot, QRectF +from PySide6.QtCore import Signal, Property, QPointF, Qt, QObject, Slot from PySide6.QtGui import QPainterPath, QVector2D from PySide6.QtQuick import QQuickItem diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 4ebda0060f..52636fddcb 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -29,7 +29,7 @@ from meshroom.core.taskManager import TaskManager -from meshroom.core.node import NodeChunk, Node, Status, ExecMode, CompatibilityNode, Position +from meshroom.core.node import Node, Status, ExecMode, CompatibilityNode, Position from meshroom.core import submitters, MrNodeType from meshroom.ui import commands from meshroom.ui.utils import makeProperty diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 46b2a27622..57ea311073 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -4,7 +4,6 @@ import os from collections.abc import Iterable from multiprocessing.pool import ThreadPool -from threading import Thread from typing import Callable from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl, QSizeF, QPoint