Skip to content

Commit 98f9aee

Browse files
Merge pull request #1: Fix attribute value change propagation and callback handling
Merged from original PR #2586 Original: alicevision/Meshroom#2586
2 parents b752b73 + cef6dfb commit 98f9aee

File tree

4 files changed

+495
-47
lines changed

4 files changed

+495
-47
lines changed

meshroom/core/attribute.py

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ def attributeFactory(description, value, isOutput, node, root=None, parent=None)
2525
root: (optional) parent Attribute (must be ListAttribute or GroupAttribute)
2626
parent (BaseObject): (optional) the parent BaseObject if any
2727
"""
28-
attr = description.instanceType(node, description, isOutput, root, parent)
28+
attr: Attribute = description.instanceType(node, description, isOutput, root, parent)
2929
if value is not None:
30-
attr._set_value(value, emitSignals=False)
30+
attr._set_value(value)
3131
else:
32-
attr.resetToDefaultValue(emitSignals=False)
32+
attr.resetToDefaultValue()
33+
34+
attr.valueChanged.connect(lambda attr=attr: node._onAttributeChanged(attr))
35+
3336
return attr
3437

3538

@@ -67,7 +70,6 @@ def __init__(self, node, attributeDesc, isOutput, root=None, parent=None):
6770
self._value = None
6871
self.initValue()
6972

70-
self.valueChanged.connect(self.onChanged)
7173

7274
@property
7375
def node(self):
@@ -180,22 +182,7 @@ def _get_value(self):
180182
return self.getLinkParam().value
181183
return self._value
182184

183-
def onChanged(self):
184-
""" Called when the attribute value has changed """
185-
if self.node.isCompatibilityNode:
186-
# We have no access to the node's implementation,
187-
# so we cannot call the custom method.
188-
return
189-
if self.isOutput and not self.node.isInputNode:
190-
# Ignore changes on output attributes for non-input nodes
191-
# as they are updated during the node's computation.
192-
# And we do not want notifications during the graph processing.
193-
return
194-
# notify the node that the attribute has changed
195-
# this will call the node descriptor "onAttrNameChanged" method
196-
self.node.onAttributeChanged(self)
197-
198-
def _set_value(self, value, emitSignals=True):
185+
def _set_value(self, value):
199186
if self._value == value:
200187
return
201188

@@ -211,9 +198,6 @@ def _set_value(self, value, emitSignals=True):
211198
convertedValue = self.validateValue(value)
212199
self._value = convertedValue
213200

214-
if not emitSignals:
215-
return
216-
217201
# Request graph update when input parameter value is set
218202
# and parent node belongs to a graph
219203
# Output attributes value are set internally during the update process,
@@ -251,8 +235,8 @@ def initValue(self):
251235
if self.desc._valueType is not None:
252236
self._value = self.desc._valueType()
253237

254-
def resetToDefaultValue(self, emitSignals=True):
255-
self._set_value(copy.copy(self.defaultValue()), emitSignals=emitSignals)
238+
def resetToDefaultValue(self):
239+
self._set_value(copy.copy(self.defaultValue()))
256240

257241
def requestGraphUpdate(self):
258242
if self.node.graph:
@@ -538,14 +522,13 @@ def index(self, item):
538522
return self._value.indexOf(item)
539523

540524
def initValue(self):
541-
self.resetToDefaultValue(emitSignals=False)
525+
self.resetToDefaultValue()
542526

543-
def resetToDefaultValue(self, emitSignals=True):
527+
def resetToDefaultValue(self):
544528
self._value = ListModel(parent=self)
545-
if emitSignals:
546-
self.valueChanged.emit()
529+
self.valueChanged.emit()
547530

548-
def _set_value(self, value, emitSignals=True):
531+
def _set_value(self, value):
549532
if self.node.graph:
550533
self.remove(0, len(self))
551534
# Link to another attribute
@@ -558,8 +541,6 @@ def _set_value(self, value, emitSignals=True):
558541
self._value = ListModel(parent=self)
559542
newValue = self.desc.validateValue(value)
560543
self.extend(newValue)
561-
if not emitSignals:
562-
return
563544
self.requestGraphUpdate()
564545

565546
def upgradeValue(self, exportedValues):
@@ -696,7 +677,7 @@ def __getattr__(self, key):
696677
except KeyError:
697678
raise AttributeError(key)
698679

699-
def _set_value(self, exportedValue, emitSignals=True):
680+
def _set_value(self, exportedValue):
700681
value = self.validateValue(exportedValue)
701682
if isinstance(value, dict):
702683
# set individual child attribute values
@@ -734,7 +715,7 @@ def initValue(self):
734715
childAttr.valueChanged.connect(self.valueChanged)
735716
self._value.reset(subAttributes)
736717

737-
def resetToDefaultValue(self, emitSignals=True):
718+
def resetToDefaultValue(self):
738719
for attrDesc in self.desc._groupDesc:
739720
self._value.get(attrDesc.name).resetToDefaultValue()
740721

meshroom/core/node.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import uuid
1515
from collections import namedtuple
1616
from enum import Enum
17+
from typing import Callable, Optional
1718

1819
import meshroom
1920
from meshroom.common import Signal, Variant, Property, BaseObject, Slot, ListModel, DictModel
@@ -929,25 +930,50 @@ def updateStatisticsFromCache(self):
929930
def _updateChunks(self):
930931
pass
931932

932-
def onAttributeChanged(self, attr):
933-
""" When an attribute changed, a specific function can be defined in the descriptor and be called.
933+
def _getAttributeChangedCallback(self, attr: Attribute) -> Optional[Callable]:
934+
"""Get the node descriptor-defined value changed callback associated to `attr` if any."""
935+
936+
# Callbacks cannot be defined on nested attributes.
937+
if attr.root is not None:
938+
return None
939+
940+
attrCapitalizedName = attr.name[:1].upper() + attr.name[1:]
941+
callbackName = f"on{attrCapitalizedName}Changed"
942+
943+
callback = getattr(self.nodeDesc, callbackName, None)
944+
return callback if callback and callable(callback) else None
945+
946+
def _onAttributeChanged(self, attr: Attribute):
947+
"""
948+
When an attribute value has changed, a specific function can be defined in the descriptor and be called.
934949
935950
Args:
936-
attr (Attribute): attribute that has changed
951+
attr: The Attribute that has changed.
937952
"""
938-
# Call the specific function if it exists in the node implementation
939-
paramName = attr.name[:1].upper() + attr.name[1:]
940-
methodName = f'on{paramName}Changed'
941-
if hasattr(self.nodeDesc, methodName):
942-
m = getattr(self.nodeDesc, methodName)
943-
if callable(m):
944-
m(self)
953+
954+
if self.isCompatibilityNode:
955+
# Compatibility nodes are not meant to be updated.
956+
return
957+
958+
if attr.isOutput and not self.isInputNode:
959+
# Ignore changes on output attributes for non-input nodes
960+
# as they are updated during the node's computation.
961+
# And we do not want notifications during the graph processing.
962+
return
963+
964+
if attr.value is None:
965+
# Discard dynamic values depending on the graph processing.
966+
return
967+
968+
callback = self._getAttributeChangedCallback(attr)
969+
970+
if callback:
971+
callback(self)
945972

