Skip to content
Merged

Dev #237

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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## v2.3.1 (2025-12-23)

### Fix

- **core**: handle duplicate IO ids and output mapping

## v2.3.0 (2025-12-23)

### Feat
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.0"
version = "2.3.1"

description = "core package for funcnodes"
authors = [{name = "Julian Kimmig", email = "julian.kimmig@linkdlab.de"}]
Expand Down
2 changes: 1 addition & 1 deletion src/funcnodes_core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def __init_subclass__(cls, **kwargs):
cls,
io.uuid,
)
ipser = ip.to_dict()
ipser = io.serialize()

cls._class_io_serialized[ipser["id"]] = ipser

Expand Down
4 changes: 2 additions & 2 deletions src/funcnodes_core/nodemaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ async def _wrapped_func(self: Node, *args, **kwargs):
outs = await asyncfunc(*args, **kwargs)
if len(outputs) > 1:
for op, out in zip(outputs, outs):
self.outputs[op.name].value = out
self.outputs[op.uuid].value = out
elif len(outputs) == 1:
self.outputs[outputs[0].name].value = outs
self.outputs[outputs[0].uuid].value = outs
return outs

kwargs.setdefault("node_name", in_func.ef_funcmeta.get("name", id))
Expand Down
61 changes: 61 additions & 0 deletions tests/test_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


import funcnodes_core as fn
import pytest
from pytest_funcnodes import funcnodes_test


Expand Down Expand Up @@ -600,3 +601,63 @@ def my_node(
await fn.run_until_complete(ins1, ins2)
assert ins1.outputs["c"].value == 1
assert ins2.outputs["c"].value == 3


@funcnodes_test
async def test_decorator_withdouble_params():
@fn.NodeDecorator(node_id="my_nodea", outputs=[{"name": "a"}])
def my_nodea(a: int, b: int) -> int:
return a + b

node = my_nodea()
input_a = node.inputs["a"]
output_a = next(op for op in node.outputs.values() if op.name == "a")

assert input_a.name == "a"
assert output_a.name == "a"
assert output_a.uuid != input_a.uuid
assert output_a.uuid == "a_"
assert output_a.uuid in my_nodea._class_io_serialized

node.inputs["a"].value = 1
node.inputs["b"].value = 2
await node

assert node.outputs[output_a.uuid].value == 3

class MultiOutputNode(fn.Node):
node_id = "my_nodea_multi"

a = fn.NodeInput(id="a", type=int)
b = fn.NodeInput(id="b", type=int)
out_a1 = fn.NodeOutput(id="a", type=int)
out_a2 = fn.NodeOutput(id="a", type=int)
out_a3 = fn.NodeOutput(id="a", type=int)

async def func(self, a: int, b: int) -> int: # noqa: A003 - signature
return a + b

node_multi = MultiOutputNode()
output_uuids = sorted(
op.uuid for op in node_multi.outputs.values() if op.name == "a"
)
assert output_uuids == ["a_", "a__", "a___"]
for uuid in output_uuids:
assert uuid in MultiOutputNode._class_io_serialized

with pytest.raises(
ValueError, match="automatically generating a new one seems to fail"
):

class MultiOutputNodeFail(fn.Node):
node_id = "my_nodea_multi_fail"

a = fn.NodeInput(id="a", type=int)
b = fn.NodeInput(id="b", type=int)
out_a1 = fn.NodeOutput(id="a", type=int)
out_a2 = fn.NodeOutput(id="a", type=int)
out_a3 = fn.NodeOutput(id="a", type=int)
out_a4 = fn.NodeOutput(id="a", type=int)

async def func(self, a: int, b: int) -> int: # noqa: A003 - signature
return a + b
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.