Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 75 additions & 35 deletions meshroom/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import copy
import os
import re
from typing import Optional
import weakref
import types
import logging
Expand All @@ -11,6 +12,7 @@
from string import Template
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
from meshroom.core import desc, hashValue
from meshroom.core.exception import InvalidEdgeError


def attributeFactory(description, value, isOutput, node, root=None, parent=None):
Expand Down Expand Up @@ -72,6 +74,7 @@
# invalidation value for output attributes
self._invalidationValue = ""

self._linkExpression: Optional[str] = None
self._value = None
self.initValue()

Expand Down Expand Up @@ -191,9 +194,9 @@
if self._value == value:
return

if isinstance(value, Attribute) or Attribute.isLinkExpression(value):
# if we set a link to another attribute
self._value = value
if self._handleLinkValue(value):
return

elif isinstance(value, types.FunctionType):
# evaluate the function
self._value = value(self)
Expand All @@ -218,6 +221,27 @@
self.valueChanged.emit()
self.validValueChanged.emit()

def _handleLinkValue(self, value) -> bool:
"""
Handle assignment of a link if `value` is a serialized link expression or in-memory Attribute reference.

Returns: Whether the value has been handled as a link, False otherwise.
"""
isAttribute = isinstance(value, Attribute)
isLinkExpression = Attribute.isLinkExpression(value)

if not isAttribute and not isLinkExpression:
return False

if isAttribute:
self._linkExpression = value.asLinkExpr()
# If the value is a direct reference to an attribute, it can be directly converted to an edge as
# the source attribute already exists in memory.
self._applyExpr()
elif isLinkExpression:
self._linkExpression = value
return True

