Skip to content

Commit 6355037

Browse files
authored
Merge pull request #2671 from alicevision/dev/nodeCreationCallback
NodeAPI: Trigger node creation callback only for explicit new node creation
2 parents 550f685 + 90acb93 commit 6355037

File tree

4 files changed

+97
-36
lines changed

4 files changed

+97
-36
lines changed

meshroom/core/desc/node.py

+5
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ def __init__(self):
6767
def upgradeAttributeValues(self, attrValues, fromVersion):
6868
return attrValues
6969

70+
@classmethod
71+
def onNodeCreated(cls, node):
72+
"""Called after a node instance had been created from this node descriptor and added to a Graph."""
73+
pass
74+
7075
@classmethod
7176
def update(cls, node):
7277
""" Method call before node's internal update on invalidation.

meshroom/core/graph.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
import os
66
import re
7-
from typing import Any, Optional
7+
from typing import Any, Iterable, Optional
88
import weakref
99
from collections import defaultdict, OrderedDict
1010
from contextlib import contextmanager
@@ -257,6 +257,11 @@ def initFromTemplate(self, filepath: PathLike, publishOutputs: bool = False):
257257
"""
258258
self._deserialize(Graph._loadGraphData(filepath))
259259

260+
# Creating nodes from a template is conceptually similar to explicit node creation,
261+
# therefore the nodes descriptors' "onNodeCreated" callback is triggered for each
262+
# node instance created by this process.
263+
self._triggerNodeCreatedCallback(self.nodes)
264+
260265
if not publishOutputs:
261266
with GraphModification(self):
262267
for node in [node for node in self.nodes if node.nodeType == "Publish"]:
@@ -621,25 +626,35 @@ def removeNode(self, nodeName):
621626

622627
return inEdges, outEdges, outListAttributes
623628

624-
def addNewNode(self, nodeType, name=None, position=None, **kwargs):
629+
def addNewNode(
630+
self, nodeType: str, name: Optional[str] = None, position: Optional[str] = None, **kwargs
631+
) -> Node:
625632
"""
626633
Create and add a new node to the graph.
627634
628635
Args:
629-
nodeType (str): the node type name.
630-
name (str): if specified, the desired name for this node. If not unique, will be prefixed (_N).
631-
position (Position): (optional) the position of the node
632-
**kwargs: keyword arguments to initialize node's attributes
636+
nodeType: the node type name.
637+
name: if specified, the desired name for this node. If not unique, will be prefixed (_N).
638+
position: the position of the node.
639+
**kwargs: keyword arguments to initialize the created node's attributes.
633640
634641
Returns:
635642
The newly created node.
636643
"""
637644
if name and name in self._nodes.keys():
638645
name = self._createUniqueNodeName(name)
639646

640-
n = self.addNode(Node(nodeType, position=position, **kwargs), uniqueName=name)
641-
n.updateInternals()
642-
return n
647+
node = self.addNode(Node(nodeType, position=position, **kwargs), uniqueName=name)
648+
node.updateInternals()
649+
self._triggerNodeCreatedCallback([node])
650+
return node
651+
652+
def _triggerNodeCreatedCallback(self, nodes: Iterable[Node]):
653+
"""Trigger the `onNodeCreated` node descriptor callback for each node instance in `nodes`."""
654+
with GraphModification(self):
655+
for node in nodes:
656+
if node.nodeDesc:
657+
node.nodeDesc.onNodeCreated(node)
643658

