From 800636baaa410ce56f538255923e3d3a29af108a Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 14:34:28 +0200 Subject: [PATCH 01/10] Add flow selection nodes --- src/basic_data_handling/control_flow_nodes.py | 35 +++++++++++++++ src/basic_data_handling/data_list_nodes.py | 45 ++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index 7d96b91..43a4878 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -1,6 +1,8 @@ from typing import Any from inspect import cleandoc from comfy.comfy_types.node_typing import IO, ComfyNodeABC +from comfy_execution.graph import ExecutionBlocker + class IfElse(ComfyNodeABC): """ @@ -192,14 +194,47 @@ def execute(self, selector: int, **kwargs) -> tuple[Any]: return (kwargs.get("default"),) +class FlowSelect(ComfyNodeABC): + """ + Select the direction of the flow. + + This node takes a value and directs it to either the "true" or "false" output. + + Note: for dynamic switching in a Data Flow you might want to use + "filter select" instead. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {}), + "select": (IO.BOOLEAN, {}), + } + } + + RETURN_TYPES = (IO.ANY, IO.ANY) + RETURN_NAMES = ("true", "false") + CATEGORY = "Basic/flow control" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "select" + + def select(self, value, select = True) -> tuple[Any, Any]: + if select: + return value, ExecutionBlocker(None) + else: + return ExecutionBlocker(None), value + + NODE_CLASS_MAPPINGS = { "Basic data handling: IfElse": IfElse, "Basic data handling: IfElifElse": IfElifElse, "Basic data handling: SwitchCase": SwitchCase, + "Basic data handling: FlowSelect": FlowSelect, } NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: IfElse": "if/else", "Basic data handling: IfElifElse": "if/elif/.../else", "Basic data handling: SwitchCase": "switch/case", + "Basic data handling: FlowSelect": "flow select", } diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 346ba61..565602d 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -295,6 +295,46 @@ def filter_data(self, **kwargs: list[Any]) -> tuple[list[Any]]: return (result,) +class DataListFilterSelect(ComfyNodeABC): + """ + Filters a Data List using boolean values. + + This node takes a value Data List and a filter Data List (containing only boolean values). + It returns two new Data Lists containing only the elements from the value list where the + corresponding element in the filter list is true or false. + + If the lists have different lengths, the last element of the shorter list is repeated + till the lengths are matching. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {}), + "select": (IO.BOOLEAN, {}), + } + } + + RETURN_TYPES = (IO.ANY, IO.ANY) + RETURN_NAMES = ("true", "false") + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "select" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True, True,) + + def select(self, **kwargs: list[Any]) -> tuple[list[Any]]: + values = kwargs.get('value', []) + selects = kwargs.get('select', []) + + # Create a new list with only items where the filter is False + result_true, result_false = [], [] + for value, select in zip(values, selects): + (result_true if select else result_false).append(value) + + return result_true, result_false + + class DataListGetItem(ComfyNodeABC): """ Retrieves an item at a specified position in a list. @@ -771,7 +811,8 @@ def INPUT_TYPES(cls): INPUT_IS_LIST = True def convert(self, **kwargs: list[Any]) -> tuple[list[Any]]: - return (kwargs.get('list', []).copy(),) + print(f"input list: '{kwargs.get('list', [])}'") + return (list(kwargs.get('list', [])).copy(),) class DataListToSet(ComfyNodeABC): @@ -810,6 +851,7 @@ def convert(self, **kwargs: list[Any]) -> tuple[set[Any]]: "Basic data handling: DataListCount": DataListCount, "Basic data handling: DataListExtend": DataListExtend, "Basic data handling: DataListFilter": DataListFilter, + "Basic data handling: DataListFilterSelect": DataListFilterSelect, "Basic data handling: DataListGetItem": DataListGetItem, "Basic data handling: DataListIndex": DataListIndex, "Basic data handling: DataListInsert": DataListInsert, @@ -838,6 +880,7 @@ def convert(self, **kwargs: list[Any]) -> tuple[set[Any]]: "Basic data handling: DataListCount": "count", "Basic data handling: DataListExtend": "extend", "Basic data handling: DataListFilter": "filter", + "Basic data handling: DataListFilterSelect": "filter select", "Basic data handling: DataListGetItem": "get item", "Basic data handling: DataListIndex": "index", "Basic data handling: DataListInsert": "insert", From 95b2a1e2133792ff64c6658b68adb43cb6d8c7e7 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 15:25:30 +0200 Subject: [PATCH 02/10] Mock also ExecutionBlocker --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index aa09b9b..e4d9d2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,3 +26,11 @@ sys.modules["comfy"] = mock_comfy sys.modules["comfy.comfy_types"] = mock_comfy sys.modules["comfy.comfy_types.node_typing"] = mock_comfy + +def mock_execution_blocker(_): + return None + +mock_comfy_execution = MagicMock() +mock_comfy_execution.ExecutionBlocker = mock_execution_blocker +sys.modules["comfy_execution"] = mock_comfy_execution +sys.modules["comfy_execution.graph"] = mock_comfy_execution From 8741e48602bf40073a5184ef5bef866c2b9da620 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 16:13:29 +0200 Subject: [PATCH 03/10] Version 0.3.1 Add node to force an execution order --- pyproject.toml | 2 +- src/basic_data_handling/control_flow_nodes.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aae12b3..19eadf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "0.3.0" +version = "0.3.1" description = """NOTE: Still in development! Expect breaking changes! Basic Python functions for manipulating data that every programmer is used to. Currently supported ComfyUI data types: BOOLEAN, FLOAT, INT, STRING and data lists. diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index 7d96b91..dc1f2bb 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -191,15 +191,43 @@ def execute(self, selector: int, **kwargs) -> tuple[Any]: # If selector is out of range or the selected case is None, return default return (kwargs.get("default"),) +class ExecutionOrder(ComfyNodeABC): + """ + Force execution order in the workflow. + + This node is lightweight and does not affect the workflow. It is used to force + the execution order of nodes in the workflow. You only need to chain this node + with the other execution order nodes in the desired order and add any + output of the nodes you want to force execution order on. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "E/O": ("E/O", {}), + "any node output": (IO.ANY, {}), + } + } + + RETURN_TYPES = ("E/O",) + CATEGORY = "Basic/flow control" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "execute" + + def execute(self, **kwargs) -> tuple[Any]: + return (None,) + NODE_CLASS_MAPPINGS = { "Basic data handling: IfElse": IfElse, "Basic data handling: IfElifElse": IfElifElse, "Basic data handling: SwitchCase": SwitchCase, + "Basic data handling: ExecutionOrder": ExecutionOrder, } NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: IfElse": "if/else", "Basic data handling: IfElifElse": "if/elif/.../else", "Basic data handling: SwitchCase": "switch/case", + "Basic data handling: ExecutionOrder": "force execution order", } From e0643e885e3629f6ec39c8effef4ab27733de586 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 14:34:28 +0200 Subject: [PATCH 04/10] Add flow selection nodes --- src/basic_data_handling/control_flow_nodes.py | 35 +++++++++++++++ src/basic_data_handling/data_list_nodes.py | 45 ++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index dc1f2bb..02bf445 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -1,6 +1,8 @@ from typing import Any from inspect import cleandoc from comfy.comfy_types.node_typing import IO, ComfyNodeABC +from comfy_execution.graph import ExecutionBlocker + class IfElse(ComfyNodeABC): """ @@ -218,10 +220,42 @@ def execute(self, **kwargs) -> tuple[Any]: return (None,) +class FlowSelect(ComfyNodeABC): + """ + Select the direction of the flow. + + This node takes a value and directs it to either the "true" or "false" output. + + Note: for dynamic switching in a Data Flow you might want to use + "filter select" instead. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {}), + "select": (IO.BOOLEAN, {}), + } + } + + RETURN_TYPES = (IO.ANY, IO.ANY) + RETURN_NAMES = ("true", "false") + CATEGORY = "Basic/flow control" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "select" + + def select(self, value, select = True) -> tuple[Any, Any]: + if select: + return value, ExecutionBlocker(None) + else: + return ExecutionBlocker(None), value + + NODE_CLASS_MAPPINGS = { "Basic data handling: IfElse": IfElse, "Basic data handling: IfElifElse": IfElifElse, "Basic data handling: SwitchCase": SwitchCase, + "Basic data handling: FlowSelect": FlowSelect, "Basic data handling: ExecutionOrder": ExecutionOrder, } @@ -229,5 +263,6 @@ def execute(self, **kwargs) -> tuple[Any]: "Basic data handling: IfElse": "if/else", "Basic data handling: IfElifElse": "if/elif/.../else", "Basic data handling: SwitchCase": "switch/case", + "Basic data handling: FlowSelect": "flow select", "Basic data handling: ExecutionOrder": "force execution order", } diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 346ba61..565602d 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -295,6 +295,46 @@ def filter_data(self, **kwargs: list[Any]) -> tuple[list[Any]]: return (result,) +class DataListFilterSelect(ComfyNodeABC): + """ + Filters a Data List using boolean values. + + This node takes a value Data List and a filter Data List (containing only boolean values). + It returns two new Data Lists containing only the elements from the value list where the + corresponding element in the filter list is true or false. + + If the lists have different lengths, the last element of the shorter list is repeated + till the lengths are matching. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {}), + "select": (IO.BOOLEAN, {}), + } + } + + RETURN_TYPES = (IO.ANY, IO.ANY) + RETURN_NAMES = ("true", "false") + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "select" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True, True,) + + def select(self, **kwargs: list[Any]) -> tuple[list[Any]]: + values = kwargs.get('value', []) + selects = kwargs.get('select', []) + + # Create a new list with only items where the filter is False + result_true, result_false = [], [] + for value, select in zip(values, selects): + (result_true if select else result_false).append(value) + + return result_true, result_false + + class DataListGetItem(ComfyNodeABC): """ Retrieves an item at a specified position in a list. @@ -771,7 +811,8 @@ def INPUT_TYPES(cls): INPUT_IS_LIST = True def convert(self, **kwargs: list[Any]) -> tuple[list[Any]]: - return (kwargs.get('list', []).copy(),) + print(f"input list: '{kwargs.get('list', [])}'") + return (list(kwargs.get('list', [])).copy(),) class DataListToSet(ComfyNodeABC): @@ -810,6 +851,7 @@ def convert(self, **kwargs: list[Any]) -> tuple[set[Any]]: "Basic data handling: DataListCount": DataListCount, "Basic data handling: DataListExtend": DataListExtend, "Basic data handling: DataListFilter": DataListFilter, + "Basic data handling: DataListFilterSelect": DataListFilterSelect, "Basic data handling: DataListGetItem": DataListGetItem, "Basic data handling: DataListIndex": DataListIndex, "Basic data handling: DataListInsert": DataListInsert, @@ -838,6 +880,7 @@ def convert(self, **kwargs: list[Any]) -> tuple[set[Any]]: "Basic data handling: DataListCount": "count", "Basic data handling: DataListExtend": "extend", "Basic data handling: DataListFilter": "filter", + "Basic data handling: DataListFilterSelect": "filter select", "Basic data handling: DataListGetItem": "get item", "Basic data handling: DataListIndex": "index", "Basic data handling: DataListInsert": "insert", From 0917a6705a4ddf109fad47dfb51c6a54770bb465 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 14:58:46 +0200 Subject: [PATCH 05/10] Optimize node display --- src/basic_data_handling/control_flow_nodes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index 02bf445..6c994a9 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -55,11 +55,11 @@ class IfElifElse(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "if": (IO.BOOLEAN, {}), + "if": (IO.BOOLEAN, {"forceInput": True}), "then": (IO.ANY, {"lazy": True}), }, "optional": { - "elif_0": (IO.BOOLEAN, {"lazy": True, "_dynamic": "number", "_dynamicGroup": 0}), + "elif_0": (IO.BOOLEAN, {"forceInput": True, "lazy": True, "_dynamic": "number", "_dynamicGroup": 0}), "then_0": (IO.ANY, {"lazy": True, "_dynamic": "number", "_dynamicGroup": 0}), "else": (IO.ANY, {"lazy": True}), } From fdeef2d407abdfc2e76b08e9cbbda2a01ce31712 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 16:38:35 +0200 Subject: [PATCH 06/10] clean up --- src/basic_data_handling/control_flow_nodes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index 23d414c..fdc1c74 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -193,6 +193,7 @@ def execute(self, selector: int, **kwargs) -> tuple[Any]: # If selector is out of range or the selected case is None, return default return (kwargs.get("default"),) + class ExecutionOrder(ComfyNodeABC): """ Force execution order in the workflow. From dfd660badcc8940e2715df54a3c99f6be86cd00a Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 16:41:02 +0200 Subject: [PATCH 07/10] clean up --- src/basic_data_handling/control_flow_nodes.py | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index fdc1c74..1f8d183 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -252,37 +252,6 @@ def select(self, value, select = True) -> tuple[Any, Any]: return ExecutionBlocker(None), value -class FlowSelect(ComfyNodeABC): - """ - Select the direction of the flow. - - This node takes a value and directs it to either the "true" or "false" output. - - Note: for dynamic switching in a Data Flow you might want to use - "filter select" instead. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": (IO.ANY, {}), - "select": (IO.BOOLEAN, {}), - } - } - - RETURN_TYPES = (IO.ANY, IO.ANY) - RETURN_NAMES = ("true", "false") - CATEGORY = "Basic/flow control" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "select" - - def select(self, value, select = True) -> tuple[Any, Any]: - if select: - return value, ExecutionBlocker(None) - else: - return ExecutionBlocker(None), value - - NODE_CLASS_MAPPINGS = { "Basic data handling: IfElse": IfElse, "Basic data handling: IfElifElse": IfElifElse, From c39481d46042f9e20ffe9c8d05328bf4475ca65c Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 16:42:35 +0200 Subject: [PATCH 08/10] Remove debug output --- src/basic_data_handling/data_list_nodes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 565602d..003461e 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -811,7 +811,6 @@ def INPUT_TYPES(cls): INPUT_IS_LIST = True def convert(self, **kwargs: list[Any]) -> tuple[list[Any]]: - print(f"input list: '{kwargs.get('list', [])}'") return (list(kwargs.get('list', [])).copy(),) From 7aa2d1c641ea8fd5da2c798b715050b1b05aad8d Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 16:43:49 +0200 Subject: [PATCH 09/10] Clean up --- src/basic_data_handling/control_flow_nodes.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index 1f8d183..88f343d 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -194,33 +194,6 @@ def execute(self, selector: int, **kwargs) -> tuple[Any]: return (kwargs.get("default"),) -class ExecutionOrder(ComfyNodeABC): - """ - Force execution order in the workflow. - - This node is lightweight and does not affect the workflow. It is used to force - the execution order of nodes in the workflow. You only need to chain this node - with the other execution order nodes in the desired order and add any - output of the nodes you want to force execution order on. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "optional": { - "E/O": ("E/O", {}), - "any node output": (IO.ANY, {}), - } - } - - RETURN_TYPES = ("E/O",) - CATEGORY = "Basic/flow control" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "execute" - - def execute(self, **kwargs) -> tuple[Any]: - return (None,) - - class FlowSelect(ComfyNodeABC): """ Select the direction of the flow. @@ -252,6 +225,33 @@ def select(self, value, select = True) -> tuple[Any, Any]: return ExecutionBlocker(None), value +class ExecutionOrder(ComfyNodeABC): + """ + Force execution order in the workflow. + + This node is lightweight and does not affect the workflow. It is used to force + the execution order of nodes in the workflow. You only need to chain this node + with the other execution order nodes in the desired order and add any + output of the nodes you want to force execution order on. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "E/O": ("E/O", {}), + "any node output": (IO.ANY, {}), + } + } + + RETURN_TYPES = ("E/O",) + CATEGORY = "Basic/flow control" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "execute" + + def execute(self, **kwargs) -> tuple[Any]: + return (None,) + + NODE_CLASS_MAPPINGS = { "Basic data handling: IfElse": IfElse, "Basic data handling: IfElifElse": IfElifElse, From 39de69d46a050c9ba1a459df3e9941b4e7c9d2f4 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sun, 18 May 2025 16:44:42 +0200 Subject: [PATCH 10/10] Bump version to 0.3.2 as new nodes are added --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 19eadf4..e669584 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "0.3.1" +version = "0.3.2" description = """NOTE: Still in development! Expect breaking changes! Basic Python functions for manipulating data that every programmer is used to. Currently supported ComfyUI data types: BOOLEAN, FLOAT, INT, STRING and data lists.