diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 2a4dca5c5b..70c67b9696 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,19 @@ +# [tests] Linting: Remove trailing whitespaces +5fe886b6b08fa19082dc0e1bf837fa34c2e2de2d +# [core] Linting: Remove remaining trailing whitespaces +a44537b65a7c53c89c16e71ae207f37fa6554832 +# Linting: Fix E203, E225, E231, E261, E302, E303 and W292 warnings +1b5664c8cc54c55fae58a5be9bf63e9af2f5af95 +# [ui] Linting: Remove all trailing whitespaces +18d7f609b1a5cd7c43f970770374b649019c1e73 +# [core] Linting: Fix import order +aae05532b2409e3bd4c119646afc08656a916cb4 +# [core] Linting: Remove all trailing whitespaces +81394d7def1fcbc08cbc2a8721bc1f0a86fe8cc6 +# [desc] Linting: Remove trailing whitespaces +0a2ab8cab4f79191b0e7364416701ba561e75b6a +# [desc] Linting: Fix order of the imported modules +adf67e33533a75f5b280e0ee3fc0ece82307199e # [build] `setup.py`: Use double quotes everywhere 571de38ef1a9e72e6c1d2a997b60de5bd3caa5bf # [bin] `meshroom_batch`: Minor clean-up in the file diff --git a/meshroom/__init__.py b/meshroom/__init__.py index d487f22d67..45f409ccb1 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 + 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) @@ -60,6 +63,7 @@ def logToRoot(message, *args, **kwargs): setattr(logging.getLoggerClass(), methodName, logForLevel) setattr(logging, methodName, logToRoot) + addTraceLevel() logStringToPython = { 'fatal': logging.CRITICAL, diff --git a/meshroom/core/__init__.py b/meshroom/core/__init__.py index cf5c602fe0..84e8d1eede 100644 --- a/meshroom/core/__init__.py +++ b/meshroom/core/__init__.py @@ -78,10 +78,10 @@ def loadClasses(folder: str, packageName: str, classType: type) -> list[type]: else package.__name__ packageVersion = getattr(package, "__version__", None) packagePath = os.path.dirname(package.__file__) - except Exception as e: - tb = traceback.extract_tb(e.__traceback__) + except Exception as exc: + tb = traceback.extract_tb(exc.__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 "{resolvedFolder}" ({type(exc).__name__}): {str(exc)}\n' # filename:lineNumber functionName f'{last_call.filename}:{last_call.lineno} {last_call.name}\n' # line of code with the error @@ -114,18 +114,18 @@ 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 valid " + f"default values/ranges: {', '.join(nodePlugin.errors)}") classes.append(nodePlugin) else: classes.append(p) - except Exception as e: + except Exception as exc: if classType == BaseSubmitter: logging.warning(f" Could not load submitter {pluginName} from package '{package.__name__}'") else: - tb = traceback.extract_tb(e.__traceback__) + tb = traceback.extract_tb(exc.__traceback__) last_call = tb[-1] - errors.append(f' * {pluginName} ({type(e).__name__}): {e}\n' + errors.append(f' * {pluginName} ({type(exc).__name__}): {exc}\n' # filename:lineNumber functionName f'{last_call.filename}:{last_call.lineno} {last_call.name}\n' # line of code with the error diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index 0e24f6cbfe..d1be53b4fd 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: from meshroom.core.graph import Edge + def attributeFactory(description: str, value, isOutput: bool, node, root=None, parent=None): """ Create an Attribute based on description type. @@ -75,22 +76,22 @@ 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._keyValues = None # list of pairs (key, value) for keyable attribute + self._keyValues = None # list of pairs (key, value) for keyable attribute 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)}]' @@ -99,8 +100,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() + "}" @@ -158,7 +159,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 @@ -254,9 +255,9 @@ def getDefaultValue(self): if callable(self._desc.value): try: return self._desc.value(self) - except Exception as e: + except Exception as exc: 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) for attribute '{self.fullName}': {exc}") return None # keyable attribute default value if self.keyable: @@ -306,13 +307,13 @@ def getValueStr(self, withQuotes=True) -> str: return str(self._getEvalValue()) def validateValue(self, value): - """ - Ensure value is compatible with the attribute description and convert value if needed. + """ + Ensure value is compatible with the attribute description and convert value if needed. """ return self._desc.validateValue(value) def upgradeValue(self, exportedValue): - """ + """ Upgrade the attribute value within a compatibility node. """ self._setValue(exportedValue) @@ -322,7 +323,7 @@ def _isDefault(self): return len(self._keyValues.pairs) == 0 else: return self._getValue() == self.getDefaultValue() - + def _isValid(self): """ Check attribute description validValue: @@ -332,15 +333,15 @@ def _isValid(self): if callable(self._desc.validValue): try: return self._desc.validValue(self.node) - except Exception as e: + except Exception as exc: 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 attribute '{self.fullName}': {exc}") 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 @@ -348,8 +349,8 @@ def _is2dDisplayable(self) -> bool: if self._desc.semantic == imageSemantic), None) is not None def _is3dDisplayable(self) -> bool: - """ - Return True if the current attribute is considered as a displayable 3d file + """ + Return True if the current attribute is considered as a displayable 3D file. """ if self._desc.semantic == "3d": return True @@ -399,9 +400,9 @@ def _getEnabled(self) -> bool: if callable(self._desc.enabled): try: return self._desc.enabled(self.node) - except Exception as e: + except Exception as exc: if not self.node.isCompatibilityNode: - logging.warning(f"Failed to evaluate 'enabled' (node lambda) for attribute '{self.fullName}': {e}") + logging.warning(f"Failed to evaluate 'enabled' (node lambda) for attribute '{self.fullName}': {exc}") return True return self._desc.enabled @@ -412,14 +413,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 """ @@ -431,7 +432,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 @@ -440,16 +441,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() @@ -493,7 +494,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) @@ -544,7 +545,7 @@ def matchText(self, text: str) -> bool: is3dDisplayable = Property(bool, _is3dDisplayable, constant=True) # Whether the attribute is a shape or a shape list, managed by the ShapeEditor and ShapeViewer. hasDisplayableShape = Property(bool, lambda self: False, constant=True) - + # Attribute link properties and signals inputLinksChanged = Signal() outputLinksChanged = Signal() @@ -570,7 +571,7 @@ def matchText(self, text: str) -> bool: def raiseIfLink(func): - """ + """ If Attribute instance is a link, raise a RuntimeError. """ def wrapper(attr, *args, **kwargs): @@ -618,12 +619,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 (param: {self.name}, " + f"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) @@ -669,7 +670,7 @@ def __iter__(self): return iter(self.value) def at(self, idx): - """ + """ Returns child attribute at index 'idx'. """ # Implement 'at' rather than '__getitem__' @@ -819,8 +820,8 @@ 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: @@ -829,8 +830,8 @@ def _getAllInputLinks(self) -> list["Attribute"]: # 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: @@ -853,7 +854,6 @@ def _hasAnyOutputLinks(self) -> bool: return super()._hasAnyOutputLinks() or \ 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) isDefault = Property(bool, lambda self: len(self.value) == 0, notify=Attribute.valueChanged) @@ -1039,9 +1039,9 @@ def upgradeValue(self, exportedValue): # Should be remove if link expression serialization is added in GroupAttribute. def getSerializedValue(self): if self.isLink: - return self._getInputLink().asLinkExpr() + return self._getInputLink().asLinkExpr() return super().getSerializedValue() - + def getValueAsDict(self) -> dict: """ Return the geometry attribute value as dict. @@ -1105,7 +1105,7 @@ def hasObservation(self, key: str) -> bool: return all((isinstance(attribute, GeometryAttribute) and attribute.hasObservation(key)) or (not isinstance(attribute, GeometryAttribute) and attribute.keyValues.hasKey(key)) for attribute in self.value) - + @raiseIfLink def removeObservation(self, key: str): """ @@ -1154,7 +1154,7 @@ def getObservation(self, key: str) -> Variant: geoObservation = attribute.getObservation(key) if geoObservation is None: return None - else : + else: observation[attribute.name] = geoObservation else: if attribute.keyable: @@ -1165,17 +1165,16 @@ def getObservation(self, key: str) -> Variant: else: observation[attribute.name] = attribute.value return observation - + # Properties and signals # Emitted when a geometry observation changed. observationsChanged = Signal() # Whether the geometry attribute childs are keyable. - observationKeyable = Property(bool,_hasKeyableChilds, constant=True) + observationKeyable = Property(bool, _hasKeyableChilds, constant=True) # The list of geometry observation keys. observationKeys = Property(Variant, _getObservationKeys, notify=observationsChanged) # The number of geometry observation defined. - nbObservations = Property(int, _getNbObservations, notify=observationsChanged) - + nbObservations = Property(int, _getNbObservations, notify=observationsChanged) class ShapeAttribute(GroupAttribute): @@ -1186,12 +1185,13 @@ class ShapeAttribute(GroupAttribute): def __init__(self, node, attributeDesc: desc.Shape, isOutput: bool, root=None, parent=None): super().__init__(node, attributeDesc, isOutput, root, parent) self._visible = True - + # Override # Connect geometry attribute valueChanged to emit geometryChanged signal. def _initValue(self): super()._initValue() - # Using Attribute.valueChanged for the userName, userColor, geometry properties results in a segmentation fault. + # Using Attribute.valueChanged for the userName, userColor, geometry properties results + # in a segmentation fault. # As a workaround, we manually connect valueChanged to shapeChanged or geometryChanged. self.value.get("userName").valueChanged.connect(self._onShapeChanged) self.value.get("userColor").valueChanged.connect(self._onShapeChanged) @@ -1202,51 +1202,51 @@ def _initValue(self): # Should be remove if link expression serialization is added in GroupAttribute. def getSerializedValue(self): if self.isLink: - return self._getInputLink().asLinkExpr() + return self._getInputLink().asLinkExpr() return super().getSerializedValue() def getShapeAsDict(self) -> dict: """ Return the shape attribute as dict with the shape file structure. """ - outDict = { - "name" : self.userName if self.userName else self.rootName, - "type" : self.type, - "properties" : { "color": self.userColor } + outDict = { + "name": self.userName if self.userName else self.rootName, + "type": self.type, + "properties": {"color": self.userColor} } if not self.geometry.observationKeyable: # Not keyable geometry, use properties. outDict.get("properties").update(self.geometry.getSerializedValue()) else: # Keyable geometry, use observations. - outDict.update({ "observations" : self.geometry.getValueAsDict()}) + outDict.update({"observations": self.geometry.getValueAsDict()}) return outDict - + def _getVisible(self) -> bool: - """ + """ Return whether the shape attribute is visible for display. """ return self._visible - - def _setVisible(self, visible:bool): - """ + + def _setVisible(self, visible: bool): + """ Set the shape attribute visibility for display. """ self._visible = visible self.shapeChanged.emit() def _getUserName(self) -> str: - """ + """ Return the shape attribute user name for display. """ return self.value.get("userName").value def _getUserColor(self) -> str: - """ + """ Return the shape attribute user color for display. """ return self.value.get("userColor").value - + @Slot() def _onShapeChanged(self): """ @@ -1279,6 +1279,7 @@ def _onGeometryChanged(self): # Override hasDisplayableShape property. hasDisplayableShape = Property(bool, lambda self: True, constant=True) + class ShapeListAttribute(ListAttribute): """ ListAttribute subtype tailored for shape-specific handling. @@ -1301,15 +1302,15 @@ def getShapesAsDict(self): return [shapeAttribute.getShapeAsDict() for shapeAttribute in self.value] def _getVisible(self) -> bool: - """ + """ Return whether the shape list is visible for display. """ if self.isLink: return self.inputLink.isVisible return self._visible - - def _setVisible(self, visible:bool): - """ + + def _setVisible(self, visible: bool): + """ Set the shape visibility for display. """ if self.isLink: @@ -1327,4 +1328,4 @@ def _setVisible(self, visible:bool): # Whether the shape list is displayable. isVisible = Property(bool, _getVisible, _setVisible, notify=shapeListChanged) # Override hasDisplayableShape property. - hasDisplayableShape = Property(bool, lambda self: True, constant=True) \ No newline at end of file + hasDisplayableShape = Property(bool, lambda self: True, constant=True) diff --git a/meshroom/core/desc/attribute.py b/meshroom/core/desc/attribute.py index 4ee686e3d6..34381e3d34 100644 --- a/meshroom/core/desc/attribute.py +++ b/meshroom/core/desc/attribute.py @@ -10,8 +10,8 @@ class Attribute(BaseObject): """ """ - def __init__(self, name, label, description, value, advanced, semantic, group, enabled, - keyable=False, keyType=None, invalidate=True, uidIgnoreValue=None, + def __init__(self, name, label, description, value, advanced, semantic, group, enabled, + keyable=False, keyType=None, invalidate=True, uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False): super(Attribute, self).__init__() self._name = name @@ -49,7 +49,7 @@ def validateValue(self, value): """ raise NotImplementedError("Attribute.validateValue is an abstract function that should be " "implemented in the derived class.") - + def validateKeyValues(self, keyValues): """ Return validated/conformed 'keyValues'. @@ -158,8 +158,8 @@ def validateValue(self, value): value = ast.literal_eval(value) if not isinstance(value, (list, tuple)): - raise ValueError("ListAttribute only supports list/tuple input values " - "(param:{}, value:{}, type:{})".format(self.name, value, type(value))) + raise ValueError(f"ListAttribute only supports list/tuple input values " + f"(param: {self.name}, value: {value}, type: {type(value)})") return value def checkValueTypes(self): @@ -214,20 +214,21 @@ def validateValue(self, value): if isinstance(value, dict): # invalidKeys = set(value.keys()).difference([attr.name for attr in self._groupDesc]) # if invalidKeys: - # raise ValueError('Value contains key that does not match group description : {}'.format(invalidKeys)) + # raise ValueError(f"Value contains key that does not match group description: " + # f"{invalidKeys}") if self._groupDesc and value.keys(): commonKeys = set(value.keys()).intersection([attr.name for attr in self._groupDesc]) if not commonKeys: - raise ValueError(f"Value contains no key that matches with the group description " - f"(name={self.name}, values={value.keys()}, " + raise ValueError(f"Value contains no key that matches with the group " + f"description (name={self.name}, values={value.keys()}, " f"desc={[attr.name for attr in self._groupDesc]})") elif isinstance(value, (list, tuple, set)): if len(value) != len(self._groupDesc): - raise ValueError("Value contains incoherent number of values: desc size: {}, value size: {}". - format(len(self._groupDesc), len(value))) + raise ValueError(f"Value contains incoherent number of values: " + f"desc size: {len(self._groupDesc)}, value size: {len(value)}") else: - raise ValueError("GroupAttribute only supports dict/list/tuple input values (param:{}, value:{}, type:{})". - format(self.name, value, type(value))) + raise ValueError(f"GroupAttribute only supports dict/list/tuple input values " + f"(param: {self.name}, value: {value}, type: {type(value)})") return value @@ -288,12 +289,12 @@ def retrieveChildrenInvalidations(self): class Param(Attribute): """ """ - def __init__(self, name, label, description, value, group, advanced, semantic, enabled, - keyable=False, keyType=None, invalidate=True, uidIgnoreValue=None, + def __init__(self, name, label, description, value, group, advanced, semantic, enabled, + keyable=False, keyType=None, invalidate=True, uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False): super(Param, self).__init__(name=name, label=label, description=description, value=value, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, - enabled=enabled, invalidate=invalidate, semantic=semantic, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, invalidate=invalidate, semantic=semantic, uidIgnoreValue=uidIgnoreValue, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) @@ -312,8 +313,8 @@ def validateValue(self, value): if value is None: return value if not isinstance(value, str): - raise ValueError("File only supports string input (param:{}, value:{}, type:{})". - format(self.name, value, type(value))) + raise ValueError(f"File only supports string input (param: {self.name}, value: " + f"{value}, type: {type(value)})") return os.path.normpath(value).replace("\\", "/") if value else "" def checkValueTypes(self): @@ -327,12 +328,12 @@ def checkValueTypes(self): class BoolParam(Param): """ """ - def __init__(self, name, label, description, value, keyable=False, keyType=None, - group="allParams", advanced=False, enabled=True, invalidate=True, + def __init__(self, name, label, description, value, keyable=False, keyType=None, + group="allParams", advanced=False, enabled=True, invalidate=True, semantic="", visible=True, exposed=False): super(BoolParam, self).__init__(name=name, label=label, description=description, value=value, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, - enabled=enabled, invalidate=invalidate, semantic=semantic, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, invalidate=invalidate, semantic=semantic, visible=visible, exposed=exposed) self._valueType = bool @@ -345,8 +346,8 @@ def validateValue(self, value): return bool(distutils.util.strtobool(value)) return bool(value) except Exception: - raise ValueError("BoolParam only supports bool value (param:{}, value:{}, type:{})". - format(self.name, value, type(value))) + raise ValueError(f"BoolParam only supports bool value (param: {self.name}, " + f"value: {value}, type: {type(value)})") def checkValueTypes(self): if not isinstance(self.value, bool): @@ -357,13 +358,13 @@ def checkValueTypes(self): class IntParam(Param): """ """ - def __init__(self, name, label, description, value, range=None, keyable=False, keyType=None, - group="allParams", advanced=False, enabled=True, invalidate=True, semantic="", + def __init__(self, name, label, description, value, range=None, keyable=False, keyType=None, + group="allParams", advanced=False, enabled=True, invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False): self._range = range super(IntParam, self).__init__(name=name, label=label, description=description, value=value, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, - enabled=enabled, invalidate=invalidate, semantic=semantic, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, invalidate=invalidate, semantic=semantic, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) self._valueType = int @@ -375,8 +376,8 @@ def validateValue(self, value): try: return int(value) except Exception: - raise ValueError("IntParam only supports int value (param:{}, value:{}, type:{})". - format(self.name, value, type(value))) + raise ValueError(f"IntParam only supports int value (param: {self.name}, value: " + f"{value}, type: {type(value)})") def checkValueTypes(self): if not isinstance(self.value, int) or (self.range and not all([isinstance(r, int) for r in self.range])): @@ -389,13 +390,13 @@ def checkValueTypes(self): class FloatParam(Param): """ """ - def __init__(self, name, label, description, value, range=None, keyable=False, keyType=None, - group="allParams", advanced=False, enabled=True, invalidate=True, semantic="", + def __init__(self, name, label, description, value, range=None, keyable=False, keyType=None, + group="allParams", advanced=False, enabled=True, invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False): self._range = range super(FloatParam, self).__init__(name=name, label=label, description=description, value=value, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, - enabled=enabled, invalidate=invalidate, semantic=semantic, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, + enabled=enabled, invalidate=invalidate, semantic=semantic, validValue=validValue, errorMessage=errorMessage, visible=visible, exposed=exposed) self._valueType = float @@ -406,8 +407,8 @@ def validateValue(self, value): try: return float(value) except Exception: - raise ValueError("FloatParam only supports float value (param:{}, value:{}, type:{})". - format(self.name, value, type(value))) + raise ValueError(f"FloatParam only supports float value (param: {self.name}, value: " + f"{value}, type:{type(value)})") def checkValueTypes(self): if not isinstance(self.value, float) or (self.range and not all([isinstance(r, float) for r in self.range])): @@ -445,11 +446,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, + + 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. + 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. """ @@ -457,8 +458,8 @@ 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="", + 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): @@ -506,8 +507,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 (param: " + f"{self.name}, value: {value}, type: {type(value)}).") return [self.conformValue(v) for v in value] @@ -550,8 +551,8 @@ 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 (param: " + f"{self.name}, value: {value}, type: {type(value)})") return value def checkValueTypes(self): @@ -574,7 +575,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 name " + f"or an hexadecimal color code (param: {self.name}, value: {value}, " + f"type: {type(value)})") return value - diff --git a/meshroom/core/desc/geometryAttribute.py b/meshroom/core/desc/geometryAttribute.py index d54ab3c73a..f1fc1379c0 100644 --- a/meshroom/core/desc/geometryAttribute.py +++ b/meshroom/core/desc/geometryAttribute.py @@ -1,7 +1,8 @@ from meshroom.core.desc import GroupAttribute, FloatParam + class Geometry(GroupAttribute): - """ + """ Base attribute for all Geometry attribute. Countains several attributes (inherit from GroupAttribute). """ @@ -13,13 +14,14 @@ def __init__(self, groupDesc, name, label, description, group="allParams", advan enabled=enabled, visible=visible, exposed=exposed) def getInstanceType(self): - """ - Return the correct Attribute instance corresponding to the description. + """ + Return the correct Attribute instance corresponding to the description. """ # Import within the method to prevent cyclic dependencies from meshroom.core.attribute import GeometryAttribute return GeometryAttribute - + + class Size2d(Geometry): """ Size2d is a Geometry attribute that allows to specify a 2d size. @@ -30,16 +32,16 @@ def __init__(self, name, label, description, width, height, widthRange=None, hei # Geometry group desciption groupDesc = [ FloatParam(name="width", label="Width", description="Width size.", value=width, range=widthRange, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), FloatParam(name="height", label="Height", description="Height size.", value=height, range=heightRange, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # GeometryAttribute constructor - super(Size2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, + super(Size2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) - + class Vec2d(Geometry): """ Vec2d is a Geometry attribute that allows to specify a 2d vector. @@ -50,12 +52,12 @@ def __init__(self, name, label, description, x, y, xRange=None, yRange=None, # Geometry group desciption groupDesc = [ FloatParam(name="x", label="X", description="X coordinate.", value=x, range=xRange, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), FloatParam(name="y", label="Y", description="Y coordinate.", value=y, range=yRange, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # GeometryAttribute constructor - super(Vec2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, - semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) \ No newline at end of file + super(Vec2d, self).__init__(groupDesc, name, label, description, group=None, advanced=advanced, + semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) diff --git a/meshroom/core/desc/node.py b/meshroom/core/desc/node.py index c8d795ae5a..83b2f8d093 100644 --- a/meshroom/core/desc/node.py +++ b/meshroom/core/desc/node.py @@ -2,19 +2,19 @@ from inspect import getfile from pathlib import Path import logging -import os -import psutil import shlex import shutil import sys -from .computation import Level, StaticNodeSize -from .attribute import StringParam, ColorParam, ChoiceParam +import psutil import meshroom from meshroom.core import cgroup from meshroom.core.utils import VERBOSE_LEVEL +from .computation import Level, StaticNodeSize +from .attribute import StringParam, ColorParam, ChoiceParam + _MESHROOM_ROOT = Path(meshroom.__file__).parent.parent.as_posix() _MESHROOM_COMPUTE = (Path(_MESHROOM_ROOT) / "bin" / "meshroom_compute").as_posix() _MESHROOM_COMPUTE_DEPS = ["psutil"] @@ -150,7 +150,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/desc/shapeAttribute.py b/meshroom/core/desc/shapeAttribute.py index 58a3e48020..2ef7311003 100644 --- a/meshroom/core/desc/shapeAttribute.py +++ b/meshroom/core/desc/shapeAttribute.py @@ -1,7 +1,7 @@ from meshroom.core.desc import ListAttribute, GroupAttribute, StringParam, FloatParam, Geometry, Size2d, Vec2d class Shape(GroupAttribute): - """ + """ Base attribute for all Shape attribute. Countains several attributes (inherit from GroupAttribute). """ @@ -13,7 +13,7 @@ def __init__(self, geometryGroupDesc, name, label, description, group="allParams group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), StringParam(name="userColor", label="User Color", description="User shape color.", value="#2a82da", group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), - Geometry(geometryGroupDesc, name="geometry", label="Geometry", description="Shape geometry.", + Geometry(geometryGroupDesc, name="geometry", label="Geometry", description="Shape geometry.", group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # GroupAttribute constructor @@ -22,28 +22,28 @@ def __init__(self, geometryGroupDesc, name, label, description, group="allParams enabled=enabled, visible=visible, exposed=exposed) def getInstanceType(self): - """ - Return the correct Attribute instance corresponding to the description. + """ + Return the correct Attribute instance corresponding to the description. """ # Import within the method to prevent cyclic dependencies from meshroom.core.attribute import ShapeAttribute return ShapeAttribute class ShapeList(ListAttribute): - """ + """ List attribute of Shape attribute. Countains several attributes (inherit from ListAttribute). """ def __init__(self, shape: Shape, name, label, description, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): # ListAttribute constructor - super(ShapeList, self).__init__(elementDesc=shape, name=name, label=label, description=description, - group=group, advanced=advanced, semantic=semantic, + super(ShapeList, self).__init__(elementDesc=shape, name=name, label=label, description=description, + group=group, advanced=advanced, semantic=semantic, enabled=enabled, visible=visible, exposed=exposed) def getInstanceType(self): - """ - Return the correct Attribute instance corresponding to the description. + """ + Return the correct Attribute instance corresponding to the description. """ # Import within the method to prevent cyclic dependencies from meshroom.core.attribute import ShapeListAttribute @@ -53,14 +53,14 @@ class Point2d(Shape): """ Point2d is a Shape attribute that allows to display and modify a 2d point. """ - def __init__(self, name, label, description, keyable=False, keyType=None, + def __init__(self, name, label, description, keyable=False, keyType=None, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): # Geometry group desciption geometryGroupDesc = [ - FloatParam(name="x", label="X", description="X coordinate.", value=-1.0, keyable=keyable, keyType=keyType, + FloatParam(name="x", label="X", description="X coordinate.", value=-1.0, keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), - FloatParam(name="y", label="Y", description="Y coordinate.", value=-1.0, keyable=keyable, keyType=keyType, + FloatParam(name="y", label="Y", description="Y coordinate.", value=-1.0, keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # ShapeAttribute constructor @@ -71,14 +71,14 @@ class Line2d(Shape): """ Line2d is a Shape attribute that allows to display and modify a 2d line. """ - def __init__(self, name, label, description, keyable=False, keyType=None, + def __init__(self, name, label, description, keyable=False, keyType=None, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): # Geometry group desciption geometryGroupDesc = [ - Vec2d(name="a", label="A", description="Line A point.", x=-1.0, y=-1.0, keyable=keyable, keyType=keyType, + Vec2d(name="a", label="A", description="Line A point.", x=-1.0, y=-1.0, keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), - Vec2d(name="b", label="B", description="Line B point.", x=-1.0, y=-1.0, keyable=keyable, keyType=keyType, + Vec2d(name="b", label="B", description="Line B point.", x=-1.0, y=-1.0, keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # ShapeAttribute constructor @@ -89,16 +89,16 @@ class Rectangle(Shape): """ Rectangle is a Shape attribute that allows to display and modify a rectangle. """ - def __init__(self, name, label, description, keyable=False, keyType=None, + def __init__(self, name, label, description, keyable=False, keyType=None, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): # Geometry group desciption geometryGroupDesc = [ - Vec2d(name="center", label="Center", description="Rectangle center.", x=-1.0, y=-1.0, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, + Vec2d(name="center", label="Center", description="Rectangle center.", x=-1.0, y=-1.0, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), Size2d(name="size", label="Size", description="Rectangle size.", width=-1.0, height=-1.0, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # ShapeAttribute constructor @@ -109,16 +109,16 @@ class Circle(Shape): """ Circle is a Shape attribute that allows to display and modify a circle. """ - def __init__(self, name, label, description, keyable=False, keyType=None, + def __init__(self, name, label, description, keyable=False, keyType=None, group="allParams", advanced=False, semantic="", enabled=True, visible=True, exposed=False): # Geometry group desciption geometryGroupDesc = [ - Vec2d(name="center", label="Center", description="Circle center.", x=-1.0, y=-1.0, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, + Vec2d(name="center", label="Center", description="Circle center.", x=-1.0, y=-1.0, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed), - FloatParam(name="radius", label="Radius", description="Circle radius.", value=-1.0, - keyable=keyable, keyType=keyType, group=group, advanced=advanced, + FloatParam(name="radius", label="Radius", description="Circle radius.", value=-1.0, + keyable=keyable, keyType=keyType, group=group, advanced=advanced, enabled=enabled, visible=visible, exposed=exposed) ] # ShapeAttribute constructor diff --git a/meshroom/core/evaluation.py b/meshroom/core/evaluation.py index 4806160ff6..26c8018a2a 100644 --- a/meshroom/core/evaluation.py +++ b/meshroom/core/evaluation.py @@ -5,7 +5,7 @@ class MathEvaluator: """ Evaluate math expressions - + ..code::py # Example usage mev = MathEvaluator() 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/fileUtils.py b/meshroom/core/fileUtils.py index adcbfb3a85..f79e3c85f9 100644 --- a/meshroom/core/fileUtils.py +++ b/meshroom/core/fileUtils.py @@ -10,7 +10,7 @@ def getFileElements(inputFilePath: str): filename = os.path.basename(inputFilePath) match = compiled_pattern.fullmatch(filename) frameId_str = match.group("FRAMEID_STR") - + fileElements = {} if match: fileElements = { @@ -63,4 +63,3 @@ def resolvePath(input, outputTemplate: str) -> str: resolved = replacePatterns(outputTemplate, compiled_element, replacements) return resolved - diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index 5ea54e4a0f..b2c4eaf29e 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -4,8 +4,8 @@ import re from typing import Any, Optional from collections.abc import Iterable -import weakref from collections import defaultdict, OrderedDict +import weakref from contextlib import contextmanager from pathlib import Path @@ -111,8 +111,10 @@ def examineEdge(self, e, g): pass def treeEdge(self, e, g): - """ Is invoked on each edge as it becomes a member of the edges that form the search tree. - If you wish to record predecessors, do so at this event point. """ + """ + Is invoked on each edge as it becomes a member of the edges that form the search tree. + If you wish to record predecessors, do so at this event point. + """ pass def backEdge(self, e, g): @@ -120,18 +122,25 @@ def backEdge(self, e, g): pass def forwardOrCrossEdge(self, e, g): - """ Is invoked on forward or cross edges in the graph. - In an undirected graph this method is never called.""" + """ + Is invoked on forward or cross edges in the graph. + In an undirected graph this method is never called. + """ pass def finishEdge(self, e, g): - """ Is invoked on the non-tree edges in the graph - as well as on each tree edge after its target vertex is finished. """ + """ + Is invoked on the non-tree edges in the graph + as well as on each tree edge after its target vertex is finished. + """ pass def finishVertex(self, u, g): - """ Is invoked on a vertex after all of its out edges have been added to the search tree and all of the - adjacent vertices have been discovered (but before their out-edges have been examined). """ + """ + Is invoked on a vertex after all of its out edges have been added to the search tree and + all of the adjacent vertices have been discovered (but before their out-edges have been + examined). + """ pass @@ -238,7 +247,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. """ @@ -372,7 +381,9 @@ def _evaluateUidConflicts(self, graphContent: dict): """ 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) @@ -664,14 +675,17 @@ 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. @@ -714,7 +728,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,10 +739,11 @@ 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". - + """ + 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} @@ -755,8 +771,8 @@ def _recreateTargetListAttributeChildren(listAttrName: str, index: int, value: A logging.warning(f"Failed to restore edge {srcName}{' (missing)' if srcAttr is None else ''} -> {dstName}{' (missing)' if dstAttr is None else ''}") continue self.addEdge(srcAttr, dstAttr) - except (KeyError, ValueError) as e: - logging.warning(f"Failed to restore edge {srcName} -> {dstName}: {e}") + except (KeyError, ValueError) as err: + logging.warning(f"Failed to restore edge {srcName} -> {dstName}: {err}") def upgradeAllNodes(self): """ Upgrade all upgradable CompatibilityNode instances in the graph. """ @@ -1522,14 +1538,16 @@ def iterChunksByStatus(self, status): yield chunk def getChunksByStatus(self, status): - """ Return the list of NodeChunks with the given status """ + """ Return the list of NodeChunks with the given status. """ chunks = [] for node in self.nodes: chunks += [chunk for chunk in node.chunks if chunk.status.status == 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] @@ -1681,8 +1699,8 @@ def executeGraph(graph, toNodes=None, forceCompute=False, forceStatus=False): print(f'\n[{n + 1}/{len(nodes)}] {node.nodeType}') chunk.process(forceCompute) node.postprocess() - except Exception as e: - logging.error(f"Error on node computation: {e}") + except Exception as exc: + logging.error(f"Error on node computation: {exc}") graph.clearSubmittedNodes() raise @@ -1717,8 +1735,8 @@ def submitGraph(graph, submitter, toNodes=None, submitLabel="{projectName}"): if res: for node in nodesToProcess: node.initStatusOnSubmit() # update node status - except Exception as e: - logging.error(f"Error on submit : {e}") + except Exception as exc: + logging.error(f"Error on submit: {exc}") def submit(graphFile, submitter, toNode=None, submitLabel="{projectName}"): diff --git a/meshroom/core/keyValues.py b/meshroom/core/keyValues.py index 73e78fbf36..93350faf34 100644 --- a/meshroom/core/keyValues.py +++ b/meshroom/core/keyValues.py @@ -1,7 +1,8 @@ +import json +from typing import Any + from meshroom.common import BaseObject, Property, Variant, Signal, DictModel, Slot from meshroom.core import desc, hashValue -from typing import Any -import json class KeyValues(BaseObject): """ @@ -9,7 +10,7 @@ class KeyValues(BaseObject): """ class KeyValuePair(BaseObject): - """ + """ Pair of (key, value), this object cannot be modified. """ def __init__(self, key: int, value: Any, parent=None): @@ -33,18 +34,18 @@ def __init__(self, desc: desc.Attribute, parent=None): # TODO: Add interpolation. For now no interpolation. def reset(self): - """ + """ Clear the list of pairs. """ self._pairs.clear() self.pairsChanged.emit() def resetFromDict(self, pairs: dict): - """ + """ Reset the list of pairs from a given dict. """ self._pairs.clear() - for k,v in pairs.items(): + for k, v in pairs.items(): self._pairs.add(KeyValues.KeyValuePair(int(k), self._desc.validateValue(v), self)) self.pairsChanged.emit() @@ -80,7 +81,7 @@ def getSerializedValues(self) -> Any: Return the list of pairs serialized. """ return { str(pair.key): pair.value for pair in self._pairs } - + def getKeys(self) -> list: """ Return the list of keys. @@ -95,7 +96,7 @@ def getJson(self) -> str: def uid(self) -> str: """ - Compute the UID from the list of pairs. + Compute the UID from the list of pairs. """ uids = [] for pair in sorted(self._pairs, key=lambda pair: pair.key): @@ -127,4 +128,4 @@ def getValueAtKeyOrDefault(self, key: str) -> Any: # The list of pairs (key, value). pairs = Property(Variant, lambda self: self._pairs, notify=pairsChanged) # The type of key used (viewId, poseId, ...). - keyType = Property(str, lambda self: self._desc.keyType, constant=True) \ No newline at end of file + keyType = Property(str, lambda self: self._desc.keyType, constant=True) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 75ab1ce5c9..2a5c9f77bc 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -397,8 +397,8 @@ def updateStatusFromCache(self): # logging.debug(f"updateStatusFromCache({self.node.name}): From status {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}") + except Exception as exc: + logging.debug(f"updateStatusFromCache({self.node.name}): Error while loading status file {statusFile}: {exc}") self.statusFileLastModTime = -1 self._status.reset() self._status.setNodeType(self.node) @@ -704,11 +704,11 @@ def __getattr__(self, k): try: # Throws exception if not in prototype chain return object.__getattribute__(self, k) - except AttributeError as e: + except AttributeError as err: try: return self.attribute(k) except KeyError: - raise e + raise err def getMrNodeType(self): # In compatibility mode, we may or may not have access to the nodeDesc and its information @@ -786,7 +786,7 @@ def getDocumentation(self): return self.nodeDesc.documentation else: return self.nodeDesc.__doc__ - + def getNodeInfo(self): if not self.nodeDesc: return [] @@ -813,7 +813,7 @@ def getNodeInfo(self): for key, value in additionalNodeInfo: info[key] = value return [{"key": k, "value": v} for k, v in info.items()] - + @property def packageFullName(self): return '-'.join([self.packageName, self.packageVersion]) @@ -1018,22 +1018,17 @@ def _buildAttributeExpVars(expVars, name, attr): except AttributeError: # If we load an old scene, the lambda associated to the 'value' could try to # access other params that could not exist yet - logging.warning('Invalid lambda evaluation for "{nodeName}.{attrName}"'. - format(nodeName=self.name, attrName=attr.name)) + logging.warning(f'Invalid lambda evaluation for "{self.name}.{attr.name}"') if defaultValue is not None: try: attr.value = defaultValue.format(**self._expVars) attr._invalidationValue = defaultValue.format(**expVarsNoCache) - 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))) - 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))) + except KeyError as err: + logging.warning(f'Invalid expression with missing key on "{self.name}.{attr.name}" with ' + f'value "{defaultValue}".\nError: {str(err)}') + except ValueError as err: + logging.warning(f'Invalid expression value on "{self.name}.{attr.name}" with value ' + f'"{defaultValue}".\nError: {str(err)}') # xxValue is exposed without quotes to allow to compose expressions self._expVars[name + 'Value'] = attr.getValueStr(withQuotes=False) @@ -1126,13 +1121,13 @@ def clearData(self): if self.internalFolder and os.path.exists(self.internalFolder): try: shutil.rmtree(self.internalFolder) - except Exception as e: + except Exception as exc: # We could get some "Device or resource busy" on .nfs file while removing the folder # on Linux network. # 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: {exc}.") self.updateStatusFromCache() @Slot(result=str) @@ -1206,7 +1201,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: @@ -1750,9 +1745,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 - + def _hasDisplayableShape(self): """ Return True if at least one attribute is a ShapeAttribute, a ShapeListAttribute or a shape File. @@ -1761,7 +1755,7 @@ def _hasDisplayableShape(self): """ return next((attr for attr in self._attributes if attr.hasDisplayableShape or attr.desc.semantic == "shapeFile"), None) is not None - + name = Property(str, getName, constant=True) defaultLabel = Property(str, getDefaultLabel, constant=True) @@ -2140,9 +2134,8 @@ def issueDetails(self): if self.issue == CompatibilityIssue.UnknownNodeType: return f"Unknown node type: '{self.nodeType}'." elif self.issue == CompatibilityIssue.VersionConflict: - return "Node version '{}' conflicts with current version '{}'.".format( - self.nodeDict["version"], nodeVersion(self.nodeDesc) - ) + version = self.nodeDict["version"] + return f"Node version '{version}' conflicts with current version '{nodeVersion(self.nodeDesc)}'." elif self.issue == CompatibilityIssue.DescriptionConflict: return "Node attributes do not match node description." elif self.issue == CompatibilityIssue.UidConflict: @@ -2213,13 +2206,12 @@ def upgrade(self): # Use upgrade method of the node description itself if available try: upgradedAttrValues = node.nodeDesc.upgradeAttributeValues(attrValues, self.version) - except Exception as e: - logging.error(f"Error in the upgrade implementation of the node: {self.name}.\n{repr(e)}") + except Exception as exc: + logging.error(f"Error in the upgrade implementation of the node: {self.name}.\n{repr(exc)}") upgradedAttrValues = attrValues if not isinstance(upgradedAttrValues, dict): - logging.error("Error in the upgrade implementation of the node: {}. The return type is incorrect.". - format(self.name)) + logging.error(f"Error in the upgrade implementation of the node: {self.name}. The return type is incorrect.") upgradedAttrValues = attrValues node.upgradeAttributeValues(upgradedAttrValues) @@ -2231,4 +2223,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 3a46500578..22e8ef4cd5 100644 --- a/meshroom/core/plugins.py +++ b/meshroom/core/plugins.py @@ -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.") @@ -679,8 +679,8 @@ def registerNode(self, nodePlugin: NodePlugin): try: self._nodePlugins[name] = nodePlugin nodePlugin.status = NodePluginStatus.LOADED - except Exception as e: - logging.error(f"NodePlugin {name} could not be loaded: {e}") + except Exception as exc: + logging.error(f"NodePlugin {name} could not be loaded: {exc}") nodePlugin.status = NodePluginStatus.LOADING_ERROR def unregisterNode(self, nodePlugin: NodePlugin): diff --git a/meshroom/core/stats.py b/meshroom/core/stats.py index fc1c7067c7..7bdc99b901 100644 --- a/meshroom/core/stats.py +++ b/meshroom/core/stats.py @@ -1,14 +1,14 @@ from collections import defaultdict -import subprocess -import logging -import psutil +import os +import platform import time import threading -import platform -import os - import xml.etree.ElementTree as ET +import subprocess +import logging +import psutil + def bytes2human(n): """ @@ -83,8 +83,8 @@ def update(self): self._addKV('vramUsage', 0) self._addKV('ioCounters', psutil.disk_io_counters()) self.updateGpu() - except Exception as e: - logging.debug(f'Failed to get statistics: "{e}".') + except Exception as exc: + logging.debug(f'Failed to get statistics: "{exc}".') def updateGpu(self): if not self.nvidia_smi: @@ -98,39 +98,39 @@ def updateGpu(self): try: self.gpuName = gpuTree.find('product_name').text - except Exception as e: - logging.debug(f'Failed to get gpuName: "{e}".') + except Exception as exc: + logging.debug(f'Failed to get gpuName: "{exc}".') pass try: gpuMemoryUsed = gpuTree.find('fb_memory_usage').find('used').text.split(" ")[0] self._addKV('gpuMemoryUsed', gpuMemoryUsed) - except Exception as e: - logging.debug(f'Failed to get gpuMemoryUsed: "{e}".') + except Exception as exc: + logging.debug(f'Failed to get gpuMemoryUsed: "{exc}".') pass try: self.gpuMemoryTotal = gpuTree.find('fb_memory_usage').find('total').text.split(" ")[0] - except Exception as e: - logging.debug(f'Failed to get gpuMemoryTotal: "{e}".') + except Exception as exc: + logging.debug(f'Failed to get gpuMemoryTotal: "{exc}".') pass try: gpuUsed = gpuTree.find('utilization').find('gpu_util').text.split(" ")[0] self._addKV('gpuUsed', gpuUsed) - except Exception as e: - logging.debug(f'Failed to get gpuUsed: "{e}".') + except Exception as exc: + logging.debug(f'Failed to get gpuUsed: "{exc}".') pass try: gpuTemperature = gpuTree.find('temperature').find('gpu_temp').text.split(" ")[0] self._addKV('gpuTemperature', gpuTemperature) - except Exception as e: - logging.debug(f'Failed to get gpuTemperature: "{e}".') + except Exception as exc: + logging.debug(f'Failed to get gpuTemperature: "{exc}".') pass - except subprocess.TimeoutExpired as e: - logging.debug(f'Timeout when retrieving information from nvidia_smi: "{e}".') + except subprocess.TimeoutExpired as exp: + logging.debug(f'Timeout when retrieving information from nvidia_smi: "{exp}".') p.kill() outs, errs = p.communicate() return - except Exception as e: - logging.debug(f'Failed to get information from nvidia_smi: "{e}".') + except Exception as exc: + logging.debug(f'Failed to get information from nvidia_smi: "{exc}".') return def toDict(self): @@ -269,16 +269,16 @@ def fromDict(self, d): self.times = [] try: self.computer.fromDict(d.get('computer', {})) - except Exception as e: - logging.debug(f'Failed while loading statistics: computer: "{e}".') + except Exception as exc: + logging.debug(f'Failed while loading statistics: computer: "{exc}".') try: self.process.fromDict(d.get('process', {})) - except Exception as e: - logging.debug(f'Failed while loading statistics: process: "{e}".') + except Exception as exc: + logging.debug(f'Failed while loading statistics: process: "{exc}".') try: self.times = d.get('times', []) - except Exception as e: - logging.debug(f'Failed while loading statistics: times: "{e}".') + except Exception as exc: + logging.debug(f'Failed while loading statistics: times: "{exc}".') bytesPerGiga = 1024. * 1024. * 1024. diff --git a/meshroom/core/submitter.py b/meshroom/core/submitter.py index a0c4f113ef..fc9996398f 100644 --- a/meshroom/core/submitter.py +++ b/meshroom/core/submitter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -from meshroom.common import BaseObject, Property import logging +from meshroom.common import BaseObject, Property logger = logging.getLogger("Submitter") logger.setLevel(logging.INFO) diff --git a/meshroom/core/taskManager.py b/meshroom/core/taskManager.py index 86a19cc79f..8fb6b7cc70 100644 --- a/meshroom/core/taskManager.py +++ b/meshroom/core/taskManager.py @@ -57,20 +57,17 @@ def run(self): continue if multiChunks: - logging.info('[{node}/{nbNodes}]({chunk}/{nbChunks}) {nodeName}'.format( - node=nId+1, nbNodes=len(self._manager._nodesToProcess), - chunk=cId+1, nbChunks=len(node.chunks), nodeName=node.nodeType)) + logging.info(f'[{nId+1}/{len(self._manager._nodesToProcess)}]({cId+1}/{len(node.chunks)}) {node.nodeType}') else: - logging.info('[{node}/{nbNodes}] {nodeName}'.format( - node=nId+1, nbNodes=len(self._manager._nodesToProcess), nodeName=node.nodeType)) + logging.info(f'[{nId+1}/{len(self._manager._nodesToProcess)}] {node.nodeType}') try: chunk.process(self.forceCompute) - except Exception as e: + except Exception as exc: if chunk.isStopped(): stopAndRestart = True break else: - logging.error(f"Error on node computation: {e}.") + logging.error(f"Error on node computation: {exc}.") nodesToRemove, _ = self._manager._graph.dfsOnDiscover(startNodes=[node], reverse=True) # remove following nodes from the task queue for n in nodesToRemove[1:]: # exclude current node @@ -212,8 +209,8 @@ def compute(self, graph: Graph = None, toNodes: list[Node] = None, forceCompute: chunksName = [node.name for node in chunksInConflict] # Warning: Syntax and terms are parsed on QML side to recognize the error # Syntax : [Context] ErrorType: ErrorMessage - msg = '[COMPUTATION] Already Submitted:\nWARNING - Some nodes are already submitted with status: ' \ - '{}\nNodes: {}'.format(', '.join(chunksStatus), ', '.join(chunksName)) + msg = f'[COMPUTATION] Already Submitted:\nWARNING - Some nodes are already submitted with status: ' \ + f'{", ".join(chunksStatus)}\nNodes: {", ".join(chunksName)}' if forceStatus: logging.warning(msg) @@ -310,9 +307,9 @@ def checkCompatibilityNodes(self, graph, nodes, context): if compatNodes: # Warning: Syntax and terms are parsed on QML side to recognize the error # Syntax : [Context] ErrorType: ErrorMessage - raise RuntimeError("[{}] Compatibility Issue:\n" - "Cannot compute because of these incompatible nodes:\n" - "{}".format(context, sorted(compatNodes))) + raise RuntimeError(f"[{context}] Compatibility Issue:\n" + f"Cannot compute because of these incompatible nodes:\n" + f"{sorted(compatNodes)}") def checkDuplicates(self, nodesToProcess, context): for node in nodesToProcess: @@ -320,12 +317,11 @@ def checkDuplicates(self, nodesToProcess, context): if duplicate in nodesToProcess: # Warning: Syntax and terms are parsed on QML side to recognize the error # Syntax : [Context] ErrorType: ErrorMessage - raise RuntimeError("[{}] Duplicates Issue:\n" - "Cannot compute because there are some duplicate nodes to process:\n\n" - "First match: '{}' and '{}'\n\n" - "There can be other duplicate nodes in the list. " - "Please, check the graph and try again.". - format(context, node.nameToLabel(node.name), node.nameToLabel(duplicate.name))) + raise RuntimeError(f"[{context}] Duplicates Issue:\n" + f"Cannot compute because there are some duplicate nodes to process:\n\n" + f"First match: '{node.nameToLabel(node.name)}' and '{node.nameToLabel(duplicate.name)}'\n\n" + f"There can be other duplicate nodes in the list. " + f"Please, check the graph and try again.") def checkNodesDependencies(self, graph, toNodes, context): """ @@ -364,16 +360,16 @@ def checkNodesDependencies(self, graph, toNodes, context): def raiseDependenciesMessage(self, context): # Warning: Syntax and terms are parsed on QML side to recognize the error # Syntax : [Context] ErrorType: ErrorMessage - raise RuntimeWarning("[{}] Unresolved dependencies:\n" - "Some nodes cannot be computed in LOCAL/submitted in EXTERN because of " - "unresolved dependencies.\n\n" - "Nodes which are ready will be processed.".format(context)) + raise RuntimeWarning(f"[{context}] Unresolved dependencies:\n" + f"Some nodes cannot be computed in LOCAL/submitted in EXTERN because of " + f"unresolved dependencies.\n\n" + f"Nodes which are ready will be processed.") def raiseImpossibleProcess(self, context): # Warning: Syntax and terms are parsed on QML side to recognize the error # Syntax : [Context] ErrorType: ErrorMessage - raise RuntimeError("[{}] Impossible Process:\n" - "There is no node able to be processed.".format(context)) + raise RuntimeError(f"[{context}] Impossible Process:\n" + f"There is no node able to be processed.") def submit(self, graph, submitter=None, toNodes=None, submitLabel="{projectName}"): """ @@ -395,11 +391,9 @@ def submit(self, graph, submitter=None, toNodes=None, submitLabel="{projectName} if sub is None: # Warning: Syntax and terms are parsed on QML side to recognize the error # Syntax : [Context] ErrorType: ErrorMessage - raise RuntimeError("[SUBMITTING] Unknown Submitter:\n" - "Unknown Submitter called '{submitter}'. Available submitters are: '{allSubmitters}'.".format( - submitter=submitter, - allSubmitters=str(meshroom.core.submitters.keys()) - )) + raise RuntimeError(f"[SUBMITTING] Unknown Submitter:\n" + f"Unknown Submitter called '{submitter}'. " + f"Available submitters are: '{str(meshroom.core.submitters.keys())}'.") # Update task manager's lists self.updateNodes() @@ -441,8 +435,8 @@ def submit(self, graph, submitter=None, toNodes=None, submitLabel="{projectName} # At the end because it raises a WarningError but should not stop processing if not allReady: self.raiseDependenciesMessage("SUBMITTING") - except Exception as e: - logging.error(f"Error on submit : {e}") + except Exception as exc: + logging.error(f"Error on submit: {exc}") def submitFromFile(self, graphFile, submitter, toNode=None, submitLabel="{projectName}"): """ diff --git a/meshroom/core/test.py b/meshroom/core/test.py index bf783a2113..32e5c6bc59 100644 --- a/meshroom/core/test.py +++ b/meshroom/core/test.py @@ -1,13 +1,13 @@ #!/usr/bin/env python +import json + from meshroom.core import pipelineTemplates, Version from meshroom.core.node import CompatibilityIssue, CompatibilityNode from meshroom.core.graphIO import GraphIO import meshroom -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". """ if not nodesAlreadyLoaded: diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 1a2bc5c981..9ed6559e68 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -7,7 +7,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 @@ -368,14 +368,14 @@ 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) """ self.processEvents() - + def showMessage(self, message, status=None, duration=5000): self._messageController.sendMessage(message, status, duration) @@ -725,7 +725,7 @@ def _getEnvironmentVariableValue(self, key: str, defaultValue: bool) -> bool: if val != True and str(val).lower() in ("0", "false", "off"): return False return True - + def _submittersList(self): """ Get the list of available submitters @@ -745,12 +745,12 @@ def _submittersList(self): "isDefault": isDefault }) return submittersList - + @Slot(str) def setDefaultSubmitter(self, name): logging.info(f"Submitter is now set to : {name}") self._defaultSubmitterName = name - + activeProjectChanged = Signal() activeProject = Property(Variant, lambda self: self._activeProject, notify=activeProjectChanged) diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index 48944abb22..d6e5ad8f5a 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -63,7 +63,7 @@ def tryAndPush(self, command): # type: (UndoCommand) -> bool try: res = command.redoImpl() - except Exception as e: + except Exception: logging.error(f"Error while trying command '{command.text()}': \n{traceback.format_exc()}") res = False if res is not False: @@ -305,7 +305,7 @@ def undoImpl(self): self.graph.attribute(self.attrName).value = self.oldValue else: self.graph.internalAttribute(self.attrName).value = self.oldValue - + class AddAttributeKeyValueCommand(GraphCommand): def __init__(self, graph, attribute, key, value, parent=None): super().__init__(graph, parent) @@ -329,7 +329,7 @@ def redoImpl(self): def undoImpl(self): if not self.keyable or self.value == self.oldValue: - return False + return False if self.graph.attribute(self.attrName) is not None: if self.oldValue is None: self.graph.attribute(self.attrName).keyValues.remove(self.key) @@ -364,7 +364,7 @@ def redoImpl(self): def undoImpl(self): if not self.keyable or self.oldValue == None: - return False + return False if self.graph.attribute(self.attrName) is not None: self.graph.attribute(self.attrName).keyValues.add(self.key, self.oldValue) else: diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py index 9095cafe55..715af2a6b5 100644 --- a/meshroom/ui/components/csvData.py +++ b/meshroom/ui/components/csvData.py @@ -76,8 +76,8 @@ def read(self): for elt in csvRows[1:]: for idx, value in enumerate(elt): dataList[idx].appendValue(value) - except Exception as e: - logging.error(f"CsvData: Failed to load file: {self._filepath}\n{e}") + except Exception as exc: + logging.error(f"CsvData: Failed to load file: {self._filepath}\n{exc}") return dataList diff --git a/meshroom/ui/components/shapes/shapeFile.py b/meshroom/ui/components/shapes/shapeFile.py index 592b9139c0..652dbf614c 100644 --- a/meshroom/ui/components/shapes/shapeFile.py +++ b/meshroom/ui/components/shapes/shapeFile.py @@ -189,10 +189,10 @@ def convertNumericStrings(obj): self._shapes.append(ShapeFile.ShapeData(name, type, properties, observations, self._shapes)) except FileNotFoundError: print("No shapes found to load.") - except json.JSONDecodeError as e: - print(f"Error decoding JSON: {e}") - except Exception as e: - print(f"Error loading shapes: {e}") + except json.JSONDecodeError as err: + print(f"Error decoding JSON: {err}") + except Exception as exc: + print(f"Error loading shapes: {exc}") self.fileChanged.emit() # Signals diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 5a2bbb86af..178097d956 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -711,10 +711,10 @@ def alignHorizontally(self): selectedNodes = self.getSelectedNodes() if len(selectedNodes) < 2: return - + # Make sure the list is correctly ordered selectedNodes = sorted(selectedNodes, key=lambda node:node.x) - + meanX, meanY = self.getMeanPosition() nodeWidth = self.layout.nodeWidth # Compute the first node X position @@ -731,7 +731,7 @@ def alignVertically(self): selectedNodes = self.getSelectedNodes() if len(selectedNodes) < 2: return - + meanX, _ = self.getMeanPosition() with self.groupedGraphModification("Align nodes vertically"): for selectedNode in selectedNodes: @@ -822,7 +822,7 @@ def duplicateNodesFrom(self, nodes: list[Node]) -> list[Node]: uniqueNodesToDuplicate = list(dict.fromkeys(nodesToDuplicate)) duplicates = self.duplicateNodes(uniqueNodesToDuplicate) return duplicates - + @Slot(Edge, result=bool) def canExpandForLoop(self, currentEdge): """ Check if the list attribute can be expanded by looking at all the edges connected to it. """ @@ -942,7 +942,7 @@ def replaceEdge(self, edge, newSrc, newDst): self.removeEdge(edge) self.addEdge(newSrc, newDst) return self._graph.edge(newDst) - + @Slot(Attribute, result=Edge) def getEdge(self, dst): return self._graph.edge(dst) @@ -1099,7 +1099,7 @@ def selectNodesByIndices( self, indices: list[int], command=QItemSelectionModel.SelectionFlag.ClearAndSelect ): """Update selection with node at given `indices` using the specified `command`. - + Args: indices: The list of indices to select. command: The selection command to use. @@ -1172,7 +1172,7 @@ def getSelectedNodesContent(self) -> str: @Slot(str, QPoint, result=list) def pasteNodes(self, serializedData: str, position: Optional[QPoint]=None) -> list[Node]: """ - Import string-serialized graph content `serializedData` in the current graph, optionally at the given + Import string-serialized graph content `serializedData` in the current graph, optionally at the given `position`. If the `serializedData` does not contain valid serialized graph data, nothing is done. @@ -1202,7 +1202,7 @@ def pasteNodes(self, serializedData: str, position: Optional[QPoint]=None) -> li logging.warning("Content is not a valid graph data.") return [] return result - + @Slot(Node, result=bool) def canComputeNode(self, node: Node) -> bool: """ Check if the node can be computed """ @@ -1213,7 +1213,7 @@ def canComputeNode(self, node: Node) -> bool: if self._graph.canComputeTopologically(node) and self._graph.canSubmitOrCompute(node) % 2 == 1: return True return False - + @Slot(Node, result=bool) def canSubmitNode(self, node: Node) -> bool: """ Check if the node can be submitted """ @@ -1224,7 +1224,7 @@ def canSubmitNode(self, node: Node) -> bool: if self._graph.canComputeTopologically(node) and self._graph.canSubmitOrCompute(node)> 1: return True return False - + undoStack = Property(QObject, lambda self: self._undoStack, constant=True) graphChanged = Signal() graph = Property(Graph, lambda self: self._graph, notify=graphChanged) diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 770cf9c518..3448e1611c 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -106,8 +106,8 @@ def _updateInitialParams(self): try: # When the viewpoint attribute has already been deleted, metadata.value becomes a PySide property (whereas a string is expected) self._metadata = json.loads(self._viewpoint.metadata.value) if isinstance(self._viewpoint.metadata.value, str) and self._viewpoint.metadata.value else None - except Exception as e: - logging.warning(f"Failed to parse Viewpoint metadata: '{e}', '{str(self._viewpoint.metadata.value)}'") + except Exception as exc: + logging.warning(f"Failed to parse Viewpoint metadata: '{exc}', '{str(self._viewpoint.metadata.value)}'") self._metadata = {} if not self._metadata: self._metadata = {} @@ -132,7 +132,7 @@ def _updateUndistortedImageParams(self): try: if self._activeNode_ExportAnimatedCamera and self._activeNode_ExportAnimatedCamera.node: self._undistortedImagePath = FilepathHelper.resolve(FilepathHelper, self._activeNode_ExportAnimatedCamera.node.outputImages.value, self._viewpoint) - self._principalPointCorrected = self._activeNode_ExportAnimatedCamera.node.correctPrincipalPoint.value + self._principalPointCorrected = self._activeNode_ExportAnimatedCamera.node.correctPrincipalPoint.value elif self._activeNode_PrepareDenseScene and self._activeNode_PrepareDenseScene.node: self._undistortedImagePath = FilepathHelper.resolve(FilepathHelper, self._activeNode_PrepareDenseScene.node.undistorted.value, self._viewpoint) self._principalPointCorrected = False @@ -142,7 +142,7 @@ def _updateUndistortedImageParams(self): else: self._undistortedImagePath = '' self._principalPointCorrected = False - except Exception as e: + except Exception: self._undistortedImagePath = '' self._principalPointCorrected = False logging.warning("Failed to retrieve undistorted images path.") @@ -832,8 +832,8 @@ def importImagesSync(self, images, cameraInit): """ Add the given list of images to the Reconstruction. """ try: self.buildIntrinsics(cameraInit, images) - except Exception as e: - self.importImagesFailed.emit(str(e)) + except Exception as exc: + self.importImagesFailed.emit(str(exc)) @Slot() def onImportImagesFailed(self, msg): @@ -878,8 +878,8 @@ def buildIntrinsics(self, cameraInit, additionalViews, rebuild=False): self.setBuildingIntrinsics(True) # Retrieve the list of updated viewpoints and intrinsics views, intrinsics = cameraInitCopy.nodeDesc.buildIntrinsics(cameraInitCopy, additionalViews) - except Exception as e: - logging.error(f"Error while building intrinsics: {e}") + except Exception as exc: + logging.error(f"Error while building intrinsics: {exc}") raise finally: # Delete the duplicate @@ -939,11 +939,11 @@ def setBuildingIntrinsics(self, value): buildingIntrinsics = Property(bool, lambda self: self._buildingIntrinsics, notify=buildingIntrinsicsChanged) displayedAttr2DChanged = Signal() - displayedAttr2D = makeProperty(QObject, "_displayedAttr2D", displayedAttr2DChanged) + displayedAttr2D = makeProperty(QObject, "_displayedAttr2D", displayedAttr2DChanged) - displayedAttrs3DChanged = Signal() + displayedAttrs3DChanged = Signal() displayedAttrs3D = Property(QObject, lambda self: self._displayedAttrs3D, notify=displayedAttrs3DChanged) - + pluginsReloaded = Signal(list) @Slot(QObject) @@ -1169,7 +1169,7 @@ def setCurrentViewPath(self, path): @Slot(str, result="QVariantList") def evaluateMathExpression(self, expr): - """ Evaluate a mathematical expression and return the result as a string + """ Evaluate a mathematical expression and return the result as a string Returns a list of 2 values : - the result value - a boolean that indicate if an error occured diff --git a/tests/test_attributeKeyValues.py b/tests/test_attributeKeyValues.py index b1ed654bd6..bba9f141cc 100644 --- a/tests/test_attributeKeyValues.py +++ b/tests/test_attributeKeyValues.py @@ -61,7 +61,7 @@ def test_initialization(self): # Check attribute pairs empty assert nodeA.keyableBool.isDefault - assert nodeA.keyableInt.isDefault + assert nodeA.keyableInt.isDefault assert nodeA.keyableFloat.isDefault # Check attribute description value @@ -182,7 +182,7 @@ def test_multipleKeys(self): assert nodeA.keyableFloat.keyValues.hasKey("2") == True # Check attributes values at key "0", should be default value - assert nodeA.keyableBool.keyValues.getValueAtKeyOrDefault("0") == False + assert nodeA.keyableBool.keyValues.getValueAtKeyOrDefault("0") == False assert nodeA.keyableInt.keyValues.getValueAtKeyOrDefault("0") == 1 assert nodeA.keyableFloat.keyValues.getValueAtKeyOrDefault("0") == 1.1 diff --git a/tests/test_attributeShape.py b/tests/test_attributeShape.py index ebb37fde7b..7b846d8dc2 100644 --- a/tests/test_attributeShape.py +++ b/tests/test_attributeShape.py @@ -158,10 +158,10 @@ def test_staticShapeGeometry(self): graph = Graph("") node = graph.addNewNode(NodeWithShapeAttributes.__name__) - observationPoint = {"x" : 1, "y" : 1} - observationLine = {"a" : {"x" : 1, "y" : 1}, "b" : {"x" : 2, "y" : 2}} - observationRectangle = {"center" : {"x" : 10, "y" : 10}, "size" : {"width" : 20, "height" : 20}} - observationCircle = {"center" : {"x" : 10, "y" : 10}, "radius" : 20} + observationPoint = {"x": 1, "y": 1} + observationLine = {"a": {"x": 1, "y": 1}, "b": {"x": 2, "y": 2}} + observationRectangle = {"center": {"x": 10, "y": 10}, "size": {"width": 20, "height": 20}} + observationCircle = {"center": {"x": 10, "y": 10}, "radius": 20} # Check static shape has observation, should be true (default) assert node.point.geometry.hasObservation("0") @@ -195,15 +195,15 @@ def test_staticShapeGeometry(self): assert node.circle.geometry.getObservation("0") == observationCircle # Update static shape observation - node.point.geometry.setObservation("0", {"x" : 2}) - node.line.geometry.setObservation("0", {"a" : {"x" : 2, "y": 2}}) - node.rectangle.geometry.setObservation("0", {"center" : {"x" : 20, "y" : 20}}) - node.circle.geometry.setObservation("0", {"radius" : 40}) + node.point.geometry.setObservation("0", {"x": 2}) + node.line.geometry.setObservation("0", {"a": {"x": 2, "y": 2}}) + node.rectangle.geometry.setObservation("0", {"center": {"x": 20, "y": 20}}) + node.circle.geometry.setObservation("0", {"radius": 40}) # Check static shape get observation, should be updated observation assert node.point.geometry.getObservation("0").get("x") == 2 - assert node.line.geometry.getObservation("0").get("a") == {"x" : 2, "y": 2} - assert node.rectangle.geometry.getObservation("0").get("center") == {"x" : 20, "y" : 20} + assert node.line.geometry.getObservation("0").get("a") == {"x": 2, "y": 2} + assert node.rectangle.geometry.getObservation("0").get("center") == {"x": 20, "y": 20} assert node.circle.geometry.getObservation("0").get("radius") == 40 # Reset static shape geometry @@ -223,10 +223,10 @@ def test_keyableShapeGeometry(self): graph = Graph("") node = graph.addNewNode(NodeWithShapeAttributes.__name__) - observationPoint = {"x" : 1, "y" : 1} - observationLine = {"a" : {"x" : 1, "y" : 1}, "b" : {"x" : 2, "y" : 2}} - observationRectangle = {"center" : {"x" : 10, "y" : 10}, "size" : {"width" : 20, "height" : 20}} - observationCircle = {"center" : {"x" : 10, "y" : 10}, "radius" : 20} + observationPoint = {"x": 1, "y": 1} + observationLine = {"a": {"x": 1, "y": 1}, "b": {"x": 2, "y": 2}} + observationRectangle = {"center": {"x": 10, "y": 10}, "size": {"width": 20, "height": 20}} + observationCircle = {"center": {"x": 10, "y": 10}, "radius": 20} # Check keyable shape has observation at key "0", should be false assert not node.keyablePoint.geometry.hasObservation("0") @@ -281,15 +281,15 @@ def test_keyableShapeGeometry(self): assert node.keyableCircle.geometry.getObservation("0") == observationCircle # Update keyable shape observation at key "1" - node.keyablePoint.geometry.setObservation("1", {"x" : 2}) - node.keyableLine.geometry.setObservation("1", {"a" : {"x" : 2, "y": 2}}) - node.keyableRectangle.geometry.setObservation("1", {"center" : {"x" : 20, "y" : 20}}) - node.keyableCircle.geometry.setObservation("1", {"radius" : 40}) + node.keyablePoint.geometry.setObservation("1", {"x": 2}) + node.keyableLine.geometry.setObservation("1", {"a": {"x": 2, "y": 2}}) + node.keyableRectangle.geometry.setObservation("1", {"center": {"x": 20, "y": 20}}) + node.keyableCircle.geometry.setObservation("1", {"radius": 40}) # Check keyable shape get observation at key "1", should be updated observation assert node.keyablePoint.geometry.getObservation("1").get("x") == 2 - assert node.keyableLine.geometry.getObservation("1").get("a") == {"x" : 2, "y": 2} - assert node.keyableRectangle.geometry.getObservation("1").get("center") == {"x" : 20, "y" : 20} + assert node.keyableLine.geometry.getObservation("1").get("a") == {"x": 2, "y": 2} + assert node.keyableRectangle.geometry.getObservation("1").get("center") == {"x": 20, "y": 20} assert node.keyableCircle.geometry.getObservation("1").get("radius") == 40 # Remove keyable shape observation at key "0" @@ -326,8 +326,8 @@ def test_shapeList(self): graph = Graph("") node = graph.addNewNode(NodeWithShapeAttributes.__name__) - pointValue = {"userName" : "testPoint", "userColor" : "#fff", "geometry" : {"x" : 1, "y" : 1}} - keyablePointValue = {"userName" : "testKeyablePoint", "userColor" : "#fff", "geometry" : {}} + pointValue = {"userName": "testPoint", "userColor": "#fff", "geometry": {"x": 1, "y": 1}} + keyablePointValue = {"userName": "testKeyablePoint", "userColor": "#fff", "geometry": {}} # Check visibility assert node.pointList.isVisible @@ -368,7 +368,7 @@ def test_shapeList(self): # Reset shape lists node.pointList.resetToDefaultValue() node.keyablePointList.resetToDefaultValue() - + # Check number of shapes, should be 0 (no shape) assert len(node.pointList) == 0 assert len(node.keyablePointList) == 0 @@ -379,8 +379,8 @@ def test_linkAttribute(self): nodeA = graph.addNewNode(NodeWithShapeAttributes.__name__) nodeB = graph.addNewNode(NodeWithShapeAttributes.__name__) - pointGeometryValue = {"x" : 1, "y" : 1} - pointValue = {"userName" : "testPoint", "userColor" : "#fff", "geometry" : pointGeometryValue} + pointGeometryValue = {"x": 1, "y": 1} + pointValue = {"userName": "testPoint", "userColor": "#fff", "geometry": pointGeometryValue} # Add link: # nodeB.pointList is a link for nodeA.pointList @@ -411,8 +411,8 @@ def test_linkAttribute(self): assert nodeB.pointList.at(2).geometry.getValueAsDict() == pointGeometryValue # Update nodeA.point and nodeA.pointList[1] geometry - nodeA.point.geometry.setObservation("0", {"x" : 2}) - nodeA.pointList.at(1).geometry.setObservation("0", {"x" : 2}) + nodeA.point.geometry.setObservation("0", {"x": 2}) + nodeA.pointList.at(1).geometry.setObservation("0", {"x": 2}) # Check nodeB second shape geometry assert nodeB.point.geometry.getObservation("0").get("x") == 2 @@ -427,14 +427,14 @@ def test_exportDict(self): graph = Graph("") node = graph.addNewNode(NodeWithShapeAttributes.__name__) - observationPoint = {"x" : 1, "y" : 1} - observationLine = {"a" : {"x" : 1, "y" : 1}, "b" : {"x" : 2, "y" : 2}} - observationRectangle = {"center" : {"x" : 10, "y" : 10}, "size" : {"width" : 20, "height" : 20}} - observationCircle = {"center" : {"x" : 10, "y" : 10}, "radius" : 20} + observationPoint = {"x": 1, "y": 1} + observationLine = {"a": {"x": 1, "y": 1}, "b": {"x": 2, "y": 2}} + observationRectangle = {"center": {"x": 10, "y": 10}, "size": {"width": 20, "height": 20}} + observationCircle = {"center": {"x": 10, "y": 10}, "radius": 20} - pointValue = {"userName" : "testPoint", "userColor" : "#fff", "geometry" : observationPoint} - keyablePointGeometryValue = {"x" : {"0" : observationPoint.get("x")}, "y" : {"0" : observationPoint.get("y")}} - keyablePointValue = {"userName" : "testKeyablePoint", "userColor" : "#fff", "geometry" : keyablePointGeometryValue} + pointValue = {"userName": "testPoint", "userColor": "#fff", "geometry": observationPoint} + keyablePointGeometryValue = {"x": {"0": observationPoint.get("x")}, "y": {"0": observationPoint.get("y")}} + keyablePointValue = {"userName": "testKeyablePoint", "userColor": "#fff", "geometry": keyablePointGeometryValue} # Check uninitialized shape attribute # Shape list attribute should be empty list @@ -443,49 +443,49 @@ def test_exportDict(self): assert node.pointList.getShapesAsDict() == [] assert node.keyablePointList.getShapesAsDict() == [] # Static shape attribute should be default - assert node.point.geometry.getValueAsDict() == {"x" : -1, "y" : -1} - assert node.line.geometry.getValueAsDict() == {"a" : {"x" : -1, "y" : -1}, "b" : {"x" : -1, "y" : -1}} - assert node.rectangle.geometry.getValueAsDict() == {"center" : {"x" : -1, "y" : -1}, "size" : {"width" : -1, "height" : -1}} - assert node.circle.geometry.getValueAsDict() == {"center" : {"x" : -1, "y" : -1}, "radius" : -1} - assert node.point.getShapeAsDict() == {"name" : node.point.rootName, - "type" : node.point.type, - "properties" : {"color" : node.point.userColor, "x" : -1, "y" : -1}} - assert node.line.getShapeAsDict() == {"name" : node.line.rootName, - "type" : node.line.type, - "properties" : {"color" : node.line.userColor, "a" : {"x" : -1, "y" : -1}, "b" : {"x" : -1, "y" : -1}}} - assert node.rectangle.getShapeAsDict() == {"name" : node.rectangle.rootName, - "type" : node.rectangle.type, - "properties" : {"color" : node.rectangle.userColor, "center" : {"x" : -1, "y" : -1}, "size" : {"width" : -1, "height" : -1}}} - assert node.circle.getShapeAsDict() == {"name" : node.circle.rootName, - "type" : node.circle.type, - "properties" : {"color" : node.circle.userColor, "center" : {"x" : -1, "y" : -1}, "radius" : -1}} + assert node.point.geometry.getValueAsDict() == {"x": -1, "y": -1} + assert node.line.geometry.getValueAsDict() == {"a": {"x": -1, "y": -1}, "b": {"x": -1, "y": -1}} + assert node.rectangle.geometry.getValueAsDict() == {"center": {"x": -1, "y": -1}, "size": {"width": -1, "height": -1}} + assert node.circle.geometry.getValueAsDict() == {"center": {"x": -1, "y": -1}, "radius": -1} + assert node.point.getShapeAsDict() == {"name": node.point.rootName, + "type": node.point.type, + "properties": {"color": node.point.userColor, "x": -1, "y": -1}} + assert node.line.getShapeAsDict() == {"name": node.line.rootName, + "type": node.line.type, + "properties": {"color": node.line.userColor, "a": {"x": -1, "y": -1}, "b": {"x": -1, "y": -1}}} + assert node.rectangle.getShapeAsDict() == {"name": node.rectangle.rootName, + "type": node.rectangle.type, + "properties": {"color": node.rectangle.userColor, "center": {"x": -1, "y": -1}, "size": {"width": -1, "height": -1}}} + assert node.circle.getShapeAsDict() == {"name": node.circle.rootName, + "type": node.circle.type, + "properties": {"color": node.circle.userColor, "center": {"x": -1, "y": -1}, "radius": -1}} # Keyable shape attribute should be empty dict assert node.keyablePoint.geometry.getValueAsDict() == {} assert node.keyableLine.geometry.getValueAsDict() == {} assert node.keyableRectangle.geometry.getValueAsDict() == {} assert node.keyableCircle.geometry.getValueAsDict() == {} - assert node.keyablePoint.getShapeAsDict() == {"name" : node.keyablePoint.rootName, - "type" : node.keyablePoint.type, - "properties" : {"color" : node.keyablePoint.userColor}, - "observations" : {}} - assert node.keyableLine.getShapeAsDict() == {"name" : node.keyableLine.rootName, - "type" : node.keyableLine.type, - "properties" : {"color" : node.keyableLine.userColor}, - "observations" : {}} - assert node.keyableRectangle.getShapeAsDict() == {"name" : node.keyableRectangle.rootName, - "type" : node.keyableRectangle.type, - "properties" : {"color" : node.keyableRectangle.userColor}, - "observations" : {}} - assert node.keyableCircle.getShapeAsDict() == {"name" : node.keyableCircle.rootName, - "type" : node.keyableCircle.type, - "properties" : {"color" : node.keyableCircle.userColor}, - "observations" : {}} + assert node.keyablePoint.getShapeAsDict() == {"name": node.keyablePoint.rootName, + "type": node.keyablePoint.type, + "properties": {"color": node.keyablePoint.userColor}, + "observations": {}} + assert node.keyableLine.getShapeAsDict() == {"name": node.keyableLine.rootName, + "type": node.keyableLine.type, + "properties": {"color": node.keyableLine.userColor}, + "observations": {}} + assert node.keyableRectangle.getShapeAsDict() == {"name": node.keyableRectangle.rootName, + "type": node.keyableRectangle.type, + "properties": {"color": node.keyableRectangle.userColor}, + "observations": {}} + assert node.keyableCircle.getShapeAsDict() == {"name": node.keyableCircle.rootName, + "type": node.keyableCircle.type, + "properties": {"color": node.keyableCircle.userColor}, + "observations": {}} # Add one shape with an observation node.pointList.append(pointValue) node.keyablePointList.append(keyablePointValue) - # Add one observation + # Add one observation node.point.geometry.setObservation("0", observationPoint) node.keyablePoint.geometry.setObservation("0", observationPoint) node.line.geometry.setObservation("0", observationLine) @@ -498,24 +498,24 @@ def test_exportDict(self): # Check shape attribute # Shape list attribute should be empty dict assert node.pointList.getGeometriesAsDict() == [observationPoint] - assert node.keyablePointList.getGeometriesAsDict() == [{"0" : observationPoint}] - assert node.pointList.getShapesAsDict()[0].get("properties") == {"color" : pointValue.get("userColor")} | observationPoint - assert node.keyablePointList.getShapesAsDict()[0].get("observations") == {"0" : observationPoint} + assert node.keyablePointList.getGeometriesAsDict() == [{"0": observationPoint}] + assert node.pointList.getShapesAsDict()[0].get("properties") == {"color": pointValue.get("userColor")} | observationPoint + assert node.keyablePointList.getShapesAsDict()[0].get("observations") == {"0": observationPoint} # Not keyable shape attribute should be default assert node.point.geometry.getValueAsDict() == observationPoint assert node.line.geometry.getValueAsDict() == observationLine assert node.rectangle.geometry.getValueAsDict() == observationRectangle assert node.circle.geometry.getValueAsDict() == observationCircle - assert node.point.getShapeAsDict().get("properties") == {"color" : node.point.userColor} | observationPoint - assert node.line.getShapeAsDict().get("properties") == {"color" : node.line.userColor} | observationLine - assert node.rectangle.getShapeAsDict().get("properties") == {"color" : node.rectangle.userColor} | observationRectangle - assert node.circle.getShapeAsDict().get("properties") == {"color" : node.circle.userColor} | observationCircle + assert node.point.getShapeAsDict().get("properties") == {"color": node.point.userColor} | observationPoint + assert node.line.getShapeAsDict().get("properties") == {"color": node.line.userColor} | observationLine + assert node.rectangle.getShapeAsDict().get("properties") == {"color": node.rectangle.userColor} | observationRectangle + assert node.circle.getShapeAsDict().get("properties") == {"color": node.circle.userColor} | observationCircle # Keyable shape attribute should be empty dict - assert node.keyablePoint.geometry.getValueAsDict() == {"0" : observationPoint} - assert node.keyableLine.geometry.getValueAsDict() == {"0" : observationLine} - assert node.keyableRectangle.geometry.getValueAsDict() == {"0" : observationRectangle} - assert node.keyableCircle.geometry.getValueAsDict() == {"0" : observationCircle} - assert node.keyablePoint.getShapeAsDict().get("observations") == {"0" : observationPoint} - assert node.keyableLine.getShapeAsDict().get("observations") == {"0" : observationLine} - assert node.keyableRectangle.getShapeAsDict().get("observations") == {"0" : observationRectangle} - assert node.keyableCircle.getShapeAsDict().get("observations") == {"0" : observationCircle} \ No newline at end of file + assert node.keyablePoint.geometry.getValueAsDict() == {"0": observationPoint} + assert node.keyableLine.geometry.getValueAsDict() == {"0": observationLine} + assert node.keyableRectangle.geometry.getValueAsDict() == {"0": observationRectangle} + assert node.keyableCircle.geometry.getValueAsDict() == {"0": observationCircle} + assert node.keyablePoint.getShapeAsDict().get("observations") == {"0": observationPoint} + assert node.keyableLine.getShapeAsDict().get("observations") == {"0": observationLine} + assert node.keyableRectangle.getShapeAsDict().get("observations") == {"0": observationRectangle} + assert node.keyableCircle.getShapeAsDict().get("observations") == {"0": observationCircle} \ No newline at end of file