@Slot()
def _onValueChanged(self):
self.node._onAttributeChanged(self)
Expand Down Expand Up @@ -329,26 +353,30 @@
this function convert the expression into a real edge in the graph
and clear the string value.
"""
v = self._value
g = self.node.graph
if not g:
if not self.isInput or not self._linkExpression:
return
if isinstance(v, Attribute):
g.addEdge(v, self)
self.resetToDefaultValue()
elif self.isInput and Attribute.isLinkExpression(v):
# value is a link to another attribute
link = v[1:-1]
linkNodeName, linkAttrName = link.split('.')
try:
node = g.node(linkNodeName)
if not node:
raise KeyError(f"Node '{linkNodeName}' not found")
g.addEdge(node.attribute(linkAttrName), self)
except KeyError as err:
logging.warning('Connect Attribute from Expression failed.')
logging.warning('Expression: "{exp}"\nError: "{err}".'.format(exp=v, err=err))
self.resetToDefaultValue()

if not (graph := self.node.graph):
return

link = self._linkExpression[1:-1]
linkNodeName, linkAttrName = link.split(".")
try:
node = graph.node(linkNodeName)
if node is None:
raise InvalidEdgeError(self.fullNameToNode, link, "Source node does not exist")
attr = node.attribute(linkAttrName)
if attr is None:
raise InvalidEdgeError(self.fullNameToNode, link, "Source attribute does not exist")

Check warning on line 370 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L370

Added line #L370 was not covered by tests
graph.addEdge(attr, self)
except InvalidEdgeError as err:
logging.warning(err)
except Exception as err:
logging.warning("Unexpected error happened during edge creation")
logging.warning(f"Expression '{self._linkExpression}': {err}")

Check warning on line 376 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L374-L376

Added lines #L374 - L376 were not covered by tests

self._linkExpression = None
self.resetToDefaultValue()

def getExportValue(self):
if self.isLink:
Expand Down Expand Up @@ -543,9 +571,8 @@
def _set_value(self, value):
if self.node.graph:
self.remove(0, len(self))
# Link to another attribute
if isinstance(value, ListAttribute) or Attribute.isLinkExpression(value):
self._value = value
if self._handleLinkValue(value):
return
# New value
else:
# During initialization self._value may not be set
Expand All @@ -556,10 +583,10 @@
self.requestGraphUpdate()

def upgradeValue(self, exportedValues):
if self._handleLinkValue(exportedValues):
return

Check warning on line 587 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L587

Added line #L587 was not covered by tests

if not isinstance(exportedValues, list):
if isinstance(exportedValues, ListAttribute) or Attribute.isLinkExpression(exportedValues):
self._set_value(exportedValues)
return
raise RuntimeError("ListAttribute.upgradeValue: the given value is of type " +
str(type(exportedValues)) + " but a 'list' is expected.")

Expand Down Expand Up @@ -620,10 +647,8 @@
return super(ListAttribute, self).uid()

def _applyExpr(self):
if not self.node.graph:
return
if isinstance(self._value, ListAttribute) or Attribute.isLinkExpression(self._value):
super(ListAttribute, self)._applyExpr()
if self._linkExpression:
super()._applyExpr()
else:
for value in self._value:
value._applyExpr()
Expand Down Expand Up @@ -690,6 +715,9 @@
raise AttributeError(key)

def _set_value(self, exportedValue):
if self._handleLinkValue(exportedValue):
return

Check warning on line 719 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L719

Added line #L719 was not covered by tests

value = self.validateValue(exportedValue)
if isinstance(value, dict):
# set individual child attribute values
Expand All @@ -704,6 +732,8 @@
raise AttributeError("Failed to set on GroupAttribute: {}".format(str(value)))

def upgradeValue(self, exportedValue):
if self._handleLinkValue(exportedValue):
return

Check warning on line 736 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L736

Added line #L736 was not covered by tests
value = self.validateValue(exportedValue)
if isinstance(value, dict):
# set individual child attribute values
Expand Down Expand Up @@ -748,20 +778,30 @@
return None

def uid(self):
if self.isLink:
return super().uid()

Check warning on line 782 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L782

Added line #L782 was not covered by tests

uids = []
for k, v in self._value.items():
if v.enabled and v.invalidate:
uids.append(v.uid())
return hashValue(uids)

def _applyExpr(self):
for value in self._value:
value._applyExpr()
if self._linkExpression:
super()._applyExpr()

Check warning on line 792 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L792

Added line #L792 was not covered by tests
else:
for value in self._value:
value._applyExpr()

def getExportValue(self):
return {key: attr.getExportValue() for key, attr in self._value.objects.items()}
if linkParam := self.getLinkParam():
return linkParam.asLinkExpr()

Check warning on line 799 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L799

Added line #L799 was not covered by tests
return {key: attr.getExportValue() for key, attr in self._value.items()}

def _isDefault(self):
if linkParam := self.getLinkParam():
return linkParam._isDefault()

Check warning on line 804 in meshroom/core/attribute.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/attribute.py#L804

Added line #L804 was not covered by tests
return all(v.isDefault for v in self._value)

def defaultValue(self):
Expand Down
7 changes: 7 additions & 0 deletions meshroom/core/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ class GraphException(MeshroomException):
pass


class InvalidEdgeError(GraphException):
"""Raised when an edge between two attributes cannot be created."""

def __init__(self, srcAttrName: str, dstAttrName: str, msg: str) -> None:
super().__init__(f"Failed to connect {srcAttrName}->{dstAttrName}: {msg}")


class GraphCompatibilityError(GraphException):
"""
Raised when node compatibility issues occur when loading a graph.
Expand Down
64 changes: 32 additions & 32 deletions meshroom/core/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from meshroom.common import BaseObject, DictModel, Slot, Signal, Property
from meshroom.core import Version
from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute
from meshroom.core.exception import GraphCompatibilityError, StopGraphVisit, StopBranchVisit
from meshroom.core.exception import GraphCompatibilityError, InvalidEdgeError, StopGraphVisit, StopBranchVisit
from meshroom.core.graphIO import GraphIO, GraphSerializer, TemplateGraphSerializer, PartialGraphSerializer
from meshroom.core.node import BaseNode, Status, Node, CompatibilityNode
from meshroom.core.nodeFactory import nodeFactory
Expand Down Expand Up @@ -485,41 +485,38 @@
node._applyExpr()
return node

