From 9976e803944e5073756a3a7395b54140d4ba6d4d Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Thu, 20 Nov 2025 13:53:20 -0300 Subject: [PATCH 1/2] [Added] Generic AND/OR (NAND/NOR/NOT) - Two nodes supporting Any as input - Optional invert - Dynamic inputs --- src/basic_data_handling/boolean_nodes.py | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/basic_data_handling/boolean_nodes.py b/src/basic_data_handling/boolean_nodes.py index fa14134..49efffd 100644 --- a/src/basic_data_handling/boolean_nodes.py +++ b/src/basic_data_handling/boolean_nodes.py @@ -1,4 +1,5 @@ from inspect import cleandoc +from typing import Any try: from comfy.comfy_types.node_typing import IO, ComfyNodeABC @@ -12,6 +13,9 @@ class IO: ANY = "*" ComfyNodeABC = object +from ._dynamic_input import ContainsDynamicDict + + class BooleanAnd(ComfyNodeABC): """ Returns the logical AND result of two boolean values. @@ -155,20 +159,80 @@ def xor_operation(self, input1: bool, input2: bool) -> tuple[bool]: return (input1 != input2,) +class GenericOr(ComfyNodeABC): + """ + Returns the logical N/OR result of one or more values. + + This node takes a dynamic number of inputs and returns their logical N/OR result. + Note that values are evaluated according Python's rules. I.e. an empty string is + `false`, an integer 0 is also `false`, etc. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "invert": (IO.BOOLEAN, {"default": False}), + }, + "optional": ContainsDynamicDict({ + "item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING"}), + }) + } + + RETURN_TYPES = (IO.BOOLEAN,) + CATEGORY = "Basic/BOOLEAN" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "or_operation" + + def or_operation(self, invert: bool, **kwargs: list[Any]) -> tuple[bool]: + return (any(kwargs.values()) ^ invert,) + + +class GenericAnd(ComfyNodeABC): + """ + Returns the logical N/AND result of one or more values. + + This node takes a dynamic number of inputs and returns their logical N/AND result. + Note that values are evaluated according Python's rules. I.e. an empty string is + `false`, an integer 0 is also `false`, etc. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "invert": (IO.BOOLEAN, {"default": False}), + }, + "optional": ContainsDynamicDict({ + "item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING", "default": "True"}), + }) + } + + RETURN_TYPES = (IO.BOOLEAN,) + CATEGORY = "Basic/BOOLEAN" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "and_operation" + + def and_operation(self, invert: bool, **kwargs: list[Any]) -> tuple[bool]: + return (all(kwargs.values()) ^ invert,) + + NODE_CLASS_MAPPINGS = { "Basic data handling: Boolean And": BooleanAnd, + "Basic data handling: Generic And": GenericAnd, "Basic data handling: Boolean Nand": BooleanNand, "Basic data handling: Boolean Nor": BooleanNor, "Basic data handling: Boolean Not": BooleanNot, "Basic data handling: Boolean Or": BooleanOr, + "Basic data handling: Generic Or": GenericOr, "Basic data handling: Boolean Xor": BooleanXor, } NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: Boolean And": "and", + "Basic data handling: Generic And": "and (generic)", "Basic data handling: Boolean Nand": "nand", "Basic data handling: Boolean Nor": "nor", "Basic data handling: Boolean Not": "not", "Basic data handling: Boolean Or": "or", + "Basic data handling: Generic Or": "or (generic)", "Basic data handling: Boolean Xor": "xor", } From f137c46aaee5c7b7945b4581b83a4c96361dff75 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 28 Nov 2025 11:04:10 -0300 Subject: [PATCH 2/2] [Added] GenericAnd/GenericOr tests --- tests/test_boolean_nodes.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_boolean_nodes.py b/tests/test_boolean_nodes.py index a20f8ea..5ce15b1 100644 --- a/tests/test_boolean_nodes.py +++ b/tests/test_boolean_nodes.py @@ -7,6 +7,8 @@ BooleanXor, BooleanNand, BooleanNor, + GenericAnd, + GenericOr, ) @@ -54,3 +56,37 @@ def test_boolean_nor(): assert node.nor_operation(True, False) == (False,) assert node.nor_operation(False, True) == (False,) assert node.nor_operation(False, False) == (True,) + + +def test_generic_or(): + node = GenericOr() + # 2xOR + assert node.or_operation(False, item_0=True, item_1=True) == (True,) + assert node.or_operation(False, item_0=True, item_1=False) == (True,) + assert node.or_operation(False, item_0=False, item_1=True) == (True,) + assert node.or_operation(False, item_0=False, item_1=False) == (False,) + # 2xNOR + assert node.or_operation(True, item_0=True, item_1=True) == (False,) + assert node.or_operation(True, item_0=True, item_1=False) == (False,) + assert node.or_operation(True, item_0=False, item_1=True) == (False,) + assert node.or_operation(True, item_0=False, item_1=False) == (True,) + # A couple of 3xOR cases + assert node.or_operation(False, item_0=False, item_1=False, item_2=True) == (True,) + assert node.or_operation(False, item_0=False, item_1=False, item_2=False) == (False,) + + +def test_generic_and(): + node = GenericAnd() + # 2xAND + assert node.and_operation(False, item_0=True, item_1=True) == (True,) + assert node.and_operation(False, item_0=True, item_1=False) == (False,) + assert node.and_operation(False, item_0=False, item_1=True) == (False,) + assert node.and_operation(False, item_0=False, item_1=False) == (False,) + # 2xNAND + assert node.and_operation(True, item_0=True, item_1=True) == (False,) + assert node.and_operation(True, item_0=True, item_1=False) == (True,) + assert node.and_operation(True, item_0=False, item_1=True) == (True,) + assert node.and_operation(True, item_0=False, item_1=False) == (True,) + # A couple of 3xAND cases + assert node.and_operation(False, item_0=True, item_1=True, item_2=True) == (True,) + assert node.and_operation(False, item_0=True, item_1=False, item_2=True) == (False,)