mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 18:27:23 +02:00
[core] Simplify handling of link assignment by value on Attribute
Centralize the handling of link assigment by value in a dedicated private method. Store link assignment by value in a separate member variable `_linkExpression`, to avoid manipulating a internal `_value` that can represent different concepts (value VS serialized edge expression).
This commit is contained in:
parent
8ee7b50204
commit
6f0542c07e
3 changed files with 96 additions and 64 deletions
|
@ -3,6 +3,7 @@
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from typing import Optional
|
||||||
import weakref
|
import weakref
|
||||||
import types
|
import types
|
||||||
import logging
|
import logging
|
||||||
|
@ -11,6 +12,7 @@ from collections.abc import Iterable, Sequence
|
||||||
from string import Template
|
from string import Template
|
||||||
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
|
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
|
||||||
from meshroom.core import desc, hashValue
|
from meshroom.core import desc, hashValue
|
||||||
|
from meshroom.core.exception import InvalidEdgeError
|
||||||
|
|
||||||
|
|
||||||
def attributeFactory(description, value, isOutput, node, root=None, parent=None):
|
def attributeFactory(description, value, isOutput, node, root=None, parent=None):
|
||||||
|
@ -72,6 +74,7 @@ class Attribute(BaseObject):
|
||||||
# invalidation value for output attributes
|
# invalidation value for output attributes
|
||||||
self._invalidationValue = ""
|
self._invalidationValue = ""
|
||||||
|
|
||||||
|
self._linkExpression: Optional[str] = None
|
||||||
self._value = None
|
self._value = None
|
||||||
self.initValue()
|
self.initValue()
|
||||||
|
|
||||||
|
@ -191,9 +194,9 @@ class Attribute(BaseObject):
|
||||||
if self._value == value:
|
if self._value == value:
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(value, Attribute) or Attribute.isLinkExpression(value):
|
if self._handleLinkValue(value):
|
||||||
# if we set a link to another attribute
|
return
|
||||||
self._value = value
|
|
||||||
elif isinstance(value, types.FunctionType):
|
elif isinstance(value, types.FunctionType):
|
||||||
# evaluate the function
|
# evaluate the function
|
||||||
self._value = value(self)
|
self._value = value(self)
|
||||||
|
@ -218,6 +221,27 @@ class Attribute(BaseObject):
|
||||||
self.valueChanged.emit()
|
self.valueChanged.emit()
|
||||||
self.validValueChanged.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()
|
@Slot()
|
||||||
def _onValueChanged(self):
|
def _onValueChanged(self):
|
||||||
self.node._onAttributeChanged(self)
|
self.node._onAttributeChanged(self)
|
||||||
|
@ -329,26 +353,30 @@ class Attribute(BaseObject):
|
||||||
this function convert the expression into a real edge in the graph
|
this function convert the expression into a real edge in the graph
|
||||||
and clear the string value.
|
and clear the string value.
|
||||||
"""
|
"""
|
||||||
v = self._value
|
if not self.isInput or not self._linkExpression:
|
||||||
g = self.node.graph
|
|
||||||
if not g:
|
|
||||||
return
|
return
|
||||||
if isinstance(v, Attribute):
|
|
||||||
g.addEdge(v, self)
|
if not (graph := self.node.graph):
|
||||||
self.resetToDefaultValue()
|
return
|
||||||
elif self.isInput and Attribute.isLinkExpression(v):
|
|
||||||
# value is a link to another attribute
|
link = self._linkExpression[1:-1]
|
||||||
link = v[1:-1]
|
linkNodeName, linkAttrName = link.split(".")
|
||||||
linkNodeName, linkAttrName = link.split('.')
|
try:
|
||||||
try:
|
node = graph.node(linkNodeName)
|
||||||
node = g.node(linkNodeName)
|
if node is None:
|
||||||
if not node:
|
raise InvalidEdgeError(self.fullNameToNode, link, "Source node does not exist")
|
||||||
raise KeyError(f"Node '{linkNodeName}' not found")
|
attr = node.attribute(linkAttrName)
|
||||||
g.addEdge(node.attribute(linkAttrName), self)
|
if attr is None:
|
||||||
except KeyError as err:
|
raise InvalidEdgeError(self.fullNameToNode, link, "Source attribute does not exist")
|
||||||
logging.warning('Connect Attribute from Expression failed.')
|
graph.addEdge(attr, self)
|
||||||
logging.warning('Expression: "{exp}"\nError: "{err}".'.format(exp=v, err=err))
|
except InvalidEdgeError as err:
|
||||||
self.resetToDefaultValue()
|
logging.warning(err)
|
||||||
|
except Exception as err:
|
||||||
|
logging.warning("Unexpected error happened during edge creation")
|
||||||
|
logging.warning(f"Expression '{self._linkExpression}': {err}")
|
||||||
|
|
||||||
|
self._linkExpression = None
|
||||||
|
self.resetToDefaultValue()
|
||||||
|
|
||||||
def getExportValue(self):
|
def getExportValue(self):
|
||||||
if self.isLink:
|
if self.isLink:
|
||||||
|
@ -543,9 +571,8 @@ class ListAttribute(Attribute):
|
||||||
def _set_value(self, value):
|
def _set_value(self, value):
|
||||||
if self.node.graph:
|
if self.node.graph:
|
||||||
self.remove(0, len(self))
|
self.remove(0, len(self))
|
||||||
# Link to another attribute
|
if self._handleLinkValue(value):
|
||||||
if isinstance(value, ListAttribute) or Attribute.isLinkExpression(value):
|
return
|
||||||
self._value = value
|
|
||||||
# New value
|
# New value
|
||||||
else:
|
else:
|
||||||
# During initialization self._value may not be set
|
# During initialization self._value may not be set
|
||||||
|
@ -556,10 +583,10 @@ class ListAttribute(Attribute):
|
||||||
self.requestGraphUpdate()
|
self.requestGraphUpdate()
|
||||||
|
|
||||||
def upgradeValue(self, exportedValues):
|
def upgradeValue(self, exportedValues):
|
||||||
|
if self._handleLinkValue(exportedValues):
|
||||||
|
return
|
||||||
|
|
||||||
if not isinstance(exportedValues, list):
|
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 " +
|
raise RuntimeError("ListAttribute.upgradeValue: the given value is of type " +
|
||||||
str(type(exportedValues)) + " but a 'list' is expected.")
|
str(type(exportedValues)) + " but a 'list' is expected.")
|
||||||
|
|
||||||
|
@ -620,10 +647,8 @@ class ListAttribute(Attribute):
|
||||||
return super(ListAttribute, self).uid()
|
return super(ListAttribute, self).uid()
|
||||||
|
|
||||||
def _applyExpr(self):
|
def _applyExpr(self):
|
||||||
if not self.node.graph:
|
if self._linkExpression:
|
||||||
return
|
super()._applyExpr()
|
||||||
if isinstance(self._value, ListAttribute) or Attribute.isLinkExpression(self._value):
|
|
||||||
super(ListAttribute, self)._applyExpr()
|
|
||||||
else:
|
else:
|
||||||
for value in self._value:
|
for value in self._value:
|
||||||
value._applyExpr()
|
value._applyExpr()
|
||||||
|
|
|
@ -12,6 +12,13 @@ class GraphException(MeshroomException):
|
||||||
pass
|
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):
|
class GraphCompatibilityError(GraphException):
|
||||||
"""
|
"""
|
||||||
Raised when node compatibility issues occur when loading a graph.
|
Raised when node compatibility issues occur when loading a graph.
|
||||||
|
|
|
@ -16,7 +16,7 @@ import meshroom.core
|
||||||
from meshroom.common import BaseObject, DictModel, Slot, Signal, Property
|
from meshroom.common import BaseObject, DictModel, Slot, Signal, Property
|
||||||
from meshroom.core import Version
|
from meshroom.core import Version
|
||||||
from meshroom.core.attribute import Attribute, ListAttribute, GroupAttribute
|
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.graphIO import GraphIO, GraphSerializer, TemplateGraphSerializer, PartialGraphSerializer
|
||||||
from meshroom.core.node import BaseNode, Status, Node, CompatibilityNode
|
from meshroom.core.node import BaseNode, Status, Node, CompatibilityNode
|
||||||
from meshroom.core.nodeFactory import nodeFactory
|
from meshroom.core.nodeFactory import nodeFactory
|
||||||
|
@ -485,41 +485,38 @@ class Graph(BaseObject):
|
||||||
node._applyExpr()
|
node._applyExpr()
|
||||||
return node
|
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.
|
Get a copy instance of a node outside the graph.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
srcNode (Node): the node to copy
|
srcNode: the node to copy
|
||||||
withEdges (bool): whether to copy edges
|
withEdges: whether to copy edges
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Node, dict: the created node instance,
|
The created node instance and the mapping of skipped edge per attribute (always empty if `withEdges` is True).
|
||||||
a dictionary of linked attributes with their original value (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)
|
||||||
|
|
||||||
with GraphModification(self):
|
with GraphModification(self):
|
||||||
# create a new node of the same type and with the same attributes values
|
node = nodeFactory(srcNode.toDict(), name=srcNode.nodeType)
|
||||||
# keep links as-is so that CompatibilityNodes attributes can be created with correct automatic description
|
|
||||||
# (File params for link expressions)
|
|
||||||
node = nodeFactory(srcNode.toDict(), srcNode.nodeType) # use nodeType as name
|
|
||||||
# skip edges: filter out attributes which are links by resetting default values
|
|
||||||
skippedEdges = {}
|
skippedEdges = {}
|
||||||
if not withEdges:
|
if not withEdges:
|
||||||
for n, attr in node.attributes.items():
|
for _, attr in node.attributes.items():
|
||||||
if attr.isOutput:
|
_removeLinkExpressions(attr, skippedEdges)
|
||||||
# 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()
|
|
||||||
return node, skippedEdges
|
return node, skippedEdges
|
||||||
|
|
||||||
def duplicateNodes(self, srcNodes):
|
def duplicateNodes(self, srcNodes):
|
||||||
|
@ -850,13 +847,16 @@ class Graph(BaseObject):
|
||||||
return set(self._nodes) - nodesWithInputLink
|
return set(self._nodes) - nodesWithInputLink
|
||||||
|
|
||||||
@changeTopology
|
@changeTopology
|
||||||
def addEdge(self, srcAttr, dstAttr):
|
def addEdge(self, srcAttr: Attribute, dstAttr: Attribute):
|
||||||
assert isinstance(srcAttr, Attribute)
|
if not (srcAttr.node.graph == dstAttr.node.graph == self):
|
||||||
assert isinstance(dstAttr, Attribute)
|
raise InvalidEdgeError(
|
||||||
if srcAttr.node.graph != self or dstAttr.node.graph != self:
|
srcAttr.fullNameToGraph, dstAttr.fullNameToGraph, "Attributes do not belong to this Graph"
|
||||||
raise RuntimeError('The attributes of the edge should be part of a common graph.')
|
)
|
||||||
if dstAttr in self.edges.keys():
|
if dstAttr in self.edges.keys():
|
||||||
raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.getFullNameToNode()))
|
raise InvalidEdgeError(
|
||||||
|
srcAttr.fullNameToNode, dstAttr.fullNameToNode, "Destination is already connected"
|
||||||
|
)
|
||||||
|
|
||||||
edge = Edge(srcAttr, dstAttr)
|
edge = Edge(srcAttr, dstAttr)
|
||||||
self.edges.add(edge)
|
self.edges.add(edge)
|
||||||
self.markNodesDirty(dstAttr.node)
|
self.markNodesDirty(dstAttr.node)
|
||||||
|
|
Loading…
Add table
Reference in a new issue