diff --git a/CHANGELOG.md b/CHANGELOG.md index 01146fc..06ad824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index a56d0cc..cb118cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"}] diff --git a/src/funcnodes_core/node.py b/src/funcnodes_core/node.py index 315d8de..505305a 100644 --- a/src/funcnodes_core/node.py +++ b/src/funcnodes_core/node.py @@ -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 diff --git a/src/funcnodes_core/nodemaker.py b/src/funcnodes_core/nodemaker.py index 859d95a..7cb4673 100644 --- a/src/funcnodes_core/nodemaker.py +++ b/src/funcnodes_core/nodemaker.py @@ -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)) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 9de58de..75ebecb 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -4,6 +4,7 @@ import funcnodes_core as fn +import pytest from pytest_funcnodes import funcnodes_test @@ -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 diff --git a/uv.lock b/uv.lock index 2f13528..7997790 100644 --- a/uv.lock +++ b/uv.lock @@ -457,7 +457,7 @@ wheels = [ [[package]] name = "funcnodes-core" -version = "2.3.0" +version = "2.3.1" source = { editable = "." } dependencies = [ { name = "dill" },