644659
def _createUniqueNodeName(self, inputName: str, existingNames: Optional[set[str]] = None):
645660
"""Create a unique node name based on the input name.

meshroom/core/node.py

-27
Original file line numberDiff line numberDiff line change
@@ -1467,33 +1467,6 @@ def __init__(self, nodeType, position=None, parent=None, uid=None, **kwargs):
14671467
if attr.invalidate:
14681468
self.invalidatingAttributes.add(attr)
14691469

1470-
self.optionalCallOnDescriptor("onNodeCreated")
1471-
1472-
def optionalCallOnDescriptor(self, methodName, *args, **kwargs):
1473-
""" Call of optional method defined in the descriptor.
1474-
Available method names are:
1475-
- onNodeCreated
1476-
"""
1477-
if hasattr(self.nodeDesc, methodName):
1478-
m = getattr(self.nodeDesc, methodName)
1479-
if callable(m):
1480-
try:
1481-
m(self, *args, **kwargs)
1482-
except Exception:
1483-
import traceback
1484-
# Format error strings with all the provided arguments
1485-
argsStr = ", ".join(str(arg) for arg in args)
1486-
kwargsStr = ", ".join(str(key) + "=" + str(value) for key, value in kwargs.items())
1487-
finalErrStr = argsStr
1488-
if kwargsStr:
1489-
if argsStr:
1490-
finalErrStr += ", "
1491-
finalErrStr += kwargsStr
1492-
1493-
logging.error("Error on call to '{}' (with args: '{}') for node type {}".
1494-
format(methodName, finalErrStr, self.nodeType))
1495-
logging.error(traceback.format_exc())
1496-
14971470
def setAttributeValues(self, values):
14981471
# initialize attribute values
14991472
for k, v in values.items():

tests/test_nodeCallbacks.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from meshroom.core import desc, registerNodeType, unregisterNodeType
2+
from meshroom.core.node import Node
3+
from meshroom.core.graph import Graph, loadGraph
4+
5+
6+
class NodeWithCreationCallback(desc.InputNode):
7+
"""Node defining an 'onNodeCreated' callback, triggered a new node is added to a Graph."""
8+
9+
inputs = [
10+
desc.BoolParam(
11+
name="triggered",
12+
label="Triggered",
13+
description="Attribute impacted by the `onNodeCreated` callback",
14+
value=False,
15+
),
16+
]
17+
18+
@classmethod
19+
def onNodeCreated(cls, node: Node):
20+
"""Triggered when a new node is created within a Graph."""
21+
node.triggered.value = True
22+
23+
24+
class TestNodeCreationCallback:
25+
@classmethod
26+
def setup_class(cls):
27+
registerNodeType(NodeWithCreationCallback)
28+
29+
@classmethod
30+
def teardown_class(cls):
31+
unregisterNodeType(NodeWithCreationCallback)
32+
33+
def test_notTriggeredOnNodeInstantiation(self):
34+
node = Node(NodeWithCreationCallback.__name__)
35+
assert node.triggered.value is False
36+
37+
def test_triggeredOnNewNodeCreationInGraph(self):
38+
graph = Graph("")
39+
node = graph.addNewNode(NodeWithCreationCallback.__name__)
40+
assert node.triggered.value is True
41+
42+
def test_notTriggeredOnNodeDuplication(self):
43+
graph = Graph("")
44+
node = graph.addNewNode(NodeWithCreationCallback.__name__)
45+
node.triggered.resetToDefaultValue()
46+
47+
duplicates = graph.duplicateNodes([node])
48+
assert duplicates[node][0].triggered.value is False
49+
50+
def test_notTriggeredOnGraphLoad(self, graphSavedOnDisk):
51+
graph: Graph = graphSavedOnDisk
52+
node = graph.addNewNode(NodeWithCreationCallback.__name__)
53+
node.triggered.resetToDefaultValue()
54+
graph.save()
55+
56+
loadedGraph = loadGraph(graph.filepath)
57+
assert loadedGraph.node(node.name).triggered.value is False
58+
59+
def test_triggeredOnGraphInitializationFromTemplate(self, graphSavedOnDisk):
60+
graph: Graph = graphSavedOnDisk
61+
node = graph.addNewNode(NodeWithCreationCallback.__name__)
62+
node.triggered.resetToDefaultValue()
63+
graph.save(template=True)
64+
65+
graphFromTemplate = Graph("")
66+
graphFromTemplate.initFromTemplate(graph.filepath)
67+
68+
assert graphFromTemplate.node(node.name).triggered.value is True

0 commit comments

Comments
 (0)