From 2f6e9e4ccf33c34561293c8b8eaf3e773cd79230 Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Tue, 23 Dec 2025 12:51:09 +0100 Subject: [PATCH 1/2] fix(core): handle duplicate IO ids and output mapping Reserialize the active IO after collisions and set outputs by uuid when names are duplicated. Extend tests for multi-output collision renames and failure bounds. --- src/funcnodes_core/node.py | 2 +- src/funcnodes_core/nodemaker.py | 4 +-- tests/test_decorator.py | 61 +++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) 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 From 0f5a35a33480c0847125e801824836b559fc1093 Mon Sep 17 00:00:00 2001 From: Julian Kimmig Date: Tue, 23 Dec 2025 12:51:40 +0100 Subject: [PATCH 2/2] =?UTF-8?q?bump:=20version=202.3.0=20=E2=86=92=202.3.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) 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/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" },