def copyNode(self, srcNode, withEdges=False):
def copyNode(self, srcNode: Node, withEdges: bool=False):
"""
Get a copy instance of a node outside the graph.

Args:
srcNode (Node): the node to copy
withEdges (bool): whether to copy edges
srcNode: the node to copy
withEdges: whether to copy edges

Returns:
Node, dict: the created node instance,
a dictionary of linked attributes with their original value (empty if withEdges is True)
The created node instance and the mapping of skipped edge per attribute (always empty if `withEdges` is True).
"""
def _removeLinkExpressions(attribute: Attribute, removed: dict[Attribute, str]):
"""Recursively remove link expressions from the given root `attribute`."""
# Link expressions are only stored on input attributes.
if attribute.isOutput:
return

if attribute._linkExpression:
removed[attribute] = attribute._linkExpression
attribute._linkExpression = None
elif isinstance(attribute, (ListAttribute, GroupAttribute)):
for child in attribute.value:
_removeLinkExpressions(child, removed)

Check warning on line 510 in meshroom/core/graph.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/graph.py#L509-L510

Added lines #L509 - L510 were not covered by tests

with GraphModification(self):
# create a new node of the same type and with the same attributes values
# keep links as-is so that CompatibilityNodes attributes can be created with correct automatic description
# (File params for link expressions)
node = nodeFactory(srcNode.toDict(), srcNode.nodeType) # use nodeType as name
# skip edges: filter out attributes which are links by resetting default values
node = nodeFactory(srcNode.toDict(), name=srcNode.nodeType)

skippedEdges = {}
if not withEdges:
for n, attr in node.attributes.items():
if attr.isOutput:
# edges are declared in input with an expression linking
# to another param (which could be an output)
continue
# find top-level links
if Attribute.isLinkExpression(attr.value):
skippedEdges[attr] = attr.value
attr.resetToDefaultValue()
# find links in ListAttribute children
elif isinstance(attr, (ListAttribute, GroupAttribute)):
for child in attr.value:
if Attribute.isLinkExpression(child.value):
skippedEdges[child] = child.value
child.resetToDefaultValue()
for _, attr in node.attributes.items():
_removeLinkExpressions(attr, skippedEdges)

return node, skippedEdges

def duplicateNodes(self, srcNodes):
Expand Down Expand Up @@ -850,13 +847,16 @@
return set(self._nodes) - nodesWithInputLink

@changeTopology
def addEdge(self, srcAttr, dstAttr):
assert isinstance(srcAttr, Attribute)
assert isinstance(dstAttr, Attribute)
if srcAttr.node.graph != self or dstAttr.node.graph != self:
raise RuntimeError('The attributes of the edge should be part of a common graph.')
def addEdge(self, srcAttr: Attribute, dstAttr: Attribute):
if not (srcAttr.node.graph == dstAttr.node.graph == self):
raise InvalidEdgeError(

Check warning on line 852 in meshroom/core/graph.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/graph.py#L852

Added line #L852 was not covered by tests
srcAttr.fullNameToGraph, dstAttr.fullNameToGraph, "Attributes do not belong to this Graph"
)
if dstAttr in self.edges.keys():
raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.getFullNameToNode()))
raise InvalidEdgeError(

Check warning on line 856 in meshroom/core/graph.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/graph.py#L856

Added line #L856 was not covered by tests
srcAttr.fullNameToNode, dstAttr.fullNameToNode, "Destination is already connected"
)

edge = Edge(srcAttr, dstAttr)
self.edges.add(edge)
self.markNodesDirty(dstAttr.node)
Expand Down
2 changes: 1 addition & 1 deletion meshroom/ui/qml/GraphEditor/AttributeItemDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ RowLayout {
var obj = cpt.createObject(groupItem,
{
'model': Qt.binding(function() { return attribute.value }),
'readOnly': Qt.binding(function() { return root.readOnly }),
'readOnly': Qt.binding(function() { return !root.editable }),
'labelWidth': 100, // Reduce label width for children (space gain)
'objectsHideable': Qt.binding(function() { return root.objectsHideable }),
'filterText': Qt.binding(function() { return root.filterText }),
Expand Down