diff --git a/__init__.py b/__init__.py index 48208d6..fe724f9 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ """Top-level package for basic_data_handling.""" +import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__))) __all__ = [ "NODE_CLASS_MAPPINGS", @@ -6,7 +7,7 @@ "WEB_DIRECTORY", ] -from .src.basic_data_handling import NODE_CLASS_MAPPINGS -from .src.basic_data_handling import NODE_DISPLAY_NAME_MAPPINGS +from src.basic_data_handling import NODE_CLASS_MAPPINGS +from src.basic_data_handling import NODE_DISPLAY_NAME_MAPPINGS WEB_DIRECTORY = "./web" diff --git a/pyproject.toml b/pyproject.toml index e669584..997058e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "0.3.2" +version = "0.3.3" 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. @@ -46,14 +46,15 @@ Icon = "" minversion = "8.0" pythonpath = [ "src", - "../..", # Path to parent directory containing comfy module + #"../..", # Path to parent directory containing comfy module "." ] testpaths = [ "tests", ] #python_files = ["test_*.py"] -python_files = ["conftest.py", "test_boolean_nodes.py"] +#python_files = ["conftest.py", "test_boolean_nodes.py"] +python_files = ["test_boolean_nodes.py"] [tool.mypy] files = "." diff --git a/src/basic_data_handling/_dynamic_input.py b/src/basic_data_handling/_dynamic_input.py new file mode 100644 index 0000000..e02d69a --- /dev/null +++ b/src/basic_data_handling/_dynamic_input.py @@ -0,0 +1,32 @@ +class ContainsDynamicDict(dict): + """ + A custom dictionary that dynamically returns values for keys based on a pattern. + - If a key in the passed dictionary has a value with `{"_dynamic": "number"}` in the tuple's second position, + then any other key starting with the same string and ending with a number will return that value. + - For other keys, normal dictionary lookup behavior applies. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Store prefixes associated with `_dynamic` values for efficient lookup + self._dynamic_prefixes = { + key.rstrip("0123456789"): value + for key, value in self.items() + if isinstance(value, tuple) and len(value) > 1 and value[1].get("_dynamic") == "number" + } + + def __contains__(self, key): + # Check if key matches a dynamically handled prefix or exists normally + return ( + any(key.startswith(prefix) and key[len(prefix):].isdigit() for prefix in self._dynamic_prefixes) + or super().__contains__(key) + ) + + def __getitem__(self, key): + # Dynamically return the value for keys matching a `prefix` pattern + print(f'_ dynamic prefixes: {self._dynamic_prefixes}; get key: {key}') + for prefix, value in self._dynamic_prefixes.items(): + if key.startswith(prefix) and key[len(prefix):].isdigit(): + return value + # Fallback to normal dictionary behavior for other keys + return super().__getitem__(key) diff --git a/src/basic_data_handling/boolean_nodes.py b/src/basic_data_handling/boolean_nodes.py index b0685f2..fa14134 100644 --- a/src/basic_data_handling/boolean_nodes.py +++ b/src/basic_data_handling/boolean_nodes.py @@ -1,5 +1,16 @@ from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class BooleanAnd(ComfyNodeABC): """ diff --git a/src/basic_data_handling/casting_nodes.py b/src/basic_data_handling/casting_nodes.py index 15c0b27..c9d76e7 100644 --- a/src/basic_data_handling/casting_nodes.py +++ b/src/basic_data_handling/casting_nodes.py @@ -1,6 +1,17 @@ from typing import Any from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class CastToBoolean(ComfyNodeABC): """ diff --git a/src/basic_data_handling/comparison_nodes.py b/src/basic_data_handling/comparison_nodes.py index e7cef2d..25e1a8b 100644 --- a/src/basic_data_handling/comparison_nodes.py +++ b/src/basic_data_handling/comparison_nodes.py @@ -1,6 +1,17 @@ from typing import Any from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class Equal(ComfyNodeABC): """ diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index 88f343d..ef4ee4b 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -1,7 +1,21 @@ from typing import Any from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC -from comfy_execution.graph import ExecutionBlocker + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC + from comfy_execution.graph import ExecutionBlocker +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object + ExecutionBlocker = lambda x: x + +from ._dynamic_input import ContainsDynamicDict class IfElse(ComfyNodeABC): @@ -58,11 +72,11 @@ def INPUT_TYPES(cls): "if": (IO.BOOLEAN, {"forceInput": True}), "then": (IO.ANY, {"lazy": True}), }, - "optional": { + "optional": ContainsDynamicDict({ "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}), - } + }) } RETURN_TYPES = (IO.ANY,) @@ -140,10 +154,10 @@ class SwitchCase(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "required": { + "required": ContainsDynamicDict({ "selector": (IO.INT, {"default": 0, "min": 0}), "case_0": (IO.ANY, {"lazy": True, "_dynamic": "number"}), - }, + }), "optional": { "default": (IO.ANY, {"lazy": True}), } diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 003461e..4c157bd 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -1,6 +1,19 @@ from typing import Any from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object + +from ._dynamic_input import ContainsDynamicDict INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes @@ -15,9 +28,9 @@ class DataListCreate(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.ANY, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.ANY,) @@ -42,9 +55,9 @@ class DataListCreateFromBoolean(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.BOOLEAN,) @@ -69,9 +82,9 @@ class DataListCreateFromFloat(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.FLOAT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.FLOAT,) @@ -96,9 +109,9 @@ class DataListCreateFromInt(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.INT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.INT,) @@ -123,9 +136,9 @@ class DataListCreateFromString(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.STRING, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = (IO.STRING,) diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index a23a17f..d243aa2 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -1,6 +1,19 @@ from typing import Any from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object + +from ._dynamic_input import ContainsDynamicDict class DictCreate(ComfyNodeABC): @@ -13,10 +26,10 @@ class DictCreate(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), "value_0": (IO.ANY, {"_dynamic": "number", "_dynamicGroup": 0}), - } + }) } RETURN_TYPES = ("DICT",) @@ -45,10 +58,10 @@ class DictCreateFromBoolean(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), - "value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0}), - } + "value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0}), + }) } RETURN_TYPES = ("DICT",) @@ -77,10 +90,10 @@ class DictCreateFromFloat(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), "value_0": (IO.FLOAT, {"_dynamic": "number", "_dynamicGroup": 0}), - } + }) } RETURN_TYPES = ("DICT",) @@ -109,10 +122,10 @@ class DictCreateFromInt(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), "value_0": (IO.INT, {"_dynamic": "number", "_dynamicGroup": 0}), - } + }) } RETURN_TYPES = ("DICT",) @@ -141,10 +154,10 @@ class DictCreateFromString(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), "value_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), - } + }) } RETURN_TYPES = ("DICT",) diff --git a/src/basic_data_handling/float_nodes.py b/src/basic_data_handling/float_nodes.py index 3241ed8..0529883 100644 --- a/src/basic_data_handling/float_nodes.py +++ b/src/basic_data_handling/float_nodes.py @@ -1,5 +1,16 @@ from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class FloatCreate(ComfyNodeABC): diff --git a/src/basic_data_handling/int_nodes.py b/src/basic_data_handling/int_nodes.py index 9a9c52f..88e4413 100644 --- a/src/basic_data_handling/int_nodes.py +++ b/src/basic_data_handling/int_nodes.py @@ -1,6 +1,17 @@ from inspect import cleandoc from typing import Literal -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class IntCreate(ComfyNodeABC): diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index c645967..11d8300 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -1,6 +1,19 @@ from typing import Any from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object + +from ._dynamic_input import ContainsDynamicDict INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes @@ -15,9 +28,9 @@ class ListCreate(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.ANY, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) @@ -40,9 +53,9 @@ class ListCreateFromBoolean(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) @@ -65,9 +78,9 @@ class ListCreateFromFloat(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.FLOAT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) @@ -90,9 +103,9 @@ class ListCreateFromInt(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.INT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) @@ -115,9 +128,9 @@ class ListCreateFromString(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.STRING, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("LIST",) diff --git a/src/basic_data_handling/math_formula_node.py b/src/basic_data_handling/math_formula_node.py index 62c4d04..9041983 100644 --- a/src/basic_data_handling/math_formula_node.py +++ b/src/basic_data_handling/math_formula_node.py @@ -1,5 +1,18 @@ from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object + +from ._dynamic_input import ContainsDynamicDict class MathFormula(ComfyNodeABC): """ @@ -19,10 +32,10 @@ class MathFormula(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "required": { + "required": ContainsDynamicDict({ "formula": (IO.STRING, {"default": "a + b"}), "a": (IO.NUMBER, {"default": 0.0, "_dynamic": "letter"}), - }, + }), } RETURN_TYPES = (IO.FLOAT,) diff --git a/src/basic_data_handling/math_nodes.py b/src/basic_data_handling/math_nodes.py index 5c6a299..35ea0b6 100644 --- a/src/basic_data_handling/math_nodes.py +++ b/src/basic_data_handling/math_nodes.py @@ -1,7 +1,18 @@ import math from inspect import cleandoc from typing import Literal, Union -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class MathAbs(ComfyNodeABC): """ diff --git a/src/basic_data_handling/path_nodes.py b/src/basic_data_handling/path_nodes.py index f825683..3846f1f 100644 --- a/src/basic_data_handling/path_nodes.py +++ b/src/basic_data_handling/path_nodes.py @@ -1,7 +1,18 @@ from inspect import cleandoc import os import glob -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class PathAbspath(ComfyNodeABC): diff --git a/src/basic_data_handling/regex_nodes.py b/src/basic_data_handling/regex_nodes.py index 267b745..a6cef22 100644 --- a/src/basic_data_handling/regex_nodes.py +++ b/src/basic_data_handling/regex_nodes.py @@ -1,6 +1,17 @@ from inspect import cleandoc import re -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class RegexFindallDataList(ComfyNodeABC): """ diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index d642d2a..ea9014e 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -1,6 +1,19 @@ from typing import Any from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object + +from ._dynamic_input import ContainsDynamicDict class SetCreate(ComfyNodeABC): @@ -13,9 +26,9 @@ class SetCreate(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.ANY, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) @@ -38,9 +51,9 @@ class SetCreateFromBoolean(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) @@ -63,9 +76,9 @@ class SetCreateFromFloat(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.FLOAT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) @@ -88,9 +101,9 @@ class SetCreateFromInt(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.INT, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) @@ -113,9 +126,9 @@ class SetCreateFromString(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "optional": { + "optional": ContainsDynamicDict({ "item_0": (IO.STRING, {"_dynamic": "number"}), - } + }) } RETURN_TYPES = ("SET",) diff --git a/src/basic_data_handling/string_nodes.py b/src/basic_data_handling/string_nodes.py index 81e3f32..8e59d29 100644 --- a/src/basic_data_handling/string_nodes.py +++ b/src/basic_data_handling/string_nodes.py @@ -1,5 +1,16 @@ from inspect import cleandoc -from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +try: + from comfy.comfy_types.node_typing import IO, ComfyNodeABC +except: + class IO: + BOOLEAN = "BOOLEAN" + INT = "INT" + FLOAT = "FLOAT" + STRING = "STRING" + NUMBER = "FLOAT,INT" + ANY = "*" + ComfyNodeABC = object class StringCapitalize(ComfyNodeABC): """Converts the first character of the input string to uppercase and all other characters to lowercase.""" diff --git a/tests/conftest.py b/tests/conftest.py index e4d9d2c..76b82ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,36 +1,36 @@ -import os -import sys -from unittest.mock import MagicMock - -# Add the project root directory to Python path -# This allows the tests to import the project -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -print("=======================================================================") -print("Mocking 'comfy.comfy_types.node_typing' via conftest.py") -print("=======================================================================") - -# Create a dummy 'comfy.comfy_types.node_typing' module -mock_comfy = MagicMock() -mock_comfy.IO = MagicMock( - BOOLEAN="BOOLEAN", - INT="INT", - FLOAT="FLOAT", - STRING="STRING", - NUMBER = "FLOAT,INT", - ANY="*", -) -mock_comfy.ComfyNodeABC = object - -# Inject the mocked module into sys.modules -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 +#import os +#import sys +# from unittest.mock import MagicMock +# +# # Add the project root directory to Python path +# # This allows the tests to import the project +#sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +# +# print("=======================================================================") +# print("Mocking 'comfy.comfy_types.node_typing' via conftest.py") +# print("=======================================================================") +# +# # Create a dummy 'comfy.comfy_types.node_typing' module +# mock_comfy = MagicMock() +# mock_comfy.IO = MagicMock( +# BOOLEAN="BOOLEAN", +# INT="INT", +# FLOAT="FLOAT", +# STRING="STRING", +# NUMBER = "FLOAT,INT", +# ANY="*", +# ) +# mock_comfy.ComfyNodeABC = object +# +# # Inject the mocked module into sys.modules +# 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 diff --git a/web/js/dynamicnode.js b/web/js/dynamicnode.js index ce8bef5..7db19b2 100644 --- a/web/js/dynamicnode.js +++ b/web/js/dynamicnode.js @@ -44,11 +44,19 @@ app.registerExtension({ const combinedInputData = { ...nodeData?.input?.required ?? {}, ...nodeData?.input?.optional ?? {} - }; + } + const combinedInputDataOrder = [ + ...nodeData?.input_order?.required ?? [], + ...nodeData?.input_order?.optional ?? [] + ]; + /** Array of (generic) dynamic inputs. */ const dynamicInputs = []; - for (const name in combinedInputData) { + /** Array of groups of (generic) dynamic inputs. */ + const dynamicInputGroups = []; + for (const name of combinedInputDataOrder) { const dynamic = combinedInputData[name][1]?._dynamic; + const dynamicGroup = combinedInputData[name][1]?._dynamicGroup ?? 0; if (dynamic) { let matcher; @@ -67,7 +75,13 @@ app.registerExtension({ } const baseName = name.match(matcher)?.[1] ?? name; const dynamicType = combinedInputData[name][0]; - dynamicInputs.push({name, baseName, matcher, dynamic, dynamicType}); + if (dynamicInputGroups[dynamicGroup] === undefined) { + dynamicInputGroups[dynamicGroup] = []; + } + dynamicInputGroups[dynamicGroup].push( + dynamicInputs.length + ) + dynamicInputs.push({name, baseName, matcher, dynamic, dynamicType, dynamicGroup}); } } if (dynamicInputs.length === 0) { @@ -83,53 +97,39 @@ app.registerExtension({ /** * Utility: Update inputs' slot indices after reordering. - * @param {Array} inputs - List of inputs for the node. - * @param {Object} graph - Graph containing the node. + * @param {ComfyNode} node - The node to update. */ - const updateSlotIndices = (inputs, graph) => { - inputs.forEach((input, index) => { + const updateSlotIndices = (node) => { + node.inputs.forEach((input, index) => { input.slot_index = index; - if (input.link !== null) { - const link = graph._links.get(input.link); - if (link) + if (input.isConnected) { + const link = node.graph._links.get(input.link); + if (link) { link.target_slot = index; - else + } else { console.error(`Input ${index} has an invalid link.`); + } } }); }; - // Override onConfigure: Ensure dynamic inputs are continuously ordered - const onConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function() { - const result = onConfigure?.apply(this, arguments); - let lastDynamicIdx = -1 - for (let idx = 0; idx < this.inputs.length; idx++) { - const input = this.inputs[idx]; - const isDynamic = isDynamicInput(input.name); - if (isDynamic) { - if (lastDynamicIdx < 0 || lastDynamicIdx === idx - 1) { - lastDynamicIdx = idx; - } else { - // non-continuous dynamic inputs -> move up - for (let i = idx-1; i > lastDynamicIdx; i--) { - this.swapInputs(i, i + 1); - } - lastDynamicIdx++; - } - } - } - return result; - } - // Add helper method to insert input at a specific position - nodeType.prototype.addInputAtPosition = function (name, type, position) { - console.warn("Adding input at position:", name, type, position); - this.addInput(name, type); // Add new input + nodeType.prototype.addInputAtPosition = function (name, type, position, isWidget, shape) { + if (isWidget) { + this.addWidget(type, name, '', ()=>{}, {}); + + const GET_CONFIG = Symbol(); + const input = this.addInput(name, type, { + shape, + widget: {name, [GET_CONFIG]: () =>{}} + }) + } else { + this.addInput(name, type, {shape}); // Add new input + } const newInput = this.inputs.pop(); // Fetch the newly added input (last item) this.inputs.splice(position, 0, newInput); // Place it at the desired position - updateSlotIndices(this.inputs, this.graph); // Update indices + updateSlotIndices(this); // Update indices return newInput; }; @@ -142,80 +142,133 @@ app.registerExtension({ nodeType.prototype.onConnectionsChange = function (type, slotIndex, isConnected, link, ioSlot) { const result = onConnectionsChange?.apply(this, arguments); - if (type !== TypeSlot.Input || isProcessingConnection) { + if (type !== TypeSlot.Input || isProcessingConnection || !isDynamicInput(this.inputs[slotIndex].name)) { return result; } + function getDynamicGroup(inputName) { + // Find the dynamicGroup by matching the baseName with input name + for (const di of dynamicInputs) { + if (inputName.startsWith(di.baseName)) { + return di.dynamicGroup; + } + } + return undefined; + } + isProcessingConnection = true; try { - const baseName = dynamicInputs[0].baseName; - //const dynamicType = nodeData.input.required[dynamicInputs[0].name][0]; - const dynamicType = dynamicInputs[0].dynamicType; - // Get dynamic input slots - const dynamicSlots = this.inputs - .map((input, idx) => ({ - index: idx, - name: input.name, - connected: input.link !== null, - isDynamic: isDynamicInput(input.name), - })) - .filter((input) => input.isDynamic); + const dynamicSlots = []; + const dynamicGroupCount = []; + const dynamicGroupConnected = []; + for (const [index, input] of this.inputs.entries()) { + const isDynamic = isDynamicInput(input.name); + if (isDynamic) { + const connected = input.isConnected; + const dynamicGroup = getDynamicGroup(input.name); + if (dynamicGroup in dynamicGroupCount) { + if (input.name.startsWith(dynamicInputs[dynamicInputGroups[dynamicGroup][0]].baseName)) { + dynamicGroupConnected[dynamicGroup][dynamicGroupCount[dynamicGroup]] ||= connected; + dynamicGroupCount[dynamicGroup]++; + } + } else { + dynamicGroupCount[dynamicGroup] = 1; + dynamicGroupConnected[dynamicGroup] = [connected]; + } + dynamicSlots.push({ + index, + name: input.name, + isWidget: input.widget !== undefined, + shape: input.shape, + connected, + isDynamic, + dynamicGroup, + dynamicGroupCount: dynamicGroupCount[dynamicGroup] + }); + + // sanity check to make sure every widget is in reality a widget. When loading a workflow this + // isn't the case so we must fix it ourselves. + if (this.widgets && !this.widgets.some((w) => w.name === input.name)) { + this.addWidget(input.type, input.name, '', ()=>{}, {}); + } + } + } // Handle connection event if (isConnected === TypeSlotEvent.Connect) { - const hasEmptyDynamic = dynamicSlots.some(di => !di.connected); + const hasEmptyDynamic = dynamicGroupConnected[0].some(dgc => !dgc); if (!hasEmptyDynamic) { // No empty slot - add a new one after the last dynamic input const lastDynamicIdx = Math.max(...dynamicSlots.map((slot) => slot.index), -1); - const insertPosition = lastDynamicIdx + 1; + let insertPosition = lastDynamicIdx + 1; let inputInRange = true; - let newName; - if (dynamicInputs[0].dynamic === 'letter') { - if (dynamicSlots.length > 26) { - inputInRange = false; + for (const groupMember of dynamicInputGroups[dynamicInputs[0].dynamicGroup]) { + const baseName = dynamicInputs[groupMember].baseName; + const dynamicType = dynamicInputs[groupMember].dynamicType; + let newName; + if (dynamicInputs[0].dynamic === 'letter') { + if (dynamicSlots.length >= 26) { + inputInRange = false; + } + // For letter type, use the next letter in sequence + newName = String.fromCharCode(97 + dynamicSlots.length); // 97 is ASCII for 'a' + } else { + // For number type, use baseName + index as before + newName = `${baseName}${dynamicSlots.length}`; } - // For letter type, use the next letter in sequence - newName = String.fromCharCode(97 + dynamicSlots.length); // 97 is ASCII for 'a' - } else { - // For number type, use baseName + index as before - newName = `${baseName}${dynamicSlots.length}`; - } - - if (inputInRange) { - // Insert the new empty input at the correct position - this.addInputAtPosition(newName, dynamicType, insertPosition); - // Renumber inputs after addition - this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic); + if (inputInRange) { + // Insert the new empty input at the correct position + this.addInputAtPosition(newName, dynamicType, insertPosition++, dynamicSlots[groupMember].isWidget, dynamicSlots[groupMember].shape); + // Renumber inputs after addition + this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic); + } } } } else if (isConnected === TypeSlotEvent.Disconnect) { - let hasEmptyDynamic = false; + let foundEmptyIndex = -1; + for (let idx = 0; idx < this.inputs.length; idx++) { const input = this.inputs[idx]; - const isDynamic = isDynamicInput(input.name); - if (hasEmptyDynamic && isDynamic) { - if (input.link === null) { - // last input is empty and this input is empty - this.removeInput(idx); - // continue with this ixd as it is now pointing to - // a new input - idx--; - continue; - } - // this input is dynamic and connected - this.swapInputs(idx, idx - 1); + + if (!isDynamicInput(input.name)) { continue; } - if (isDynamic && input.link === null) { - hasEmptyDynamic = true; + + if (!input.isConnected) { // Check if the input is empty + // remove empty input - but only when it's not the last one + const dynamicGroup = getDynamicGroup(input.name); + let isLast = true; + for (let i = idx + 1; i < this.inputs.length; i++) { + isLast &&= !this.inputs[i].name.startsWith(dynamicInputs[dynamicInputGroups[dynamicGroup][0]].baseName); + } + if (isLast) { + continue; + } + + for (let i = idx + 1; i < this.inputs.length; i++) { + this.swapInputs(i - 1, i); + } + const lastIdx = this.inputs.length - 1; + if (this.inputs[lastIdx].widget !== undefined) { + const widgetIdx = this.widgets.findIndex((w) => w.name === this.inputs[lastIdx].widget.name) + this.widgets.splice(widgetIdx, 1); + this.widgets_values?.splice(widgetIdx, 1); + } + this.removeInput(lastIdx); } } - this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic); + + // Renumber dynamic inputs to ensure proper ordering + for (const groupMember of dynamicInputGroups[dynamicInputs[0].dynamicGroup]) { + const baseName = dynamicInputs[groupMember].baseName; + this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic); + } + } this.setDirtyCanvas(true, true); @@ -230,6 +283,35 @@ app.registerExtension({ return result; }; + const onConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function(inputIndex, outputType, outputSlot, outputNode, outputIndex) { + const result = onRemoved?.apply(this, arguments) ?? true; + + if (this.inputs[inputIndex].isConnected) { + const pre_isProcessingConnection = isProcessingConnection; + isProcessingConnection = true; + this.disconnectInput(inputIndex, true); + isProcessingConnection = pre_isProcessingConnection; + } + return result; + } + + const onRemoved = nodeType.prototype.onRemoved; + nodeType.prototype.onRemoved = function () { + const result = onRemoved?.apply(this, arguments); + + // When this is called, the input links are already removed - but + // due to the implementation of the remove() method it might not + // have worked with the dynamic inputs. So we need to fix it here. + for (let i = this.inputs.length-1; i >= 0; i--) { + if ( this.inputs[i].isConnected) { + this.disconnectInput(i, true); + } + } + + return result; + } + // Method to swap two inputs in the "this.inputs" array by their indices nodeType.prototype.swapInputs = function(indexA, indexB) { // Validate indices @@ -239,13 +321,30 @@ app.registerExtension({ indexB >= this.inputs.length || indexA === indexB ) { - console.warn("Invalid input indices for swapping:", indexA, indexB); + console.error("Invalid input indices for swapping:", indexA, indexB); return; } + // reflect the swap with the widgets + if (this.inputs[indexA].widget !== undefined) { + if (this.inputs[indexB].widget === undefined) { + console.error("Bad swap: input A is a widget but input B is not", indexA, indexB); + } + const widgetIdxA = this.widgets.findIndex((w) => w.name === this.inputs[indexA].widget.name); + const widgetIdxB = this.widgets.findIndex((w) => w.name === this.inputs[indexB].widget.name); + [this.widgets[widgetIdxA].y, this.widgets[widgetIdxB].y] = [this.widgets[widgetIdxB].y, this.widgets[widgetIdxA].y]; + [this.widgets[widgetIdxA].last_y, this.widgets[widgetIdxB].last_y] = [this.widgets[widgetIdxB].last_y, this.widgets[widgetIdxA].last_y]; + [this.widgets[widgetIdxA], this.widgets[widgetIdxB]] = [this.widgets[widgetIdxB], this.widgets[widgetIdxA]]; + if (this.widgets_values) { + [this.widgets_values[widgetIdxA], this.widgets_values[widgetIdxB]] = [this.widgets_values[widgetIdxB], this.widgets_values[widgetIdxA]]; + } + } + // Swap the inputs in the array + [this.inputs[indexA].boundingRect, this.inputs[indexB].boundingRect] = [this.inputs[indexB].boundingRect, this.inputs[indexA].boundingRect]; + [this.inputs[indexA].pos, this.inputs[indexB].pos] = [this.inputs[indexB].pos, this.inputs[indexA].pos]; [this.inputs[indexA], this.inputs[indexB]] = [this.inputs[indexB], this.inputs[indexA]]; - updateSlotIndices(this.inputs, this.graph); // Refresh indices + updateSlotIndices(this); // Refresh indices // Redraw the node to ensure the graph updates properly // -> not needed as the calling method must do it! @@ -261,31 +360,32 @@ app.registerExtension({ const input = this.inputs[i]; const isDynamic = isDynamicInput(input.name); - if (isDynamic) { + if (isDynamic && input.name.startsWith(baseName)) { dynamicInputInfo.push({ index: i, + widgetIdx: input.widget !== undefined ? this.widgets.findIndex((w) => w.name === input.widget.name) : undefined, name: input.name, - connected: input.link !== null + connected: input.isConnected }); } } - // Sort connected first, then by index - dynamicInputInfo.sort((a, b) => { - if (a.connected !== b.connected) { - return b.connected ? 1 : -1; // Connected first - } - return a.index - b.index; // Keep order for same connection status - }); - // Just rename the inputs in place - don't remove/add to keep connections intact for (let i = 0; i < dynamicInputInfo.length; i++) { const info = dynamicInputInfo[i]; + const input = this.inputs[info.index]; const newName = dynamic === "number" ? `${baseName}${i}` : String.fromCharCode(97 + i); // 97 is ASCII for 'a' - if (this.inputs[info.index].name !== newName) { - this.inputs[info.index].name = newName; - this.inputs[info.index].localized_name = newName; + if (input.widget !== undefined) { + const widgetIdx = info.widgetIdx; + input.widget.name = newName; + this.widgets[widgetIdx].name = newName; + this.widgets[widgetIdx].label = newName; + } + + if (input.name !== newName) { + input.name = newName; + input.localized_name = newName; } } };