946973
if self.graph:
947974
# If we are in a graph, propagate the notification to the connected output attributes
948-
outEdges = self.graph.outEdges(attr)
949-
for edge in outEdges:
950-
edge.dst.onChanged()
975+
for edge in self.graph.outEdges(attr):
976+
edge.dst.node._onAttributeChanged(edge.dst)
951977

952978
def onAttributeClicked(self, attr):
953979
""" When an attribute is clicked, a specific function can be defined in the descriptor and be called.

tests/conftest.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from pathlib import Path
2+
import tempfile
3+
4+
import pytest
5+
6+
from meshroom.core.graph import Graph
7+
8+
9+
@pytest.fixture
10+
def graphWithIsolatedCache():
11+
"""
12+
Yield a Graph instance using a unique temporary cache directory.
13+
14+
Can be used for testing graph computation in isolation, without having to save the graph to disk.
15+
"""
16+
with tempfile.TemporaryDirectory() as cacheDir:
17+
graph = Graph("")
18+
graph.cacheDir = cacheDir
19+
yield graph
20+
21+
22+
@pytest.fixture
23+
def graphSavedOnDisk():
24+
"""
25+
Yield a Graph instance saved in a unique temporary folder.
26+
27+
Can be used for testing graph IO and computation in isolation.
28+
"""
29+
with tempfile.TemporaryDirectory() as cacheDir:
30+
graph = Graph("")
31+
graph.save(Path(cacheDir) / "test_graph.mg")
32+
yield graph

0 commit comments

Comments
 (0)