Skip to content
Merged

Test #245

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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## v2.4.0 (2026-03-17)

### Feat

- **context**: add context7 configuration file with URL and public key

### Fix

- **io**: preserve trigger propagation semantics across node chains
- **tests**: correct typo in test function name for clarity

## v2.3.2 (2025-12-24)

### Fix
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "funcnodes-core"

version = "2.3.2"
version = "2.4.0"

description = "core package for funcnodes"
authors = [{name = "Julian Kimmig", email = "[email protected]"}]
Expand Down
16 changes: 11 additions & 5 deletions src/funcnodes_core/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1039,10 +1039,11 @@ def set_value(

self.datapath = new_datapath

resolved_does_trigger = does_trigger
if self.node is not None:
if does_trigger is None:
does_trigger = self.does_trigger
if does_trigger:
if resolved_does_trigger is None:
resolved_does_trigger = self.does_trigger
if resolved_does_trigger:
self.node.request_trigger()

for other in self._forwards:
Expand Down Expand Up @@ -1153,7 +1154,7 @@ def forward(self, other: NodeInput, replace=False):
self._forwards.add(other)
other.forwards_from(self, replace=replace)

other.set_value(self.value)
other.set_value(self.value, does_trigger=False)

return [
self.node.uuid if self.node else None,
Expand Down Expand Up @@ -1304,14 +1305,19 @@ def set_value(
else:
datapath = None
# input_paths = []
propagated_does_trigger = False if does_trigger is False else None
for other in self.connections:
# if self.node is not None:
# for input_path in input_paths:
# datapath.add_src_path(input_path)
# else:
# datapath = None

other.set_value(value, does_trigger=does_trigger, datapath=datapath)
other.set_value(
value,
does_trigger=propagated_does_trigger,
datapath=datapath,
)

def post_connect(self, other: NodeIO):
"""Called after a connection is made.
Expand Down
174 changes: 174 additions & 0 deletions tests/test_nodeclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ async def func(self, input: int) -> int: # noqa: A003 - matching fn signature
return input


def make_counter_node(node_id: str, *, does_trigger: bool = True):
resolved_node_id = node_id

class CounterNode(Node):
node_id = resolved_node_id
value = NodeInput(id="value", type=int, does_trigger=does_trigger)
output = NodeOutput(id="output", type=int)

def __init__(self, *args, **kwargs):
super().__init__(*args, pretrigger_delay=0.0, **kwargs)
self.call_count = 0

async def func(self, value: int):
self.call_count += 1
self.outputs["output"].value = value

return CounterNode


@funcnodes_test
async def test_nodeclass_initialization():
with pytest.raises(TypeError):
Expand Down Expand Up @@ -128,6 +147,161 @@ async def test_trigger_stack():
assert not trigger_stack.done()


@funcnodes_test
async def test_forwarded_input_respects_target_does_trigger_flag():
ForwardSourceNode = make_counter_node("forward_source_node_test")
ForwardTargetNode = make_counter_node(
"forward_target_node_test", does_trigger=False
)

node_a = ForwardSourceNode()
node_b = ForwardTargetNode()
node_a.inputs["value"].connect(node_b.inputs["value"])

node_a.inputs["value"].value = 42
await fn.run_until_complete(node_a, node_b)

assert node_a.inputs["value"].value == 42
assert node_b.inputs["value"].value == 42
assert node_a.call_count == 1
assert node_b.call_count == 0
assert node_a.outputs["output"].value == 42
assert node_b.outputs["output"].value is fn.NoValue


@funcnodes_test
async def test_forwarded_input_uses_target_flag_when_source_flag_disables_self_only():
ForwardSourceNode = make_counter_node(
"forward_source_node_no_trigger_test", does_trigger=False
)
ForwardTargetNode = make_counter_node("forward_target_node_allow_test")

node_a = ForwardSourceNode()
node_b = ForwardTargetNode()
node_a.inputs["value"].connect(node_b.inputs["value"])

node_a.inputs["value"].value = 42
await fn.run_until_complete(node_a, node_b)

assert node_a.call_count == 0
assert node_b.call_count == 1
assert node_b.inputs["value"].value == 42
assert node_b.outputs["output"].value == 42


@funcnodes_test
async def test_forwarded_input_propagates_explicit_false_downstream():
ForwardSourceNode = make_counter_node("forward_source_node_explicit_false_test")
ForwardTargetNode = make_counter_node("forward_target_node_explicit_false_test")

node_a = ForwardSourceNode()
node_b = ForwardTargetNode()
node_a.inputs["value"].connect(node_b.inputs["value"])

node_a.inputs["value"].set_value(42, does_trigger=False)
await fn.run_until_complete(node_a, node_b)

assert node_a.call_count == 0
assert node_b.call_count == 0
assert node_b.inputs["value"].value == 42
assert node_b.outputs["output"].value is fn.NoValue


@funcnodes_test
async def test_forwarded_input_triggers_downstream_when_both_allow_triggering():
ForwardSourceNode = make_counter_node("forward_source_node_trigger_test")
ForwardTargetNode = make_counter_node("forward_target_node_trigger_test")

node_a = ForwardSourceNode()
node_b = ForwardTargetNode()
node_a.inputs["value"].connect(node_b.inputs["value"])

node_a.inputs["value"].value = 42
await fn.run_until_complete(node_a, node_b)

assert node_a.call_count == 1
assert node_b.call_count == 1
assert node_b.outputs["output"].value == 42


@funcnodes_test
async def test_forward_chain_skips_non_triggering_middle_node_but_triggers_downstream():
NodeA = make_counter_node("forward_chain_source_node_test")
NodeB = make_counter_node("forward_chain_middle_node_test", does_trigger=False)
NodeC = make_counter_node("forward_chain_target_node_test")

node_a = NodeA()
node_b = NodeB()
node_c = NodeC()

node_a.inputs["value"].connect(node_b.inputs["value"])
node_b.inputs["value"].connect(node_c.inputs["value"])

node_a.inputs["value"].value = 42
await fn.run_until_complete(node_a, node_b, node_c)

assert node_a.call_count == 1
assert node_b.call_count == 0
assert node_c.call_count == 1
assert node_b.inputs["value"].value == 42
assert node_c.inputs["value"].value == 42
assert node_b.outputs["output"].value is fn.NoValue
assert node_c.outputs["output"].value == 42


@funcnodes_test
async def test_output_set_value_respects_explicit_false_downstream():
SourceNode = make_counter_node("output_source_node_explicit_false_test")
TargetNode = make_counter_node("output_target_node_explicit_false_test")

node_a = SourceNode()
node_b = TargetNode()
node_a.outputs["output"].connect(node_b.inputs["value"])

node_a.outputs["output"].set_value(42, does_trigger=False)
await fn.run_until_complete(node_a, node_b)

assert node_b.inputs["value"].value == 42
assert node_b.call_count == 0
assert node_b.outputs["output"].value is fn.NoValue


@funcnodes_test
async def test_output_set_value_uses_target_does_trigger_flag_when_unspecified():
SourceNode = make_counter_node("output_source_node_unspecified_test")
TargetNode = make_counter_node(
"output_target_node_unspecified_false_test", does_trigger=False
)

node_a = SourceNode()
node_b = TargetNode()
node_a.outputs["output"].connect(node_b.inputs["value"])

node_a.outputs["output"].set_value(42)
await fn.run_until_complete(node_a, node_b)

assert node_b.inputs["value"].value == 42
assert node_b.call_count == 0
assert node_b.outputs["output"].value is fn.NoValue


@funcnodes_test
async def test_output_set_value_triggers_target_when_target_allows_triggering():
SourceNode = make_counter_node("output_source_node_trigger_test")
TargetNode = make_counter_node("output_target_node_trigger_test")

node_a = SourceNode()
node_b = TargetNode()
node_a.outputs["output"].connect(node_b.inputs["value"])

node_a.outputs["output"].set_value(42)
await fn.run_until_complete(node_a, node_b)

assert node_b.inputs["value"].value == 42
assert node_b.call_count == 1
assert node_b.outputs["output"].value == 42


@funcnodes_test
def test_nodeclass_string():
test_node = DummyNode(uuid="test_uuid")
Expand Down
Loading
Loading