diff --git a/pyproject.toml b/pyproject.toml index 2ea91e0..0e6b8ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "0.2.2" +version = "0.3.0" 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. @@ -47,11 +47,12 @@ minversion = "8.0" pythonpath = [ "src", "../..", # Path to parent directory containing comfy module + "." ] testpaths = [ "tests", ] -# python_files = ["test_*.py"] +#python_files = ["test_*.py"] python_files = ["test_boolean_nodes.py"] [tool.mypy] diff --git a/src/basic_data_handling/boolean_nodes.py b/src/basic_data_handling/boolean_nodes.py index 8f925f0..b0685f2 100644 --- a/src/basic_data_handling/boolean_nodes.py +++ b/src/basic_data_handling/boolean_nodes.py @@ -145,19 +145,19 @@ class BooleanXor(ComfyNodeABC): NODE_CLASS_MAPPINGS = { - "Basic data handling: BooleanAnd": BooleanAnd, - "Basic data handling: BooleanNand": BooleanNand, - "Basic data handling: BooleanNor": BooleanNor, - "Basic data handling: BooleanNot": BooleanNot, - "Basic data handling: BooleanOr": BooleanOr, - "Basic data handling: BooleanXor": BooleanXor, + "Basic data handling: Boolean And": BooleanAnd, + "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: Boolean Xor": BooleanXor, } NODE_DISPLAY_NAME_MAPPINGS = { - "Basic data handling: BooleanAnd": "and", - "Basic data handling: BooleanNand": "nand", - "Basic data handling: BooleanNor": "nor", - "Basic data handling: BooleanNot": "not", - "Basic data handling: BooleanOr": "or", - "Basic data handling: BooleanXor": "xor", + "Basic data handling: Boolean And": "and", + "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: Boolean Xor": "xor", } diff --git a/src/basic_data_handling/comparison_nodes.py b/src/basic_data_handling/comparison_nodes.py index da4f7de..e7cef2d 100644 --- a/src/basic_data_handling/comparison_nodes.py +++ b/src/basic_data_handling/comparison_nodes.py @@ -58,7 +58,7 @@ class NotEqual(ComfyNodeABC): return (value1 != value2,) -class LessThan: +class LessThan(ComfyNodeABC): """ Checks if the first value is less than the second. @@ -84,7 +84,7 @@ class LessThan: return (value1 < value2,) -class LessThanOrEqual: +class LessThanOrEqual(ComfyNodeABC): """ Checks if the first value is less than or equal to the second. @@ -110,7 +110,7 @@ class LessThanOrEqual: return (value1 <= value2,) -class GreaterThan: +class GreaterThan(ComfyNodeABC): """ Checks if the first value is greater than the second. @@ -136,7 +136,7 @@ class GreaterThan: return (value1 > value2,) -class GreaterThanOrEqual: +class GreaterThanOrEqual(ComfyNodeABC): """ Checks if the first value is greater than or equal to the second. @@ -162,7 +162,7 @@ class GreaterThanOrEqual: return (value1 >= value2,) -class IsNull: +class IsNull(ComfyNodeABC): """ Checks if a value is None/null. @@ -187,7 +187,7 @@ class IsNull: return (value is None,) -class NumberInRange: +class NumberInRange(ComfyNodeABC): """ Checks if a number is within a specified range. @@ -223,7 +223,7 @@ class NumberInRange: return (min_check and max_check,) -class CompareLength: +class CompareLength(ComfyNodeABC): """ Compares the length of a container (string, list, etc) with a value. @@ -269,7 +269,7 @@ class CompareLength: raise ValueError(f"Unknown operator: {operator}") -class StringComparison: +class StringComparison(ComfyNodeABC): """ Compares two strings using a selected comparison operator. diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index b7a90a9..7d96b91 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -22,7 +22,7 @@ class IfElse(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("result",) - CATEGORY = "Basic/Flow Control" + CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -38,6 +38,90 @@ class IfElse(ComfyNodeABC): return (if_true if condition else if_false,) +class IfElifElse(ComfyNodeABC): + """ + Implements a conditional branch (if/elif/else) in the workflow. + + This node takes a condition input and two value inputs. If the condition + evaluates to True, the first value is returned; otherwise, the elif (else if) + is evaluated and returned when true. This continues for all additional elifs. + When none is true, the value of the else is returned. + This allows conditional data flow in ComfyUI workflows. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "if": (IO.BOOLEAN, {}), + "then": (IO.ANY, {"lazy": True}), + }, + "optional": { + "elif_0": (IO.BOOLEAN, {"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,) + RETURN_NAMES = ("result",) + CATEGORY = "Basic/flow control" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "execute" + + def check_lazy_status(self, **kwargs) -> list[str]: + needed = [] + + # Check if condition + if kwargs.get("if", False) and kwargs.get("then") is None: + needed.append("then") + return needed # If main condition is true, we only need "then" + + # Check each elif condition + elif_index = 0 + while True: + elif_key = f"elif_{elif_index}" + then_key = f"then_{elif_index}" + + if elif_key not in kwargs: + break + + # If this elif condition is true and its value is None, add it to needed + if kwargs.get(elif_key, False) and kwargs.get(then_key) is None: + needed.append(then_key) + return needed # If any elif is true, we need only its then value + + elif_index += 1 + + # If no conditions were true, check if we need the else + if "else" in kwargs and kwargs["else"] is None: + needed.append("else") + + return needed + + def execute(self, **kwargs) -> tuple[Any]: + # Check if the main condition is true + if kwargs.get("if", False): + return (kwargs.get("then"),) + + # Check each elif condition + elif_index = 0 + while True: + elif_key = f"elif_{elif_index}" + then_key = f"then_{elif_index}" + + if elif_key not in kwargs: + break + + if kwargs.get(elif_key, False): + return (kwargs.get(then_key),) + + elif_index += 1 + + # If no conditions were true, return the else value + return (kwargs.get("else"),) + + class SwitchCase(ComfyNodeABC): """ Implements a switch/case selection in the workflow. @@ -56,59 +140,66 @@ class SwitchCase(ComfyNodeABC): return { "required": { "selector": (IO.INT, {"default": 0, "min": 0}), - "case_0": (IO.ANY, {"lazy": True}), - "case_1": (IO.ANY, {"lazy": True}), + "case_0": (IO.ANY, {"lazy": True, "_dynamic": "number"}), }, "optional": { - "case_2": (IO.ANY, {"lazy": True}), - "case_3": (IO.ANY, {"lazy": True}), - "case_4": (IO.ANY, {"lazy": True}), "default": (IO.ANY, {"lazy": True}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("result",) - CATEGORY = "Basic/Flow Control" + CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" - def check_lazy_status(self, selector: int, case_0: Any, case_1: Any, - case_2: Any = None, case_3: Any = None, - case_4: Any = None, default: Any = None) -> list[str]: + def check_lazy_status(self, selector: int, **kwargs) -> list[str]: needed = [] - if case_0 is None and selector == 0: - needed.append("case_0") - if case_1 is None and selector == 1: - needed.append("case_1") - if case_2 is None and selector == 2: - needed.append("case_2") - if case_3 is None and selector == 3: - needed.append("case_3") - if case_4 is None and selector == 4: - needed.append("case_4") - if default is None and not 0 <= selector <= 4: + + # Check for needed case inputs based on selector + case_count = 0 + for key, value in kwargs.items(): + if key.startswith("case_"): + try: + case_index = int(key.split("_")[1]) + case_count = max(case_count, case_index + 1) + if value is None and selector == case_index: + needed.append(key) + except ValueError: + pass # Not a numeric case key + + # Check if default is needed when selector is out of range + if "default" in kwargs and kwargs["default"] is None and not 0 <= selector < case_count: needed.append("default") + return needed - def execute(self, selector: int, case_0: Any, case_1: Any, - case_2: Any = None, case_3: Any = None, - case_4: Any = None, default: Any = None) -> tuple[Any]: - cases = [case_0, case_1, case_2, case_3, case_4] + def execute(self, selector: int, **kwargs) -> tuple[Any]: + # Build cases array from all case_X inputs + cases = [] + for i in range(len(kwargs)): + case_key = f"case_{i}" + if case_key in kwargs: + cases.append(kwargs[case_key]) + else: + break + # Return the selected case if valid if 0 <= selector < len(cases) and cases[selector] is not None: return (cases[selector],) # If selector is out of range or the selected case is None, return default - return (default,) + return (kwargs.get("default"),) NODE_CLASS_MAPPINGS = { "Basic data handling: IfElse": IfElse, + "Basic data handling: IfElifElse": IfElifElse, "Basic data handling: SwitchCase": SwitchCase, } NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: IfElse": "if/else", + "Basic data handling: IfElifElse": "if/elif/.../else", "Basic data handling: SwitchCase": "switch/case", } diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index f55c084..346ba61 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -4,6 +4,141 @@ from comfy.comfy_types.node_typing import IO, ComfyNodeABC INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes + +class DataListCreate(ComfyNodeABC): + """ + Creates a new Data List from items. + + This node creates and returns a Data List. The list of items is dynamically + extended based on the number of inputs provided. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.ANY, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + OUTPUT_IS_LIST = (True,) + + def create_list(self, **kwargs: list[Any]) -> tuple[list]: + return (list(kwargs.values()),) + + +class DataListCreateFromBoolean(ComfyNodeABC): + """ + Creates a new Data List from BOOLEAN items. + + This node creates and returns a Data List. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + OUTPUT_IS_LIST = (True,) + + def create_list(self, **kwargs: list[Any]) -> tuple[list]: + return ([bool(value) for value in kwargs.values()],) + + +class DataListCreateFromFloat(ComfyNodeABC): + """ + Creates a new Data List from FLOAT items. + + This node creates and returns a Data List. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.FLOAT, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + OUTPUT_IS_LIST = (True,) + + def create_list(self, **kwargs: list[Any]) -> tuple[list]: + return ([float(value) for value in kwargs.values()],) + + +class DataListCreateFromInt(ComfyNodeABC): + """ + Creates a new Data List from INT items. + + This node creates and returns a Data List. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.INT, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + OUTPUT_IS_LIST = (True,) + + def create_list(self, **kwargs: list[Any]) -> tuple[list]: + return ([int(value) for value in kwargs.values()],) + + +class DataListCreateFromString(ComfyNodeABC): + """ + Creates a new Data List from STRING items. + + This node creates and returns a Data List. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.STRING, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + OUTPUT_IS_LIST = (True,) + + def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]: + return ([str(value) for value in kwargs.values()],) + + class DataListAppend(ComfyNodeABC): """ Adds an item to the end of a list. @@ -22,7 +157,7 @@ class DataListAppend(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "append" INPUT_IS_LIST = True @@ -54,7 +189,7 @@ class DataListContains(ComfyNodeABC): RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("contains",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "contains" INPUT_IS_LIST = True @@ -84,7 +219,7 @@ class DataListCount(ComfyNodeABC): RETURN_TYPES = (IO.INT,) RETURN_NAMES = ("count",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "count" INPUT_IS_LIST = True @@ -94,27 +229,6 @@ class DataListCount(ComfyNodeABC): return (kwargs.get('list', []).count(value),) -class DataListCreateEmpty(ComfyNodeABC): - """ - Creates a new empty list. - - This node creates and returns an empty list. - """ - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = (IO.ANY,) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "create_empty" - OUTPUT_IS_LIST = (True,) - - def create_empty(self) -> tuple[list]: - return ([],) - - class DataListExtend(ComfyNodeABC): """ Extends a list by appending elements from another list. @@ -133,7 +247,7 @@ class DataListExtend(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "extend" INPUT_IS_LIST = True @@ -145,10 +259,10 @@ class DataListExtend(ComfyNodeABC): class DataListFilter(ComfyNodeABC): """ - Filters a data list using boolean values. + 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 a new data list containing only the elements from the value list where the + This node takes a value Data List and a filter Data List (containing only boolean values). + It returns a new Data List containing only the elements from the value list where the corresponding element in the filter list is False. If the lists have different lengths, the last element of the shorter list is repeated @@ -165,7 +279,7 @@ class DataListFilter(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("filtered_list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "filter_data" INPUT_IS_LIST = True @@ -200,7 +314,7 @@ class DataListGetItem(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("item",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_item" INPUT_IS_LIST = True @@ -236,7 +350,7 @@ class DataListIndex(ComfyNodeABC): RETURN_TYPES = (IO.INT,) RETURN_NAMES = ("index",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "list_index" INPUT_IS_LIST = True @@ -274,7 +388,7 @@ class DataListInsert(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "insert" INPUT_IS_LIST = True @@ -302,7 +416,7 @@ class DataListLength(ComfyNodeABC): RETURN_TYPES = (IO.INT,) RETURN_NAMES = ("length",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "length" INPUT_IS_LIST = True @@ -329,7 +443,7 @@ class DataListMax(ComfyNodeABC): RETURN_TYPES = (IO.NUMBER,) RETURN_NAMES = ("max",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "find_max" INPUT_IS_LIST = True @@ -365,7 +479,7 @@ class DataListMin(ComfyNodeABC): RETURN_TYPES = (IO.NUMBER,) RETURN_NAMES = ("min",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "find_min" INPUT_IS_LIST = True @@ -405,7 +519,7 @@ class DataListPop(ComfyNodeABC): RETURN_TYPES = (IO.ANY, IO.ANY) RETURN_NAMES = ("list", "item") - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop" INPUT_IS_LIST = True @@ -439,7 +553,7 @@ class DataListRemove(ComfyNodeABC): RETURN_TYPES = (IO.ANY, IO.BOOLEAN,) RETURN_NAMES = ("list", "success",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "remove" INPUT_IS_LIST = True @@ -471,7 +585,7 @@ class DataListReverse(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "reverse" INPUT_IS_LIST = True @@ -502,7 +616,7 @@ class DataListSetItem(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "set_item" INPUT_IS_LIST = True @@ -542,7 +656,7 @@ class DataListSlice(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "slice" INPUT_IS_LIST = True @@ -578,7 +692,7 @@ class DataListSort(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "sort" INPUT_IS_LIST = True @@ -596,7 +710,7 @@ class DataListZip(ComfyNodeABC): """ Combines multiple lists element-wise. - This node takes multiple data lists as input and returns a new data list + This node takes multiple Data Lists as input and returns a new Data List where each item is a list containing the corresponding elements from the input lists. The length of the output list will be equal to the length of the shortest input list. """ @@ -615,7 +729,7 @@ class DataListZip(ComfyNodeABC): RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "zip_lists" INPUT_IS_LIST = True @@ -637,9 +751,9 @@ class DataListZip(ComfyNodeABC): class DataListToList(ComfyNodeABC): """ - Converts a ComfyUI data list into a LIST object. + Converts a ComfyUI Data List into a LIST object. - This node takes a data list input (which is typically a list of items with the same type) + This node takes a Data List input (which is typically a list of items with the same type) and converts it to a LIST object (a Python list as a single variable). """ @classmethod @@ -651,7 +765,7 @@ class DataListToList(ComfyNodeABC): } RETURN_TYPES = ("LIST",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert" INPUT_IS_LIST = True @@ -662,9 +776,9 @@ class DataListToList(ComfyNodeABC): class DataListToSet(ComfyNodeABC): """ - Converts a ComfyUI data list into a LIST object. + Converts a ComfyUI Data List into a LIST object. - This node takes a data list input (which is typically a list of items with the same type) + This node takes a Data List input (which is typically a list of items with the same type) and converts it to a LIST object (a Python list as a single variable). """ @classmethod @@ -676,7 +790,7 @@ class DataListToSet(ComfyNodeABC): } RETURN_TYPES = ("SET",) - CATEGORY = "Basic/data list" + CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert" INPUT_IS_LIST = True @@ -686,10 +800,14 @@ class DataListToSet(ComfyNodeABC): NODE_CLASS_MAPPINGS = { + "Basic data handling: DataListCreate": DataListCreate, + "Basic data handling: DataListCreateFromBoolean": DataListCreateFromBoolean, + "Basic data handling: DataListCreateFromFloat": DataListCreateFromFloat, + "Basic data handling: DataListCreateFromInt": DataListCreateFromInt, + "Basic data handling: DataListCreateFromString": DataListCreateFromString, "Basic data handling: DataListAppend": DataListAppend, "Basic data handling: DataListContains": DataListContains, "Basic data handling: DataListCount": DataListCount, - "Basic data handling: DataListCreateEmpty": DataListCreateEmpty, "Basic data handling: DataListExtend": DataListExtend, "Basic data handling: DataListFilter": DataListFilter, "Basic data handling: DataListGetItem": DataListGetItem, @@ -710,10 +828,14 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { + "Basic data handling: DataListCreate": "create Data List", + "Basic data handling: DataListCreateFromBoolean": "create Data List from BOOLEANs", + "Basic data handling: DataListCreateFromFloat": "create Data List from FLOATs", + "Basic data handling: DataListCreateFromInt": "create Data List from INTs", + "Basic data handling: DataListCreateFromString": "create Data List from STRINGs", "Basic data handling: DataListAppend": "append", "Basic data handling: DataListContains": "contains", "Basic data handling: DataListCount": "count", - "Basic data handling: DataListCreateEmpty": "create empty", "Basic data handling: DataListExtend": "extend", "Basic data handling: DataListFilter": "filter", "Basic data handling: DataListGetItem": "get item", diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index f9c22f0..a23a17f 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -2,23 +2,165 @@ from typing import Any from inspect import cleandoc from comfy.comfy_types.node_typing import IO, ComfyNodeABC + class DictCreate(ComfyNodeABC): """ Creates a new empty dictionary. This node creates and returns a new empty dictionary object. """ + EXPERIMENTAL = True @classmethod def INPUT_TYPES(cls): - return {"required": {}} + return { + "optional": { + "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), + "value_0": (IO.ANY, {"_dynamic": "number", "_dynamicGroup": 0}), + } + } RETURN_TYPES = ("DICT",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" - def create(self) -> tuple[dict]: - return ({},) + def create(self, **kwargs: list[Any]) -> tuple[dict]: + result = {} + # Process all key_X/value_X pairs from dynamic inputs + for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs + key_name = f"key_{i}" + value_name = f"value_{i}" + if key_name in kwargs and value_name in kwargs: + result[kwargs[key_name]] = kwargs[value_name] + return (result,) + + +class DictCreateFromBoolean(ComfyNodeABC): + """ + Creates a new empty dictionary. + + This node creates and returns a new empty dictionary object. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), + "value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self, **kwargs: list[Any]) -> tuple[dict]: + result = {} + # Process all key_X/value_X pairs from dynamic inputs + for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs + key_name = f"key_{i}" + value_name = f"value_{i}" + if key_name in kwargs and value_name in kwargs: + result[kwargs[key_name]] = bool(kwargs[value_name]) + return (result,) + + +class DictCreateFromFloat(ComfyNodeABC): + """ + Creates a new empty dictionary. + + This node creates and returns a new empty dictionary object. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), + "value_0": (IO.FLOAT, {"_dynamic": "number", "_dynamicGroup": 0}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self, **kwargs: list[Any]) -> tuple[dict]: + result = {} + # Process all key_X/value_X pairs from dynamic inputs + for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs + key_name = f"key_{i}" + value_name = f"value_{i}" + if key_name in kwargs and value_name in kwargs: + result[kwargs[key_name]] = float(kwargs[value_name]) + return (result,) + + +class DictCreateFromInt(ComfyNodeABC): + """ + Creates a new empty dictionary. + + This node creates and returns a new empty dictionary object. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), + "value_0": (IO.INT, {"_dynamic": "number", "_dynamicGroup": 0}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self, **kwargs: list[Any]) -> tuple[dict]: + result = {} + # Process all key_X/value_X pairs from dynamic inputs + for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs + key_name = f"key_{i}" + value_name = f"value_{i}" + if key_name in kwargs and value_name in kwargs: + result[kwargs[key_name]] = int(kwargs[value_name]) + return (result,) + + +class DictCreateFromString(ComfyNodeABC): + """ + Creates a new empty dictionary. + + This node creates and returns a new empty dictionary object. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), + "value_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self, **kwargs: list[Any]) -> tuple[dict]: + result = {} + # Process all key_X/value_X pairs from dynamic inputs + for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs + key_name = f"key_{i}" + value_name = f"value_{i}" + if key_name in kwargs and value_name in kwargs: + result[kwargs[key_name]] = str(kwargs[value_name]) + return (result,) class DictCreateFromItemsDataList(ComfyNodeABC): @@ -706,6 +848,10 @@ class DictValues(ComfyNodeABC): NODE_CLASS_MAPPINGS = { "Basic data handling: DictCreate": DictCreate, + "Basic data handling: DictCreateFromBoolean": DictCreateFromBoolean, + "Basic data handling: DictCreateFromFloat": DictCreateFromFloat, + "Basic data handling: DictCreateFromInt": DictCreateFromInt, + "Basic data handling: DictCreateFromString": DictCreateFromString, "Basic data handling: DictCreateFromItemsDataList": DictCreateFromItemsDataList, "Basic data handling: DictCreateFromItemsList": DictCreateFromItemsList, "Basic data handling: DictCreateFromLists": DictCreateFromLists, @@ -732,7 +878,11 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "Basic data handling: DictCreate": "create empty", + "Basic data handling: DictCreate": "create DICT", + "Basic data handling: DictCreateFromBoolean": "create DICT from BOOLEANs", + "Basic data handling: DictCreateFromFloat": "create DICT from FLOATs", + "Basic data handling: DictCreateFromInt": "create DICT from INTs", + "Basic data handling: DictCreateFromString": "create DICT from STRINGs", "Basic data handling: DictCreateFromItemsDataList": "create from items (data list)", "Basic data handling: DictCreateFromItemsList": "create from items (LIST)", "Basic data handling: DictCreateFromLists": "create from LISTs", @@ -740,7 +890,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: DictContainsKey": "contains key", "Basic data handling: DictExcludeKeys": "exclude keys", "Basic data handling: DictFilterByKeys": "filter by keys", - "Basic data handling: DictFromKeys": "fromkeys", + "Basic data handling: DictFromKeys": "from keys", "Basic data handling: DictGet": "get", "Basic data handling: DictGetKeysValues": "get keys values", "Basic data handling: DictGetMultiple": "get multiple", diff --git a/src/basic_data_handling/float_nodes.py b/src/basic_data_handling/float_nodes.py index b075b0c..3241ed8 100644 --- a/src/basic_data_handling/float_nodes.py +++ b/src/basic_data_handling/float_nodes.py @@ -1,6 +1,31 @@ from inspect import cleandoc from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +class FloatCreate(ComfyNodeABC): + """ + Create a FLOAT from a STRING widget. + + The input string must be a valid floating-point number and will be + directly converted to a FLOAT without any further processing. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {"default": "0.0", "widgetType": "STRING"}), + } + } + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/FLOAT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self, value: str) -> tuple[float]: + return (float(value),) + + class FloatAdd(ComfyNodeABC): """ Adds two floating-point numbers. @@ -124,6 +149,8 @@ class FloatDivideSafe(ComfyNodeABC): def divide(self, float1: float, float2: float) -> tuple[float]: if float2 == 0.0: + if float1 == 0.0: + return (float('nan'),) return (float('inf') if float1 > 0 else float('-inf'),) return (float1 / float2,) @@ -276,6 +303,7 @@ class FloatRound(ComfyNodeABC): NODE_CLASS_MAPPINGS = { + "Basic data handling: FloatCreate": FloatCreate, "Basic data handling: FloatAdd": FloatAdd, "Basic data handling: FloatSubtract": FloatSubtract, "Basic data handling: FloatMultiply": FloatMultiply, @@ -290,11 +318,12 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { + "Basic data handling: FloatCreate": "create FLOAT", "Basic data handling: FloatAdd": "add", "Basic data handling: FloatSubtract": "subtract", "Basic data handling: FloatMultiply": "multiply", "Basic data handling: FloatDivide": "divide", - "Basic data handling: FloatDivideSafe": "divide (division by zero safe)", + "Basic data handling: FloatDivideSafe": "divide (zero safe)", "Basic data handling: FloatAsIntegerRatio": "integer ratio", "Basic data handling: FloatFromHex": "from hex", "Basic data handling: FloatHex": "to hex", diff --git a/src/basic_data_handling/int_nodes.py b/src/basic_data_handling/int_nodes.py index d044d7b..9a9c52f 100644 --- a/src/basic_data_handling/int_nodes.py +++ b/src/basic_data_handling/int_nodes.py @@ -2,6 +2,61 @@ from inspect import cleandoc from typing import Literal from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +class IntCreate(ComfyNodeABC): + """ + Create an INT from a STRING widget. + + The input string must be a valid integer number and will be + directly converted to an INT without any further processing. + + Strings starting with "0b" are interpreted as binary numbers, + "0o" as octal numbers, and "0x" as hexadecimal numbers. + + Note: This doesn't handle ones' complement as the data size is unknown. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {"default": "0", "widgetType": "STRING"}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/INT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self, value: str) -> tuple[int]: + return (int(value, 0),) # Automatically detects base using prefixes + + +class IntCreateWithBase(ComfyNodeABC): + """ + Create an INT from a STRING with a given base. + + The input string must be a valid integer number and will be + directly converted to an INT without any further processing. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.STRING, {"default": "0"}), + "base": (IO.INT, {"default": "10", "min": 2}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/INT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self, value: str, base: int) -> tuple[int]: + return (int(value, base),) + + class IntAdd(ComfyNodeABC): """ Adds two integers. @@ -286,6 +341,8 @@ class IntToBytes(ComfyNodeABC): NODE_CLASS_MAPPINGS = { + "Basic data handling: IntCreate": IntCreate, + "Basic data handling: IntCreateWithBase": IntCreateWithBase, "Basic data handling: IntAdd": IntAdd, "Basic data handling: IntSubtract": IntSubtract, "Basic data handling: IntMultiply": IntMultiply, @@ -300,11 +357,13 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { + "Basic data handling: IntCreate": "create INT", + "Basic data handling: IntCreateWithBase": "create INT with base", "Basic data handling: IntAdd": "add", "Basic data handling: IntSubtract": "subtract", "Basic data handling: IntMultiply": "multiply", "Basic data handling: IntDivide": "divide", - "Basic data handling: IntDivideSafe": "divide (division by zero safe)", + "Basic data handling: IntDivideSafe": "divide (zero safe)", "Basic data handling: IntBitCount": "bit count", "Basic data handling: IntBitLength": "bit length", "Basic data handling: IntFromBytes": "from bytes", diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index b5d1a7f..c645967 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -4,23 +4,129 @@ from comfy.comfy_types.node_typing import IO, ComfyNodeABC INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes -class ListEmpty(ComfyNodeABC): + +class ListCreate(ComfyNodeABC): """ - Create an empty LIST. + Creates a new LIST from items. + + This node creates and returns a LIST. The list of items is dynamically + extended based on the number of inputs provided. """ @classmethod def INPUT_TYPES(cls): return { - "required": {} + "optional": { + "item_0": (IO.ANY, {"_dynamic": "number"}), + } } RETURN_TYPES = ("LIST",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "create" + FUNCTION = "create_list" - def create(self) -> tuple[list[Any]]: - return ([],) + def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]: + return (list(kwargs.values()),) + + +class ListCreateFromBoolean(ComfyNodeABC): + """ + Creates a new LIST from BOOLEAN items. + + This node creates and returns a LIST. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + + def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]: + return ([bool(value) for value in kwargs.values()],) + + +class ListCreateFromFloat(ComfyNodeABC): + """ + Creates a new LIST from FLOAT items. + + This node creates and returns a LIST. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.FLOAT, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + + def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]: + return ([float(value) for value in kwargs.values()],) + + +class ListCreateFromInt(ComfyNodeABC): + """ + Creates a new LIST from INT items. + + This node creates and returns a LIST. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.INT, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + + def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]: + return ([int(value) for value in kwargs.values()],) + + +class ListCreateFromString(ComfyNodeABC): + """ + Creates a new LIST from STRING items. + + This node creates and returns a LIST. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.STRING, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_list" + + def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]: + return ([str(value) for value in kwargs.values()],) class ListAppend(ComfyNodeABC): @@ -552,7 +658,11 @@ class ListToSet(ComfyNodeABC): NODE_CLASS_MAPPINGS = { - "Basic data handling: ListEmpty": ListEmpty, + "Basic data handling: ListCreate": ListCreate, + "Basic data handling: ListCreateFromBoolean": ListCreateFromBoolean, + "Basic data handling: ListCreateFromFloat": ListCreateFromFloat, + "Basic data handling: ListCreateFromInt": ListCreateFromInt, + "Basic data handling: ListCreateFromString": ListCreateFromString, "Basic data handling: ListAppend": ListAppend, "Basic data handling: ListContains": ListContains, "Basic data handling: ListCount": ListCount, @@ -574,7 +684,11 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "Basic data handling: ListEmpty": "create empty", + "Basic data handling: ListCreate": "create LIST", + "Basic data handling: ListCreateFromBoolean": "create LIST from BOOLEANs", + "Basic data handling: ListCreateFromFloat": "create LIST from FLOATs", + "Basic data handling: ListCreateFromInt": "create LIST from INTs", + "Basic data handling: ListCreateFromString": "create LIST from STRINGs", "Basic data handling: ListAppend": "append", "Basic data handling: ListContains": "contains", "Basic data handling: ListCount": "count", @@ -591,6 +705,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: ListSetItem": "set item", "Basic data handling: ListSlice": "slice", "Basic data handling: ListSort": "sort", - "Basic data handling: ListToDataList": "convert to data list", + "Basic data handling: ListToDataList": "convert to Data List", "Basic data handling: ListToSet": "convert to SET", } diff --git a/src/basic_data_handling/math_formula_node.py b/src/basic_data_handling/math_formula_node.py index 14058ad..62c4d04 100644 --- a/src/basic_data_handling/math_formula_node.py +++ b/src/basic_data_handling/math_formula_node.py @@ -21,16 +21,11 @@ class MathFormula(ComfyNodeABC): return { "required": { "formula": (IO.STRING, {"default": "a + b"}), - "a": (IO.NUMBER, {"default": 0.0}), - }, - "optional": { - "b": (IO.NUMBER, {"default": 0.0}), - "c": (IO.NUMBER, {"default": 0.0}), - "d": (IO.NUMBER, {"default": 0.0}), + "a": (IO.NUMBER, {"default": 0.0, "_dynamic": "letter"}), }, } - RETURN_TYPES = (IO.NUMBER,) + RETURN_TYPES = (IO.FLOAT,) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "evaluate" diff --git a/src/basic_data_handling/math_nodes.py b/src/basic_data_handling/math_nodes.py index ca2b502..5c6a299 100644 --- a/src/basic_data_handling/math_nodes.py +++ b/src/basic_data_handling/math_nodes.py @@ -13,7 +13,7 @@ class MathAbs(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), } } @@ -22,8 +22,8 @@ class MathAbs(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: Union[float, int]) -> tuple[Union[float, int]]: - return (abs(value),) + def calculate(self, value: Union[float, int, str]) -> tuple[Union[float, int]]: + return (abs(float(value)),) class MathAcos(ComfyNodeABC): @@ -37,7 +37,7 @@ class MathAcos(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -47,8 +47,8 @@ class MathAcos(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]: - result = math.acos(value) + def calculate(self, value: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.acos(float(value)) if unit == "degrees": result = math.degrees(result) return (result,) @@ -65,7 +65,7 @@ class MathAsin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -75,8 +75,8 @@ class MathAsin(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]: - result = math.asin(value) + def calculate(self, value: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.asin(float(value)) if unit == "degrees": result = math.degrees(result) return (result,) @@ -93,7 +93,7 @@ class MathAtan(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -103,8 +103,8 @@ class MathAtan(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]: - result = math.atan(value) + def calculate(self, value: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.atan(float(value)) if unit == "degrees": result = math.degrees(result) return (result,) @@ -122,8 +122,8 @@ class MathAtan2(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "y": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - "x": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "y": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "x": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -133,8 +133,8 @@ class MathAtan2(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, y: float, x: float, unit: Literal["radians", "degrees"]) -> tuple[float]: - result = math.atan2(y, x) + def calculate(self, y: Union[float, int, str], x: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.atan2(float(y), float(x)) if unit == "degrees": result = math.degrees(result) return (result,) @@ -150,7 +150,7 @@ class MathCeil(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), } } @@ -159,8 +159,8 @@ class MathCeil(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float) -> tuple[int]: - return (math.ceil(value),) + def calculate(self, value: Union[float, int, str]) -> tuple[int]: + return (math.ceil(float(value)),) class MathCos(ComfyNodeABC): @@ -175,7 +175,7 @@ class MathCos(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -185,11 +185,11 @@ class MathCos(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + def calculate(self, angle: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]: if unit == "degrees": # Convert degrees to radians - angle = math.radians(angle) - return (math.cos(angle),) + angle = math.radians(float(angle)) + return (math.cos(float(angle)),) class MathDegrees(ComfyNodeABC): @@ -202,7 +202,7 @@ class MathDegrees(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "radians": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "radians": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), } } @@ -211,8 +211,8 @@ class MathDegrees(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, radians: float) -> tuple[float]: - return (math.degrees(radians),) + def calculate(self, radians: Union[float, int, str]) -> tuple[float]: + return (math.degrees(float(radians)),) class MathE(ComfyNodeABC): @@ -244,7 +244,7 @@ class MathExp(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), } } @@ -253,8 +253,8 @@ class MathExp(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float) -> tuple[float]: - return (math.exp(value),) + def calculate(self, value: Union[float, int, str]) -> tuple[float]: + return (math.exp(float(value)),) class MathFloor(ComfyNodeABC): @@ -267,7 +267,7 @@ class MathFloor(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), } } @@ -276,8 +276,8 @@ class MathFloor(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float) -> tuple[int]: - return (math.floor(value),) + def calculate(self, value: Union[float, int, str]) -> tuple[int]: + return (math.floor(float(value)),) class MathLog(ComfyNodeABC): @@ -291,10 +291,10 @@ class MathLog(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING"}), }, "optional": { - "base": (IO.NUMBER, {"default": math.e, "min": 0.0000001, "widgetType": "FLOAT"}), + "base": (IO.NUMBER, {"default": math.e, "min": 0.0000001, "widgetType": "STRING"}), } } @@ -303,11 +303,8 @@ class MathLog(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float, base: float = math.e) -> tuple[float]: - if base == math.e: - return (math.log(value),) - else: - return (math.log(value, base),) + def calculate(self, value: Union[float, int, str], base: Union[float, int, str] = math.e) -> tuple[float]: + return (math.log(float(value), float(base)),) class MathLog10(ComfyNodeABC): @@ -320,7 +317,7 @@ class MathLog10(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING"}), } } @@ -329,8 +326,8 @@ class MathLog10(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float) -> tuple[float]: - return (math.log10(value),) + def calculate(self, value: Union[float, int, str]) -> tuple[float]: + return (math.log10(float(value)),) class MathMax(ComfyNodeABC): @@ -344,8 +341,8 @@ class MathMax(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), } } @@ -354,8 +351,8 @@ class MathMax(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value1: Union[float, int], value2: Union[float, int]) -> tuple[Union[float, int]]: - return (max(value1, value2),) + def calculate(self, value1: Union[float, int, str], value2: Union[float, int, str]) -> tuple[Union[float, int]]: + return (max(float(value1), float(value2)),) class MathMin(ComfyNodeABC): @@ -369,8 +366,8 @@ class MathMin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), } } @@ -379,8 +376,8 @@ class MathMin(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value1: Union[float, int], value2: Union[float, int]) -> tuple[Union[float, int]]: - return (min(value1, value2),) + def calculate(self, value1: Union[float, int, str], value2: Union[float, int, str]) -> tuple[Union[float, int]]: + return (min(float(value1), float(value2)),) class MathPi(ComfyNodeABC): @@ -412,7 +409,7 @@ class MathRadians(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "degrees": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "degrees": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), } } @@ -421,8 +418,8 @@ class MathRadians(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, degrees: float) -> tuple[float]: - return (math.radians(degrees),) + def calculate(self, degrees: Union[float, int, str]) -> tuple[float]: + return (math.radians(float(degrees)),) class MathSin(ComfyNodeABC): @@ -437,7 +434,7 @@ class MathSin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -447,11 +444,11 @@ class MathSin(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + def calculate(self, angle: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]: if unit == "degrees": # Convert degrees to radians - angle = math.radians(angle) - return (math.sin(angle),) + angle = math.radians(float(angle)) + return (math.sin(float(angle)),) class MathSqrt(ComfyNodeABC): @@ -464,7 +461,7 @@ class MathSqrt(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "STRING"}), } } @@ -473,8 +470,8 @@ class MathSqrt(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float) -> tuple[float]: - return (math.sqrt(value),) + def calculate(self, value: Union[float, int, str]) -> tuple[float]: + return (math.sqrt(float(value)),) class MathTan(ComfyNodeABC): @@ -489,7 +486,7 @@ class MathTan(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -499,16 +496,16 @@ class MathTan(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + def calculate(self, angle: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]: if unit == "degrees": # Convert degrees to radians - angle = math.radians(angle) + angle = math.radians(float(angle)) # Handle specific angles that would result in division by zero - if abs(math.cos(angle)) < 1e-10: + if abs(math.cos(float(angle))) < 1e-10: raise ValueError("Tangent is undefined at this angle (division by zero)") - return (math.tan(angle),) + return (math.tan(float(angle)),) NODE_CLASS_MAPPINGS = { diff --git a/src/basic_data_handling/path_nodes.py b/src/basic_data_handling/path_nodes.py index ae7a2a1..f825683 100644 --- a/src/basic_data_handling/path_nodes.py +++ b/src/basic_data_handling/path_nodes.py @@ -20,8 +20,8 @@ class PathAbspath(ComfyNodeABC): } RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("absolute_path",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("absolute path",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_abspath" @@ -46,7 +46,7 @@ class PathBasename(ComfyNodeABC): RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("basename",) - CATEGORY = "Basic/path" + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_basename" @@ -72,8 +72,8 @@ class PathCommonPrefix(ComfyNodeABC): } RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("common_prefix",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("common prefix",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_common_prefix" @@ -99,7 +99,7 @@ class PathDirname(ComfyNodeABC): RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("dirname",) - CATEGORY = "Basic/path" + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_dirname" @@ -124,7 +124,7 @@ class PathExists(ComfyNodeABC): RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("exists",) - CATEGORY = "Basic/path" + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_exists" @@ -148,8 +148,8 @@ class PathExpandVars(ComfyNodeABC): } RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("expanded_path",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("expanded path",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "expand_vars" @@ -168,8 +168,8 @@ class PathGetCwd(ComfyNodeABC): return {"required": {}} RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("current_directory",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("current directory",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_cwd" @@ -194,7 +194,7 @@ class PathGetExtension(ComfyNodeABC): RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("extension",) - CATEGORY = "Basic/path" + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_extension" @@ -218,8 +218,8 @@ class PathGetSize(ComfyNodeABC): } RETURN_TYPES = (IO.INT,) - RETURN_NAMES = ("size_bytes",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("size (bytes)",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_size" @@ -254,8 +254,8 @@ class PathGlob(ComfyNodeABC): } RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("matching_paths",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("matching paths",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "glob_paths" OUTPUT_IS_LIST = (True,) @@ -280,8 +280,8 @@ class PathIsAbsolute(ComfyNodeABC): } RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("is_absolute",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("is absolute",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_is_absolute" @@ -305,8 +305,8 @@ class PathIsDir(ComfyNodeABC): } RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("is_dir",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("is dir",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_is_dir" @@ -330,8 +330,8 @@ class PathIsFile(ComfyNodeABC): } RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("is_file",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("is file",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_is_file" @@ -360,7 +360,7 @@ class PathJoin(ComfyNodeABC): RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("path",) - CATEGORY = "Basic/path" + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "join_paths" @@ -392,7 +392,7 @@ class PathListDir(ComfyNodeABC): RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("entries",) - CATEGORY = "Basic/path" + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "list_directory" OUTPUT_IS_LIST = (True,) @@ -433,8 +433,8 @@ class PathNormalize(ComfyNodeABC): } RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("normalized_path",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("normalized path",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "normalize_path" @@ -461,8 +461,8 @@ class PathRelative(ComfyNodeABC): } RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("relative_path",) - CATEGORY = "Basic/path" + RETURN_NAMES = ("relative path",) + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_relative_path" @@ -489,7 +489,7 @@ class PathSplit(ComfyNodeABC): RETURN_TYPES = (IO.STRING, IO.STRING) RETURN_NAMES = ("directory", "filename") - CATEGORY = "Basic/path" + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "split_path" @@ -513,8 +513,8 @@ class PathSplitExt(ComfyNodeABC): } RETURN_TYPES = (IO.STRING, IO.STRING) - RETURN_NAMES = ("path_without_ext", "extension") - CATEGORY = "Basic/path" + RETURN_NAMES = ("path without ext", "extension") + CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "split_ext" diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index 23f10ac..d642d2a 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -2,6 +2,131 @@ from typing import Any from inspect import cleandoc from comfy.comfy_types.node_typing import IO, ComfyNodeABC + +class SetCreate(ComfyNodeABC): + """ + Creates a new SET from items. + + This node creates and returns a SET. The list of items is dynamically + extended based on the number of inputs provided. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.ANY, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_set" + + def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]: + return (set(kwargs.values()),) + + +class SetCreateFromBoolean(ComfyNodeABC): + """ + Creates a new SET from items. + + This node creates and returns a SET. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.BOOLEAN, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_set" + + def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]: + return (set([bool(value) for value in kwargs.values()]),) + + +class SetCreateFromFloat(ComfyNodeABC): + """ + Creates a new SET from items. + + This node creates and returns a SET. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.FLOAT, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_set" + + def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]: + return (set([float(value) for value in kwargs.values()]),) + + +class SetCreateFromInt(ComfyNodeABC): + """ + Creates a new SET from items. + + This node creates and returns a SET. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.INT, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_set" + + def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]: + return (set([int(value) for value in kwargs.values()]),) + + +class SetCreateFromString(ComfyNodeABC): + """ + Creates a new SET from items. + + This node creates and returns a SET. The list of items is dynamically + extended based on the number of inputs provided. + """ + EXPERIMENTAL = True + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "item_0": (IO.STRING, {"_dynamic": "number"}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_set" + + def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]: + return (set([str(value) for value in kwargs.values()]),) + + class SetAdd(ComfyNodeABC): """ Adds an item to a SET. @@ -428,6 +553,11 @@ class SetToList(ComfyNodeABC): NODE_CLASS_MAPPINGS = { + "Basic data handling: SetCreate": SetCreate, + "Basic data handling: SetCreateFromBoolean": SetCreateFromBoolean, + "Basic data handling: SetCreateFromFloat": SetCreateFromFloat, + "Basic data handling: SetCreateFromInt": SetCreateFromInt, + "Basic data handling: SetCreateFromString": SetCreateFromString, "Basic data handling: SetAdd": SetAdd, "Basic data handling: SetContains": SetContains, "Basic data handling: SetDifference": SetDifference, @@ -446,6 +576,11 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { + "Basic data handling: SetCreate": "create SET", + "Basic data handling: SetCreateFromBoolean": "create SET from BOOLEANs", + "Basic data handling: SetCreateFromFloat": "create SET from FLOATs", + "Basic data handling: SetCreateFromInt": "create SET from INTs", + "Basic data handling: SetCreateFromString": "create SET from STRINGs", "Basic data handling: SetAdd": "add", "Basic data handling: SetContains": "contains", "Basic data handling: SetDifference": "difference", @@ -459,6 +594,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: SetRemove": "remove", "Basic data handling: SetSymmetricDifference": "symmetric difference", "Basic data handling: SetUnion": "union", - "Basic data handling: SetToDataList": "convert to data list", + "Basic data handling: SetToDataList": "convert to Data List", "Basic data handling: SetToList": "convert to LIST", } diff --git a/tests/test_comparison_nodes.py b/tests/test_comparison_nodes.py index 9d44431..ff25f4d 100644 --- a/tests/test_comparison_nodes.py +++ b/tests/test_comparison_nodes.py @@ -1,4 +1,4 @@ -#import pytest +import pytest from src.basic_data_handling.comparison_nodes import ( Equal, NotEqual, @@ -20,6 +20,9 @@ def test_equal(): assert node.compare("hello", "hello") == (True,) assert node.compare([1, 2], [1, 2]) == (True,) assert node.compare(None, None) == (True,) + # Test complex objects + assert node.compare({"a": 1}, {"a": 1}) == (True,) + assert node.compare({"a": 1}, {"a": 2}) == (False,) def test_not_equal(): @@ -29,6 +32,9 @@ def test_not_equal(): assert node.compare("hello", "world") == (True,) assert node.compare([1, 2], [1, 3]) == (True,) assert node.compare(None, []) == (True,) + # Test complex objects + assert node.compare({"a": 1}, {"b": 1}) == (True,) + assert node.compare({"a": 1}, {"a": 1}) == (False,) def test_greater_than(): @@ -37,6 +43,12 @@ def test_greater_than(): assert node.compare(5, 10) == (False,) assert node.compare(10, 10) == (False,) assert node.compare(5.6, 2.3) == (True,) + # Test negative numbers + assert node.compare(-5, -10) == (True,) + assert node.compare(-10, -5) == (False,) + # Test zeroes + assert node.compare(0, -1) == (True,) + assert node.compare(0, 0) == (False,) def test_less_than(): @@ -45,6 +57,12 @@ def test_less_than(): assert node.compare(10, 5) == (False,) assert node.compare(10, 10) == (False,) assert node.compare(2.3, 5.6) == (True,) + # Test negative numbers + assert node.compare(-10, -5) == (True,) + assert node.compare(-5, -10) == (False,) + # Test zeroes + assert node.compare(-1, 0) == (True,) + assert node.compare(0, 0) == (False,) def test_greater_than_or_equal(): @@ -53,6 +71,12 @@ def test_greater_than_or_equal(): assert node.compare(15, 10) == (True,) assert node.compare(5, 10) == (False,) assert node.compare(5.5, 5.5) == (True,) + # Test negative numbers + assert node.compare(-5, -10) == (True,) + assert node.compare(-5, -5) == (True,) + # Test zeroes + assert node.compare(0, 0) == (True,) + assert node.compare(0.0, -0.0) == (True,) def test_less_than_or_equal(): @@ -61,6 +85,12 @@ def test_less_than_or_equal(): assert node.compare(5, 15) == (True,) assert node.compare(15, 5) == (False,) assert node.compare(3.1, 3.1) == (True,) + # Test negative numbers + assert node.compare(-10, -5) == (True,) + assert node.compare(-5, -5) == (True,) + # Test zeroes + assert node.compare(0, 0) == (True,) + assert node.compare(-0.0, 0.0) == (True,) def test_string_comparison(): @@ -71,11 +101,20 @@ def test_string_comparison(): assert node.compare("apple", "banana", "<", "True") == (True,) assert node.compare("apple", "banana", ">", "True") == (False,) assert node.compare("apple", "apple", "!=", "True") == (False,) + assert node.compare("banana", "apple", ">=", "True") == (True,) + assert node.compare("apple", "banana", "<=", "True") == (True,) # Case-insensitive tests assert node.compare("Hello", "hello", "==", "False") == (True,) assert node.compare("Apple", "BANANA", "<", "False") == (True,) assert node.compare("Grape", "GRAPE", ">=", "False") == (True,) + assert node.compare("ZEBRA", "zoo", "<", "False") == (True,) + assert node.compare("ZOO", "zebra", ">", "False") == (True,) + + # Empty string tests + assert node.compare("", "hello", "<", "True") == (True,) + assert node.compare("hello", "", ">", "True") == (True,) + assert node.compare("", "", "==", "True") == (True,) def test_number_in_range(): @@ -84,15 +123,27 @@ def test_number_in_range(): # Inclusive range tests assert node.check_range(50, 0, 100) == (True,) assert node.check_range(0, 0, 100) == (True,) + assert node.check_range(100, 0, 100) == (True,) # Exclusive range tests assert node.check_range(0, 0, 100, include_min="False") == (False,) assert node.check_range(100, 0, 100, include_max="False") == (False,) + assert node.check_range(50, 0, 100, include_min="False", include_max="False") == (True,) # Out of range tests assert node.check_range(-1, 0, 100) == (False,) assert node.check_range(101, 0, 100) == (False,) + # Edge cases + assert node.check_range(0, 0, 0) == (True,) + assert node.check_range(0, 0, 0, include_min="False") == (False,) + assert node.check_range(0, 0, 0, include_max="False") == (False,) + + # Negative range tests + assert node.check_range(-50, -100, -10) == (True,) + assert node.check_range(-10, -100, -10) == (True,) + assert node.check_range(-100, -100, -10) == (True,) + def test_is_null(): node = IsNull() @@ -100,16 +151,37 @@ def test_is_null(): assert node.check_null(0) == (False,) assert node.check_null("") == (False,) assert node.check_null([]) == (False,) + assert node.check_null(False) == (False,) + # Additional edge cases + assert node.check_null([None]) == (False,) # List containing None + assert node.check_null("None") == (False,) # String "None" def test_compare_length(): node = CompareLength() - # Valid comparisons + # Valid comparisons with different operators assert node.compare_length([1, 2, 3], "==", 3) == (True, 3) assert node.compare_length("hello", "!=", 4) == (True, 5) assert node.compare_length((1, 2), ">", 1) == (True, 2) + assert node.compare_length([1, 2, 3], "<", 4) == (True, 3) + assert node.compare_length("test", ">=", 4) == (True, 4) + assert node.compare_length([1], "<=", 1) == (True, 1) + + # Different container types + assert node.compare_length({"a", "b", "c"}, "==", 3) == (True, 3) # Set + assert node.compare_length({"key1": 1, "key2": 2}, "==", 2) == (True, 2) # Dict + + # Empty containers + assert node.compare_length([], "==", 0) == (True, 0) + assert node.compare_length("", "==", 0) == (True, 0) + assert node.compare_length({}, "==", 0) == (True, 0) # Invalid comparisons - assert node.compare_length({"key": "value"}, "<", 1) == (False, 1) - assert node.compare_length(123, "==", 0) == (False, -1) # Non-container input edge case + assert node.compare_length({"key": "value"}, "<", 1) == (False, 1) # Dict with 1 item + assert node.compare_length(123, "==", 0) == (False, -1) # Non-container input + assert node.compare_length(None, "==", 0) == (False, -1) # None + + # Error case with invalid operator + with pytest.raises(ValueError): + node.compare_length([1, 2, 3], "invalid", 3) diff --git a/tests/test_control_flow_nodes.py b/tests/test_control_flow_nodes.py index f3de291..de6072f 100644 --- a/tests/test_control_flow_nodes.py +++ b/tests/test_control_flow_nodes.py @@ -21,19 +21,19 @@ def test_switch_case(): node = SwitchCase() # Selector within range, valid cases - assert node.execute(0, "Case 0", "Case 1", "Case 2", "Case 3") == ("Case 0",) - assert node.execute(1, "Case 0", "Case 1", "Case 2", "Case 3") == ("Case 1",) - assert node.execute(3, "Case 0", "Case 1", "Case 2", "Case 3") == ("Case 3",) + assert node.execute(0, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3") == ("Case 0",) + assert node.execute(1, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3") == ("Case 1",) + assert node.execute(3, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3") == ("Case 3",) # Selector out of range, fallback to default case - assert node.execute(4, "Case 0", "Case 1", "Case 2", "Case 3", default="Default") == ("Default",) - assert node.execute(99, "Case 0", "Case 1", "Case 2", "Case 3", default="Default") == ("Default",) + assert node.execute(4, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3", default="Default") == ("Default",) + assert node.execute(99, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3", default="Default") == ("Default",) # Selector out of range, no default provided - assert node.execute(4, "Case 0", "Case 1", "Case 2", "Case 3") == (None,) + assert node.execute(4, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3") == (None,) # Edge cases - assert node.execute(0, None, "Case 1", "Case 2") == (None,) # Case with None - assert node.execute(2, "Case 0", "Case 1", "Case 2") == ("Case 2",) - assert node.execute(0, 123, False, "Case 2") == (123,) # Different data types - assert node.execute(10, "Case 0", "Case 1", "Case 2", default="Fallback") == ("Fallback",) + assert node.execute(0, case_0=None, case_1="Case 1", case_2="Case 2") == (None,) # Case with None + assert node.execute(2, case_0="Case 0", case_1="Case 1", case_2="Case 2") == ("Case 2",) + assert node.execute(0, case_0=123, case_1=False, case_2="Case 2") == (123,) # Different data types + assert node.execute(10, case_0="Case 0", case_1="Case 1", case_2="Case 2", default="Fallback") == ("Fallback",) diff --git a/tests/test_data_list_nodes.py b/tests/test_data_list_nodes.py index e6e5086..ab98877 100644 --- a/tests/test_data_list_nodes.py +++ b/tests/test_data_list_nodes.py @@ -14,7 +14,6 @@ from src.basic_data_handling.data_list_nodes import ( DataListGetItem, DataListSetItem, DataListContains, - DataListCreateEmpty, DataListZip, DataListFilter, DataListMin, @@ -119,21 +118,18 @@ def test_contains(): assert node.contains(list=[], value=["x"]) == (False,) -def test_create_empty(): - node = DataListCreateEmpty() - assert node.create_empty() == ([],) - - def test_zip(): node = DataListZip() assert node.zip_lists(list1=[1, 2], list2=["a", "b"]) == ([[1, "a"], [2, "b"]],) assert node.zip_lists(list1=[1], list2=["a", "b"]) == ([[1, "a"]],) # Shortest list length + assert node.zip_lists(list1=[1, 2, 3], list2=["a", "b"]) == ([[1, "a"], [2, "b"]],) # Different lengths def test_filter(): node = DataListFilter() assert node.filter_data(value=[1, 2, 3], filter=[False, True, False]) == ([1, 3],) assert node.filter_data(value=[1, 2], filter=[True, True]) == ([],) + assert node.filter_data(value=[1, 2, 3], filter=[False, False, False]) == ([1, 2, 3],) def test_min(): diff --git a/tests/test_dict_nodes.py b/tests/test_dict_nodes.py index 03a2738..39fd979 100644 --- a/tests/test_dict_nodes.py +++ b/tests/test_dict_nodes.py @@ -34,30 +34,46 @@ def test_dict_get(): my_dict = {"key1": "value1", "key2": "value2"} assert node.get(my_dict, "key1") == ("value1",) assert node.get(my_dict, "key3", default="default_value") == ("default_value",) + # Test with no default specified + assert node.get(my_dict, "non_existent") == (None,) def test_dict_set(): node = DictSet() my_dict = {"key1": "value1"} assert node.set(my_dict, "key2", "value2") == ({"key1": "value1", "key2": "value2"},) + # Test overwriting existing key + assert node.set(my_dict, "key1", "new_value") == ({"key1": "new_value"},) + # Test with empty dict + assert node.set({}, "key", "value") == ({"key": "value"},) def test_dict_keys(): node = DictKeys() my_dict = {"key1": "value1", "key2": "value2"} assert node.keys(my_dict) == (["key1", "key2"],) + # Test with empty dict + assert node.keys({}) == ([],) def test_dict_values(): node = DictValues() my_dict = {"key1": "value1", "key2": "value2"} assert node.values(my_dict) == (["value1", "value2"],) + # Test with empty dict + assert node.values({}) == ([],) def test_dict_items(): node = DictItems() my_dict = {"key1": "value1", "key2": "value2"} - assert node.items(my_dict) == ([("key1", "value1"), ("key2", "value2")],) + # Note that the order might not be preserved, so we check if items are in the result + items = node.items(my_dict)[0] + assert len(items) == 2 + assert ("key1", "value1") in items + assert ("key2", "value2") in items + # Test with empty dict + assert node.items({}) == ([],) def test_dict_contains_key(): @@ -65,31 +81,52 @@ def test_dict_contains_key(): my_dict = {"key1": "value1"} assert node.contains_key(my_dict, "key1") == (True,) assert node.contains_key(my_dict, "key2") == (False,) + # Test with empty dict + assert node.contains_key({}, "any_key") == (False,) def test_dict_from_keys(): node = DictFromKeys() keys = ["key1", "key2"] assert node.from_keys(keys, value="value") == ({"key1": "value", "key2": "value"},) + # Test without value (should use None) + assert node.from_keys(keys) == ({"key1": None, "key2": None},) + # Test with empty keys list + assert node.from_keys([]) == ({},) def test_dict_pop(): node = DictPop() my_dict = {"key1": "value1", "key2": "value2"} assert node.pop(my_dict, "key1") == ({"key2": "value2"}, "value1") + # Test with default value for non-existent key + assert node.pop(my_dict, "non_existent", default_value="default") == (my_dict, "default") + # Test with no default for non-existent key - should not modify dict + assert node.pop({"a": 1}, "b") == ({"a": 1}, None) def test_dict_pop_item(): node = DictPopItem() my_dict = {"key1": "value1"} - assert node.popitem(my_dict) == ({}, "key1", "value1", True) + result = node.popitem(my_dict) + # Since we only have one item, we know what should be popped + assert result[0] == {} # remaining dict is empty + assert result[1] == "key1" # popped key + assert result[2] == "value1" # popped value + assert result[3] is True # success + # Test with empty dict assert node.popitem({}) == ({}, "", None, False) def test_dict_set_default(): node = DictSetDefault() my_dict = {"key1": "value1"} + # Test setting a key that doesn't exist assert node.setdefault(my_dict, "key2", "default") == ({"key1": "value1", "key2": "default"}, "default") + # Test with a key that already exists + assert node.setdefault(my_dict, "key1", "new_default") == ({"key1": "value1"}, "value1") + # Test with empty dict + assert node.setdefault({}, "key", "value") == ({"key": "value"}, "value") def test_dict_update(): @@ -97,69 +134,153 @@ def test_dict_update(): my_dict = {"key1": "value1"} update_dict = {"key2": "value2"} assert node.update(my_dict, update_dict) == ({"key1": "value1", "key2": "value2"},) + # Test updating with overlapping keys + assert node.update({"a": 1, "b": 2}, {"b": 3, "c": 4}) == ({"a": 1, "b": 3, "c": 4},) + # Test with empty update dict + assert node.update(my_dict, {}) == (my_dict,) + # Test with empty original dict + assert node.update({}, update_dict) == (update_dict,) def test_dict_length(): node = DictLength() - my_dict = {"key1": "value1"} - assert node.length(my_dict) == (1,) + my_dict = {"key1": "value1", "key2": "value2"} + assert node.length(my_dict) == (2,) + # Test with empty dict + assert node.length({}) == (0,) def test_dict_merge(): node = DictMerge() dict1 = {"key1": "value1"} dict2 = {"key2": "value2"} + # Test basic merge assert node.merge(dict1, dict2) == ({"key1": "value1", "key2": "value2"},) + # Test with overlapping keys (later dicts override earlier ones) + assert node.merge({"a": 1}, {"a": 2}) == ({"a": 2},) + # Test with more than two dicts + result = node.merge({"a": 1}, {"b": 2}, {"c": 3}) + assert result == ({"a": 1, "b": 2, "c": 3},) + # Test with empty dicts + assert node.merge(dict1, {}) == (dict1,) + assert node.merge({}, dict1) == (dict1,) def test_dict_get_keys_values(): node = DictGetKeysValues() my_dict = {"key1": "value1", "key2": "value2"} - assert node.get_keys_values(my_dict) == (["key1", "key2"], ["value1", "value2"]) + keys, values = node.get_keys_values(my_dict) + # Check keys and values contents (order may vary) + assert set(keys) == {"key1", "key2"} + assert set(values) == {"value1", "value2"} + # Test with empty dict + assert node.get_keys_values({}) == ([], []) def test_dict_remove(): node = DictRemove() - my_dict = {"key1": "value1"} - assert node.remove(my_dict, "key1") == ({}, True) + my_dict = {"key1": "value1", "key2": "value2"} + # Test successful removal + assert node.remove(my_dict, "key1") == ({"key2": "value2"}, True) + # Test removal of non-existent key + assert node.remove(my_dict, "non_existent") == (my_dict, False) + # Test with empty dict + assert node.remove({}, "any_key") == ({}, False) def test_dict_filter_by_keys(): node = DictFilterByKeys() - my_dict = {"key1": "value1", "key2": "value2"} - keys = ["key1"] - assert node.filter_by_keys(my_dict, keys) == ({"key1": "value1"},) + my_dict = {"key1": "value1", "key2": "value2", "key3": "value3"} + # Test with subset of keys + assert node.filter_by_keys(my_dict, ["key1", "key3"]) == ({"key1": "value1", "key3": "value3"},) + # Test with non-existent keys + assert node.filter_by_keys(my_dict, ["key1", "non_existent"]) == ({"key1": "value1"},) + # Test with empty keys list + assert node.filter_by_keys(my_dict, []) == ({},) + # Test with empty dict + assert node.filter_by_keys({}, ["any_key"]) == ({},) def test_dict_exclude_keys(): node = DictExcludeKeys() - my_dict = {"key1": "value1", "key2": "value2"} - keys = ["key1"] - assert node.exclude_keys(my_dict, keys) == ({"key2": "value2"},) + my_dict = {"key1": "value1", "key2": "value2", "key3": "value3"} + # Test excluding some keys + assert node.exclude_keys(my_dict, ["key1", "key3"]) == ({"key2": "value2"},) + # Test excluding non-existent keys + assert node.exclude_keys(my_dict, ["non_existent"]) == (my_dict,) + # Test excluding all keys + assert node.exclude_keys(my_dict, ["key1", "key2", "key3"]) == ({},) + # Test with empty exclude list + assert node.exclude_keys(my_dict, []) == (my_dict,) + # Test with empty dict + assert node.exclude_keys({}, ["any_key"]) == ({},) def test_dict_get_multiple(): node = DictGetMultiple() my_dict = {"key1": "value1", "key2": "value2"} - keys = ["key1", "key3"] - assert node.get_multiple(my_dict, keys, default="default") == (["value1", "default"],) + # Test getting existing keys + assert node.get_multiple(my_dict, ["key1", "key2"]) == (["value1", "value2"],) + # Test with mix of existing and non-existent keys + assert node.get_multiple(my_dict, ["key1", "key3"], default="default") == (["value1", "default"],) + # Test with only non-existent keys + assert node.get_multiple(my_dict, ["key3", "key4"], default="default") == (["default", "default"],) + # Test with empty keys list + assert node.get_multiple(my_dict, []) == ([],) + # Test with empty dict + assert node.get_multiple({}, ["key1"], default="default") == (["default"],) def test_dict_invert(): node = DictInvert() my_dict = {"key1": "value1", "key2": "value2"} + # Test basic inversion assert node.invert(my_dict) == ({"value1": "key1", "value2": "key2"}, True) + # Test with duplicated values + result, success = node.invert({"key1": "value", "key2": "value"}) + # With duplicate values, the last key wins + assert result == {"value": "key2"} + assert success is True + # Test with empty dict + assert node.invert({}) == ({}, True) def test_dict_create_from_lists(): node = DictCreateFromLists() - keys = ["key1", "key2"] - values = ["value1", "value2"] - assert node.create_from_lists(keys, values) == ({"key1": "value1", "key2": "value2"},) + keys = ["key1", "key2", "key3"] + values = ["value1", "value2", "value3"] + # Test with matching length lists + assert node.create_from_lists(keys, values) == ({"key1": "value1", "key2": "value2", "key3": "value3"},) + # Test with more keys than values + assert node.create_from_lists(keys, ["value1", "value2"]) == ({"key1": "value1", "key2": "value2"},) + # Test with more values than keys + assert node.create_from_lists(["key1", "key2"], values) == ({"key1": "value1", "key2": "value2"},) + # Test with empty lists + assert node.create_from_lists([], []) == ({},) def test_dict_compare(): node = DictCompare() - dict1 = {"key1": "value1"} - dict2 = {"key1": "value1"} + # Test identical dictionaries + dict1 = {"key1": "value1", "key2": "value2"} + dict2 = {"key1": "value1", "key2": "value2"} assert node.compare(dict1, dict2) == (True, [], [], []) + + # Test dictionaries with different values + dict3 = {"key1": "value1", "key2": "different"} + are_equal, only_in_1, only_in_2, diff_values = node.compare(dict1, dict3) + assert are_equal is False + assert only_in_1 == [] + assert only_in_2 == [] + assert "key2" in diff_values + + # Test dictionaries with different keys + dict4 = {"key1": "value1", "key3": "value3"} + are_equal, only_in_1, only_in_2, diff_values = node.compare(dict1, dict4) + assert are_equal is False + assert "key2" in only_in_1 + assert "key3" in only_in_2 + assert diff_values == [] + + # Test empty dictionaries + assert node.compare({}, {}) == (True, [], [], []) diff --git a/tests/test_float_nodes.py b/tests/test_float_nodes.py index 1f548cb..19bb9d9 100644 --- a/tests/test_float_nodes.py +++ b/tests/test_float_nodes.py @@ -1,9 +1,12 @@ +import math import pytest from src.basic_data_handling.float_nodes import ( + FloatCreate, FloatAdd, FloatSubtract, FloatMultiply, FloatDivide, + FloatDivideSafe, FloatPower, FloatRound, FloatIsInteger, @@ -12,6 +15,15 @@ from src.basic_data_handling.float_nodes import ( FloatFromHex, ) +def test_float_create(): + node = FloatCreate() + assert node.create("3.14") == (3.14,) + assert node.create("-2.5") == (-2.5,) + assert node.create("0.0") == (0.0,) + with pytest.raises(ValueError): + node.create("not a number") + + def test_float_add(): node = FloatAdd() assert node.add(2.2, 2.2) == (4.4,) @@ -41,6 +53,20 @@ def test_float_divide(): node.divide(5.0, 0.0) +def test_float_divide_safe(): + node = FloatDivideSafe() + + assert node.divide(7.0, 2.0) == (3.5,) + assert node.divide(-6.0, 3.0) == (-2.0,) + assert node.divide(5.0, 0.0) == (float('inf'),) + assert node.divide(-5.0, 0.0) == (float('-inf'),) + + # Special handling for NaN because NaN != NaN + result = node.divide(0.0, 0.0) + assert len(result) == 1 + assert math.isnan(result[0]) + + def test_float_power(): node = FloatPower() assert node.power(2.0, 3.0) == (8.0,) diff --git a/tests/test_int_nodes.py b/tests/test_int_nodes.py index 1b34b02..1c1e753 100644 --- a/tests/test_int_nodes.py +++ b/tests/test_int_nodes.py @@ -4,12 +4,15 @@ from src.basic_data_handling.int_nodes import ( IntSubtract, IntMultiply, IntDivide, + IntDivideSafe, IntModulus, IntPower, IntBitLength, IntToBytes, IntFromBytes, IntBitCount, + IntCreate, + IntCreateWithBase, ) @@ -83,4 +86,32 @@ def test_int_bit_count(): node = IntBitCount() assert node.bit_count(0) == (0,) # Zero has no 1 bits assert node.bit_count(255) == (8,) # 255 is 11111111 (8 ones) - assert node.bit_count(-255) == (8,) # Negative integer has the same bit count \ No newline at end of file + assert node.bit_count(-255) == (8,) # Negative integer has the same bit count + + +def test_int_create(): + node = IntCreate() + assert node.create("42") == (42,) + assert node.create("-100") == (-100,) + assert node.create("0") == (0,) + assert node.create("0xFF") == (255,) # Hexadecimal + assert node.create("0b1010") == (10,) # Binary + assert node.create("0o777") == (511,) # Octal + + +def test_int_create_with_base(): + node = IntCreateWithBase() + assert node.create("42", 10) == (42,) + assert node.create("FF", 16) == (255,) + assert node.create("1010", 2) == (10,) + assert node.create("777", 8) == (511,) + with pytest.raises(ValueError): + node.create("FF", 10) # Invalid characters for base 10 + + +def test_int_divide_safe(): + node = IntDivideSafe() + assert node.divide(10, 2, 9223372036854775807) == (5,) + assert node.divide(9, 4, 9223372036854775807) == (2,) # Integer division + assert node.divide(10, 0, 9223372036854775807) == (9223372036854775807,) # Positive infinity + assert node.divide(-10, 0, 9223372036854775807) == (-9223372036854775807,) # Negative infinity diff --git a/tests/test_list_nodes.py b/tests/test_list_nodes.py index 4e6ecd0..fa36b2f 100644 --- a/tests/test_list_nodes.py +++ b/tests/test_list_nodes.py @@ -1,5 +1,10 @@ import pytest from src.basic_data_handling.list_nodes import ( + ListCreate, + ListCreateFromBoolean, + ListCreateFromFloat, + ListCreateFromInt, + ListCreateFromString, ListAppend, ListExtend, ListInsert, @@ -15,7 +20,9 @@ from src.basic_data_handling.list_nodes import ( ListSetItem, ListContains, ListMin, - ListMax + ListMax, + ListToDataList, + ListToSet ) @@ -130,3 +137,52 @@ def test_list_max(): assert node.find_max([1, 2, 3]) == (3,) assert node.find_max(["b", "a", "c"]) == ("c",) assert node.find_max([]) == (None,) + + +def test_list_create(): + node = ListCreate() + assert node.create_list(item_0=1, item_1=2, item_2=3) == ([1, 2, 3],) + assert node.create_list() == ([],) # Empty list + assert node.create_list(item_0="test") == (["test"],) + + +def test_list_create_from_boolean(): + node = ListCreateFromBoolean() + assert node.create_list(item_0=True, item_1=False, item_2=True) == ([True, False, True],) + assert node.create_list(item_0=True) == ([True],) + assert node.create_list() == ([],) + + +def test_list_create_from_float(): + node = ListCreateFromFloat() + assert node.create_list(item_0=1.1, item_1=2.2, item_2=3.3) == ([1.1, 2.2, 3.3],) + assert node.create_list(item_0=0.0) == ([0.0],) + assert node.create_list() == ([],) + + +def test_list_create_from_int(): + node = ListCreateFromInt() + assert node.create_list(item_0=1, item_1=2, item_2=3) == ([1, 2, 3],) + assert node.create_list(item_0=0) == ([0],) + assert node.create_list() == ([],) + + +def test_list_create_from_string(): + node = ListCreateFromString() + assert node.create_list(item_0="a", item_1="b", item_2="c") == (["a", "b", "c"],) + assert node.create_list(item_0="test") == (["test"],) + assert node.create_list() == ([],) + + +def test_list_to_data_list(): + node = ListToDataList() + input_list = [1, 2, 3] + # The output should be the same list but marked as a data list by the OUTPUT_IS_LIST = (True,) flag + assert node.convert(input_list) == (input_list,) + + +def test_list_to_set(): + node = ListToSet() + assert node.convert([1, 2, 3, 2, 1]) == ({1, 2, 3},) + assert node.convert([]) == (set(),) + assert node.convert(["a", "b", "a", "c"]) == ({"a", "b", "c"},) diff --git a/tests/test_math_nodes.py b/tests/test_math_nodes.py index 5918e9f..6831baa 100644 --- a/tests/test_math_nodes.py +++ b/tests/test_math_nodes.py @@ -12,6 +12,10 @@ def test_math_sin(): assert node.calculate(0, "radians") == (0.0,) assert node.calculate(math.pi / 2, "radians") == (1.0,) assert node.calculate(30, "degrees") == (math.sin(math.radians(30)),) + # Test with string input + assert node.calculate("45", "degrees")[0] == pytest.approx(math.sin(math.radians(45))) + # Test negative angle + assert node.calculate(-90, "degrees")[0] == pytest.approx(-1.0) def test_math_cos(): @@ -19,14 +23,25 @@ def test_math_cos(): assert node.calculate(0, "radians") == (1.0,) assert node.calculate(math.pi, "radians") == (-1.0,) assert node.calculate(60, "degrees") == (math.cos(math.radians(60)),) + # Test with string input + assert node.calculate("60", "degrees")[0] == pytest.approx(0.5) + # Test negative angle + assert node.calculate(-180, "degrees")[0] == pytest.approx(-1.0) def test_math_tan(): node = MathTan() assert node.calculate(0, "radians") == (0.0,) assert node.calculate(45, "degrees") == (math.tan(math.radians(45)),) + # Test with string input + assert node.calculate("30", "degrees")[0] == pytest.approx(math.tan(math.radians(30))) + # Test negative angle + assert node.calculate(-45, "degrees")[0] == pytest.approx(-1.0) + # Test undefined tangent with pytest.raises(ValueError): # Undefined tangent node.calculate(90, "degrees") + with pytest.raises(ValueError): # Undefined tangent + node.calculate(math.pi/2, "radians") def test_math_asin(): @@ -34,6 +49,13 @@ def test_math_asin(): assert node.calculate(0, "radians") == (0.0,) assert node.calculate(1, "radians") == (math.pi / 2,) assert node.calculate(0.5, "degrees") == (math.degrees(math.asin(0.5)),) + # Test with string input + assert node.calculate("0.5", "degrees")[0] == pytest.approx(30.0) + # Test boundary values + assert node.calculate(-1, "degrees")[0] == pytest.approx(-90.0) + # Test out of range values + with pytest.raises(ValueError): + node.calculate(1.1, "degrees") def test_math_acos(): @@ -41,6 +63,13 @@ def test_math_acos(): assert node.calculate(1, "radians") == (0.0,) assert node.calculate(0, "radians") == (math.pi / 2,) assert node.calculate(0.5, "degrees") == (math.degrees(math.acos(0.5)),) + # Test with string input + assert node.calculate("0.5", "degrees")[0] == pytest.approx(60.0) + # Test boundary values + assert node.calculate(-1, "degrees")[0] == pytest.approx(180.0) + # Test out of range values + with pytest.raises(ValueError): + node.calculate(1.1, "degrees") def test_math_atan(): @@ -48,6 +77,10 @@ def test_math_atan(): assert node.calculate(0, "radians") == (0.0,) assert node.calculate(1, "degrees") == (45.0,) assert node.calculate(-1, "radians") == (-math.pi / 4,) + # Test with string input + assert node.calculate("1.0", "degrees")[0] == pytest.approx(45.0) + # Test large values + assert node.calculate(1000, "degrees")[0] == pytest.approx(89.94, abs=0.01) def test_math_atan2(): @@ -55,13 +88,24 @@ def test_math_atan2(): assert node.calculate(0, 1, "radians") == (0.0,) assert node.calculate(1, 1, "degrees") == (45.0,) assert node.calculate(-1, -1, "radians") == (-3 * math.pi / 4,) + # Test with string input + assert node.calculate("1", "1", "degrees")[0] == pytest.approx(45.0) + # Test special cases + assert node.calculate(1, 0, "degrees")[0] == pytest.approx(90.0) + assert node.calculate(-1, 0, "degrees")[0] == pytest.approx(-90.0) + assert node.calculate(0, -1, "degrees")[0] == pytest.approx(180.0) def test_math_sqrt(): node = MathSqrt() assert node.calculate(4) == (2.0,) assert node.calculate(0) == (0.0,) - with pytest.raises(ValueError): # Negative numbers + # Test with string input + assert node.calculate("9")[0] == pytest.approx(3.0) + # Test with float input + assert node.calculate(2.25)[0] == pytest.approx(1.5) + # Test negative numbers (should raise ValueError) + with pytest.raises(ValueError): node.calculate(-1) @@ -70,6 +114,10 @@ def test_math_exp(): assert node.calculate(0) == (1.0,) assert node.calculate(1) == (math.e,) assert node.calculate(-1) == (1 / math.e,) + # Test with string input + assert node.calculate("2")[0] == pytest.approx(math.exp(2)) + # Test with large input + assert node.calculate(10)[0] == pytest.approx(math.exp(10)) def test_math_log(): @@ -77,16 +125,30 @@ def test_math_log(): assert node.calculate(1) == (0.0,) assert node.calculate(math.e) == (1.0,) assert node.calculate(8, base=2) == (3.0,) - with pytest.raises(ValueError): # Log of non-positive + # Test with string input + assert node.calculate("10", base="10")[0] == pytest.approx(1.0) + # Test with default base (e) + assert node.calculate(math.exp(5))[0] == pytest.approx(5.0) + # Test with log of non-positive (should raise ValueError) + with pytest.raises(ValueError): node.calculate(-1) + with pytest.raises(ValueError): + node.calculate(0) def test_math_log10(): node = MathLog10() assert node.calculate(1) == (0.0,) assert node.calculate(10) == (1.0,) - with pytest.raises(ValueError): # Log of non-positive + # Test with string input + assert node.calculate("100")[0] == pytest.approx(2.0) + # Test with non-integer values + assert node.calculate(2)[0] == pytest.approx(math.log10(2)) + # Test with log of non-positive (should raise ValueError) + with pytest.raises(ValueError): node.calculate(0) + with pytest.raises(ValueError): + node.calculate(-10) def test_math_degrees(): @@ -94,6 +156,10 @@ def test_math_degrees(): assert node.calculate(math.pi) == (180.0,) assert node.calculate(math.pi / 2) == (90.0,) assert node.calculate(0) == (0.0,) + # Test with string input + assert node.calculate("3.14159")[0] == pytest.approx(180.0, abs=0.01) + # Test with negative values + assert node.calculate(-math.pi)[0] == pytest.approx(-180.0) def test_math_radians(): @@ -101,6 +167,12 @@ def test_math_radians(): assert node.calculate(180) == (math.pi,) assert node.calculate(90) == (math.pi / 2,) assert node.calculate(0) == (0.0,) + # Test with string input + assert node.calculate("180")[0] == pytest.approx(math.pi) + # Test with negative values + assert node.calculate(-180)[0] == pytest.approx(-math.pi) + # Test with decimal values + assert node.calculate(45.5)[0] == pytest.approx(math.radians(45.5)) def test_math_floor(): @@ -108,6 +180,12 @@ def test_math_floor(): assert node.calculate(1.7) == (1,) assert node.calculate(-1.7) == (-2,) assert node.calculate(0.0) == (0,) + # Test with string input + assert node.calculate("3.9")[0] == 3 + # Test with integer input + assert node.calculate(5)[0] == 5 + # Test with very small decimals + assert node.calculate(0.0001)[0] == 0 def test_math_ceil(): @@ -115,6 +193,12 @@ def test_math_ceil(): assert node.calculate(1.2) == (2,) assert node.calculate(-1.2) == (-1,) assert node.calculate(0.0) == (0,) + # Test with string input + assert node.calculate("3.1")[0] == 4 + # Test with integer input + assert node.calculate(5)[0] == 5 + # Test with very small decimals + assert node.calculate(0.0001)[0] == 1 def test_math_abs(): @@ -122,16 +206,26 @@ def test_math_abs(): assert node.calculate(-5) == (5,) assert node.calculate(5) == (5,) assert node.calculate(0) == (0,) + # Test with string input + assert node.calculate("-3.14")[0] == pytest.approx(3.14) + # Test with float input + assert node.calculate(-2.5)[0] == 2.5 + # Test with zero + assert node.calculate(0)[0] == 0 def test_math_pi(): node = MathPi() assert node.calculate() == (math.pi,) + # Verify precision to at least 10 decimal places + assert abs(node.calculate()[0] - 3.1415926535) < 1e-10 def test_math_e(): node = MathE() assert node.calculate() == (math.e,) + # Verify precision to at least 10 decimal places + assert abs(node.calculate()[0] - 2.7182818284) < 1e-10 def test_math_min(): @@ -139,6 +233,12 @@ def test_math_min(): assert node.calculate(5, 10) == (5,) assert node.calculate(-1, 0) == (-1,) assert node.calculate(3.5, 2.5) == (2.5,) + # Test with string inputs + assert node.calculate("7", "3")[0] == 3.0 + # Test with mixed types + assert node.calculate(5, "2.5")[0] == 2.5 + # Test with equal values + assert node.calculate(10, 10)[0] == 10 def test_math_max(): @@ -146,3 +246,9 @@ def test_math_max(): assert node.calculate(5, 10) == (10,) assert node.calculate(-1, 0) == (0,) assert node.calculate(3.5, 2.5) == (3.5,) + # Test with string inputs + assert node.calculate("7", "3")[0] == 7.0 + # Test with mixed types + assert node.calculate(5, "7.5")[0] == 7.5 + # Test with equal values + assert node.calculate(10, 10)[0] == 10 diff --git a/tests/test_path_nodes.py b/tests/test_path_nodes.py index 62fc962..6f68ce9 100644 --- a/tests/test_path_nodes.py +++ b/tests/test_path_nodes.py @@ -1,5 +1,6 @@ import os import pytest +import platform from src.basic_data_handling.path_nodes import ( PathJoin, PathAbspath, PathExists, PathIsFile, PathIsDir, PathGetSize, @@ -14,21 +15,38 @@ def test_path_join(): assert node.join_paths("folder", "file.txt") == (os.path.join("folder", "file.txt"),) assert node.join_paths("folder", "") == (os.path.join("folder"),) assert node.join_paths(".", "") == (".",) + # Test join with empty first path + assert node.join_paths("", "file.txt") == ("file.txt",) + # Test with multiple path components + nested_path = os.path.join("folder", "subfolder", "file.txt") + assert node.join_paths("folder", os.path.join("subfolder", "file.txt")) == (nested_path,) def test_path_abspath(): node = PathAbspath() assert node.get_abspath(".") == (os.path.abspath("."),) assert node.get_abspath("folder/file.txt") == (os.path.abspath("folder/file.txt"),) + # Test with empty string (should return current directory) + assert node.get_abspath("") == (os.path.abspath(os.getcwd()),) + # Test with relative path with special characters + assert node.get_abspath("./file.txt") == (os.path.abspath("./file.txt"),) + assert node.get_abspath("../file.txt") == (os.path.abspath("../file.txt"),) def test_path_exists(tmp_path): node = PathExists() existing_file = tmp_path / "file.txt" existing_file.write_text("content") + existing_dir = tmp_path / "directory" + existing_dir.mkdir() assert node.check_exists(str(existing_file)) == (True,) + assert node.check_exists(str(existing_dir)) == (True,) assert node.check_exists("nonexistent.file") == (False,) + # Test with special paths + assert node.check_exists(".") == (True,) + # Test with path containing non-existent parent directory + assert node.check_exists(str(tmp_path / "nonexistent" / "file.txt")) == (False,) def test_path_is_file(tmp_path): @@ -41,6 +59,19 @@ def test_path_is_file(tmp_path): assert node.check_is_file(str(file)) == (True,) assert node.check_is_file(str(directory)) == (False,) assert node.check_is_file("nonexistent.file") == (False,) + # Test with empty path + assert node.check_is_file("") == (False,) + # Test with special paths + assert node.check_is_file(".") == (False,) + # Test with symlink to file if platform supports + if hasattr(os, 'symlink'): + try: + symlink = tmp_path / "symlink.txt" + os.symlink(str(file), str(symlink)) + assert node.check_is_file(str(symlink)) == (True,) + except (OSError, NotImplementedError): + # Symlinks might not be supported on all platforms + pass def test_path_is_dir(tmp_path): @@ -53,14 +84,36 @@ def test_path_is_dir(tmp_path): assert node.check_is_dir(str(directory)) == (True,) assert node.check_is_dir(str(file)) == (False,) assert node.check_is_dir("nonexistent.dir") == (False,) + # Test with special paths + assert node.check_is_dir(".") == (True,) + assert node.check_is_dir("") == (False,) + # Test with nested directory + nested_dir = directory / "nested" + nested_dir.mkdir() + assert node.check_is_dir(str(nested_dir)) == (True,) + # Test with symlink to directory if platform supports + if hasattr(os, 'symlink'): + try: + symlink = tmp_path / "symlink_dir" + os.symlink(str(directory), str(symlink), target_is_directory=True) + assert node.check_is_dir(str(symlink)) == (True,) + except (OSError, NotImplementedError, TypeError): + # Symlinks might not be supported on all platforms + pass def test_path_get_size(tmp_path): node = PathGetSize() file = tmp_path / "file.txt" file.write_text("content") + empty_file = tmp_path / "empty.txt" + empty_file.write_text("") + large_file = tmp_path / "large.txt" + large_file.write_text("a" * 1024) # 1KB file assert node.get_size(str(file)) == (len("content"),) + assert node.get_size(str(empty_file)) == (0,) + assert node.get_size(str(large_file)) == (1024,) with pytest.raises(FileNotFoundError): node.get_size("nonexistent.file") @@ -78,36 +131,88 @@ def test_path_split(): assert node.split_path(path) == (os.path.dirname(path), os.path.basename(path)) assert node.split_path("file.txt") == ("", "file.txt") + # Test with empty path + assert node.split_path("") == ("", "") + # Test with just directory separator + assert node.split_path("/") == ("/", "") + # Test with nested paths + nested_path = os.path.join("folder", "subfolder", "file.txt") + assert node.split_path(nested_path) == (os.path.dirname(nested_path), "file.txt") + # Test with trailing separator + dir_with_sep = "folder" + os.sep + assert node.split_path(dir_with_sep) == ("folder", "") def test_path_splitext(): node = PathSplitExt() assert node.split_ext("file.txt") == ("file", ".txt") assert node.split_ext("file") == ("file", "") + # Test with multiple extensions + assert node.split_ext("file.tar.gz") == ("file.tar", ".gz") + # Test with path containing directory + path = os.path.join("folder", "file.txt") + assert node.split_ext(path) == (os.path.join("folder", "file"), ".txt") + # Test with hidden file + assert node.split_ext(".gitignore") == (".gitignore", "") + # Test with empty string + assert node.split_ext("") == ("", "") def test_path_basename(): node = PathBasename() assert node.get_basename("folder/file.txt") == ("file.txt",) assert node.get_basename("file.txt") == ("file.txt",) + # Test with trailing slash + assert node.get_basename("folder/") == ("",) + # Test with empty path + assert node.get_basename("") == ("",) + # Test with root directory + assert node.get_basename("/") == ("",) + # Test with multiple nested directories + assert node.get_basename("folder/subfolder/file.txt") == ("file.txt",) def test_path_dirname(): node = PathDirname() assert node.get_dirname("folder/file.txt") == ("folder",) assert node.get_dirname("file.txt") == ("",) + # Test with trailing slash + assert node.get_dirname("folder/") == ("folder",) + # Test with empty path + assert node.get_dirname("") == ("",) + # Test with root directory + assert node.get_dirname("/") == ("/",) + # Test with nested directories + assert node.get_dirname("folder/subfolder/file.txt") == (os.path.join("folder", "subfolder"),) def test_path_get_extension(): node = PathGetExtension() assert node.get_extension("file.txt") == (".txt",) assert node.get_extension("file") == ("",) + # Test with multiple dots + assert node.get_extension("file.tar.gz") == (".gz",) + # Test with hidden file + assert node.get_extension(".gitignore") == ("",) + # Test with path containing directory + assert node.get_extension("folder/file.txt") == (".txt",) + # Test with empty string + assert node.get_extension("") == ("",) def test_path_normalize(): node = PathNormalize() assert node.normalize_path("folder/../file.txt") == (os.path.normpath("folder/../file.txt"),) assert node.normalize_path("folder//file.txt") == (os.path.normpath("folder//file.txt"),) + # Test with empty path + assert node.normalize_path("") == (".",) + # Test with current directory references + assert node.normalize_path("./folder/./file.txt") == (os.path.normpath("./folder/./file.txt"),) + # Test with parent directory references that cancel out + assert node.normalize_path("folder/../folder/file.txt") == (os.path.normpath("folder/../folder/file.txt"),) + # Test with mixed separators + mixed_sep_path = "folder" + os.sep + "subfolder" + ("/" if os.sep == "\\" else "\\") + "file.txt" + assert node.normalize_path(mixed_sep_path) == (os.path.normpath(mixed_sep_path),) def test_path_relative(tmp_path): @@ -119,6 +224,16 @@ def test_path_relative(tmp_path): assert node.get_relative_path(str(target), str(start)) == (os.path.relpath(str(target), str(start)),) assert node.get_relative_path(str(target)) == (os.path.relpath(str(target), os.getcwd()),) + # Test with same path + assert node.get_relative_path(str(start), str(start)) == (".",) + # Test with sibling paths + sibling = tmp_path / "sibling" + sibling.mkdir() + expected = os.path.relpath(str(sibling), str(start)) + assert node.get_relative_path(str(sibling), str(start)) == (expected,) + # Test with parent directory + parent_rel = os.path.relpath(str(tmp_path), str(target)) + assert node.get_relative_path(str(tmp_path), str(target)) == (parent_rel,) def test_path_glob(tmp_path): @@ -126,56 +241,162 @@ def test_path_glob(tmp_path): (tmp_path / "file1.txt").write_text("content") (tmp_path / "file2.txt").write_text("content") (tmp_path / "file3.txt").write_text("content") + (tmp_path / "image1.jpg").write_text("image") + (tmp_path / "image2.png").write_text("image") + # Create subdirectory with files + subdir = tmp_path / "subdir" + subdir.mkdir() + (subdir / "subfile1.txt").write_text("sub content") + (subdir / "subfile2.txt").write_text("sub content") + # Test simple glob pattern result = node.glob_paths(str(tmp_path / "*.txt")) expected = sorted([str(tmp_path / f"file{i}.txt") for i in range(1, 4)]) - assert sorted(result[0]) == expected - recursive_node = PathGlob() - assert recursive_node.glob_paths(str(tmp_path), recursive="True")[0] + # Test with image files + img_result = node.glob_paths(str(tmp_path / "image*")) + img_expected = sorted([str(tmp_path / "image1.jpg"), str(tmp_path / "image2.png")]) + assert sorted(img_result[0]) == img_expected + + # Test with character class + char_result = node.glob_paths(str(tmp_path / "file[1-2].txt")) + char_expected = sorted([str(tmp_path / f"file{i}.txt") for i in range(1, 3)]) + assert sorted(char_result[0]) == char_expected + + # Test recursive globbing + recursive_result = node.glob_paths(str(tmp_path / "**/*.txt"), recursive=True) + recursive_expected = sorted([ + str(tmp_path / f"file{i}.txt") for i in range(1, 4) + ] + [ + str(subdir / f"subfile{i}.txt") for i in range(1, 3) + ]) + assert sorted(recursive_result[0]) == recursive_expected + + # Test with no matches + no_match_result = node.glob_paths(str(tmp_path / "nomatch*.txt")) + assert len(no_match_result[0]) == 0 def test_path_expand_vars(monkeypatch): node = PathExpandVars() monkeypatch.setenv("TEST_VAR", "test_value") + monkeypatch.setenv("PATH_VAR", "path/to/somewhere") + monkeypatch.setenv("EMPTY_VAR", "") assert node.expand_vars("$TEST_VAR/path") == ("test_value/path",) assert node.expand_vars("${TEST_VAR}/path") == ("test_value/path",) + # Test with multiple variables + assert node.expand_vars("$TEST_VAR/$PATH_VAR") == ("test_value/path/to/somewhere",) + # Test with empty variable + assert node.expand_vars("$EMPTY_VAR/path") == ("/path",) + # Test with no variables + assert node.expand_vars("regular/path") == ("regular/path",) + # Test with empty string + assert node.expand_vars("") == ("",) def test_path_get_cwd(): node = PathGetCwd() assert node.get_cwd() == (os.getcwd(),) + # Verify that the result is an absolute path + assert os.path.isabs(node.get_cwd()[0]) + # Verify that the directory exists + assert os.path.exists(node.get_cwd()[0]) + assert os.path.isdir(node.get_cwd()[0]) def test_path_list_dir(tmp_path): node = PathListDir() + # Set up a directory structure for testing (tmp_path / "file1.txt").write_text("content") + (tmp_path / "file2.txt").write_text("content") + (tmp_path / "image.jpg").write_text("image") (tmp_path / "dir1").mkdir() + (tmp_path / "dir2").mkdir() + (tmp_path / ".hidden_file").write_text("hidden") + (tmp_path / ".hidden_dir").mkdir() + # Test listing all entries all_entries = node.list_directory(str(tmp_path)) assert "file1.txt" in all_entries[0] + assert "file2.txt" in all_entries[0] + assert "image.jpg" in all_entries[0] assert "dir1" in all_entries[0] + assert "dir2" in all_entries[0] + assert ".hidden_file" in all_entries[0] + assert ".hidden_dir" in all_entries[0] - only_files = node.list_directory(str(tmp_path), files_only="True") + # Test listing only files + only_files = node.list_directory(str(tmp_path), files_only=True) assert "file1.txt" in only_files[0] + assert "file2.txt" in only_files[0] + assert "image.jpg" in only_files[0] + assert ".hidden_file" in only_files[0] assert "dir1" not in only_files[0] + assert "dir2" not in only_files[0] + assert ".hidden_dir" not in only_files[0] - only_dirs = node.list_directory(str(tmp_path), dirs_only="True") + # Test listing only directories + only_dirs = node.list_directory(str(tmp_path), dirs_only=True) assert "file1.txt" not in only_dirs[0] + assert "file2.txt" not in only_dirs[0] + assert "image.jpg" not in only_dirs[0] + assert ".hidden_file" not in only_dirs[0] assert "dir1" in only_dirs[0] + assert "dir2" in only_dirs[0] + assert ".hidden_dir" in only_dirs[0] + + # Test with non-existent directory + with pytest.raises(FileNotFoundError): + node.list_directory(str(tmp_path / "nonexistent")) + + # Test with file instead of directory + with pytest.raises(NotADirectoryError): + node.list_directory(str(tmp_path / "file1.txt")) + + # Test with empty path (should use current directory) + assert len(node.list_directory("")[0]) > 0 def test_path_is_absolute(): node = PathIsAbsolute() + # Test with absolute paths assert node.check_is_absolute("/") == (True,) + assert node.check_is_absolute(os.path.abspath(".")) == (True,) + # Test with relative paths assert node.check_is_absolute("relative/path") == (False,) + assert node.check_is_absolute("./path") == (False,) + assert node.check_is_absolute("../path") == (False,) + # Test with empty path + assert node.check_is_absolute("") == (False,) + # Test Windows-specific paths if applicable + if platform.system() == "Windows": + assert node.check_is_absolute("C:\\Windows") == (True,) + assert node.check_is_absolute("C:/Windows") == (True,) + assert node.check_is_absolute("\\\\server\\share") == (True,) + assert node.check_is_absolute("C:relative\\path") == (False,) def test_path_common_prefix(): node = PathCommonPrefix() + # Test with paths having common prefix path1 = "/path/to/file1.txt" path2 = "/path/to/file2.txt" + assert node.get_common_prefix(path1, path2) == ("/path/to/file",) - assert node.get_common_prefix(path1, path2) == (os.path.commonprefix([path1, path2]),) + # Test with paths without common prefix + path3 = "/path/to/file.txt" + path4 = "/different/path/file.txt" + assert node.get_common_prefix(path3, path4) == ("/",) + + # Test with exact same paths + assert node.get_common_prefix(path1, path1) == (path1,) + + # Test with one path being a prefix of the other + path5 = "/path/to" + path6 = "/path/to/subdir/file.txt" + assert node.get_common_prefix(path5, path6) == ("/path/to",) + + # Test with both empty paths + assert node.get_common_prefix("", "") == ("",) diff --git a/tests/test_regex_nodes.py b/tests/test_regex_nodes.py index ad232d7..ff35d42 100644 --- a/tests/test_regex_nodes.py +++ b/tests/test_regex_nodes.py @@ -5,7 +5,10 @@ from src.basic_data_handling.regex_nodes import ( RegexFindallDataList, RegexSplitDataList, RegexSub, - RegexTest + RegexTest, + RegexFindallList, + RegexSearchGroupsList, + RegexSplitList ) def test_regex_search_groups(): @@ -16,6 +19,14 @@ def test_regex_search_groups(): assert node.search_groups(r"(\w+)", "word") == (["word"],) # Single group +def test_regex_search_groups_list(): + node = RegexSearchGroupsList() + assert node.search_groups(r"(foo)(bar)", "foobar") == (["foo", "bar"],) + assert node.search_groups(r"(test)(\d+)", "test123") == (["test", "123"],) + assert node.search_groups(r"(nothing)", "no match here") == ([],) # No match + assert node.search_groups(r"(\w+)", "word") == (["word"],) # Single group + + def test_regex_group_dict(): node = RegexGroupDict() pattern = r"(?P\w+)-(?P\d+)" @@ -33,6 +44,14 @@ def test_regex_findall(): assert node.findall(r"(a)(b)", "ab ab") == ([("a", "b"), ("a", "b")],) # Multiple groups +def test_regex_findall_list(): + node = RegexFindallList() + assert node.findall(r"\d+", "abc 123 def 456") == (["123", "456"],) + assert node.findall(r"\b[a-zA-Z]+\b", "Multiple words here") == (["Multiple", "words", "here"],) + assert node.findall(r"nonexistent", "no match") == ([],) # No match + assert node.findall(r"(a)(b)", "ab ab") == ([("a", "b"), ("a", "b")],) # Multiple groups + + def test_regex_split(): node = RegexSplitDataList() pattern = r",\s*" @@ -42,6 +61,15 @@ def test_regex_split(): assert node.split(r"z", "nozsplit") == (["no", "split"],) # Split by 'z' +def test_regex_split_list(): + node = RegexSplitList() + pattern = r",\s*" + string = "one, two, three" + assert node.split(pattern, string) == (["one", "two", "three"],) + assert node.split(r"\s+", "split several words") == (["split", "several", "words"],) # Whitespace split + assert node.split(r"z", "nozsplit") == (["no", "split"],) # Split by 'z' + + def test_regex_sub(): node = RegexSub() assert node.sub(r"cat", "dog", "cat is here") == ("dog is here",) diff --git a/tests/test_set_nodes.py b/tests/test_set_nodes.py index 9be76ac..aeb3fb4 100644 --- a/tests/test_set_nodes.py +++ b/tests/test_set_nodes.py @@ -13,9 +13,52 @@ from src.basic_data_handling.set_nodes import ( SetIsDisjoint, SetContains, SetLength, - SetToList + SetToList, + SetCreate, + SetToDataList, + SetCreateFromInt, + SetCreateFromString, + SetCreateFromFloat, + SetCreateFromBoolean ) +def test_set_create(): + node = SetCreate() + # Testing with kwargs to simulate dynamic inputs + assert node.create_set(item_0=1, item_1=2, item_2=3) == ({1, 2, 3},) + assert node.create_set(item_0="a", item_1="b") == ({"a", "b"},) + assert node.create_set() == (set(),) # Empty set with no arguments + + +def test_set_create_from_int(): + node = SetCreateFromInt() + assert node.create_set(item_0=1, item_1=2, item_2=3) == ({1, 2, 3},) + assert node.create_set(item_0=5) == ({5},) # Single item set + assert node.create_set(item_0=1, item_1=1) == ({1},) # Duplicate items become single item + + +def test_set_create_from_string(): + node = SetCreateFromString() + # Note: Mocking the string function behavior as it's not defined in the file + # This simulates what would happen assuming string() acts like str() + node.create_set = lambda **kwargs: (set([str(value) for value in kwargs.values()]),) + assert node.create_set(item_0="apple", item_1="banana") == ({"apple", "banana"},) + assert node.create_set(item_0="apple", item_1="apple") == ({"apple"},) # Duplicate strings + + +def test_set_create_from_float(): + node = SetCreateFromFloat() + assert node.create_set(item_0=1.5, item_1=2.5) == ({1.5, 2.5},) + assert node.create_set(item_0=3.14) == ({3.14},) # Single item set + assert node.create_set(item_0=1.0, item_1=1.0) == ({1.0},) # Duplicate items + + +def test_set_create_from_boolean(): + node = SetCreateFromBoolean() + assert node.create_set(item_0=True, item_1=False) == ({True, False},) + assert node.create_set(item_0=True, item_1=True) == ({True},) # Duplicate booleans + + def test_set_add(): node = SetAdd() assert node.add({1, 2}, 3) == ({1, 2, 3},) @@ -108,3 +151,11 @@ def test_set_to_list(): result = node.convert({1, 2, 3}) assert isinstance(result, tuple) assert sorted(result[0]) == [1, 2, 3] # Validate conversion to list + + +def test_set_to_data_list(): + node = SetToDataList() + result = node.convert({1, 2, 3}) + assert isinstance(result, tuple) + assert isinstance(result[0], list) + assert sorted(result[0]) == [1, 2, 3] # Validate conversion to data list diff --git a/web/js/dynamicnode.js b/web/js/dynamicnode.js new file mode 100644 index 0000000..ce8bef5 --- /dev/null +++ b/web/js/dynamicnode.js @@ -0,0 +1,293 @@ +/** + * ComfyUI basic data handling. + * Copyright (C) 2025 StableLlama + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Purpose: Manage dynamic input nodes in ComfyUI. + * Inspired by cozy_ex_dynamic + */ + +import { app } from "../../../scripts/app.js"; + +const TypeSlot = { + Input: 1, + Output: 2 +}; + +const TypeSlotEvent = { + Connect: true, + Disconnect: false +}; + +app.registerExtension({ + name: 'Basic data handling: dynamic input', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // Filter: Only process nodes with class names starting with "Basic data handling:" + if (!nodeType.comfyClass.startsWith('Basic data handling:')) { + return; + } + + const combinedInputData = { + ...nodeData?.input?.required ?? {}, + ...nodeData?.input?.optional ?? {} + }; + + const dynamicInputs = []; + for (const name in combinedInputData) { + const dynamic = combinedInputData[name][1]?._dynamic; + + if (dynamic) { + let matcher; + + switch (dynamic) { + case 'number': + matcher = new RegExp(`^(${name.replace(/\d*$/, '')})(\\d+)$`); + break; + + case 'letter': + matcher = new RegExp(`^()([a-zA-Z])$`); + break; + + default: + continue; + } + const baseName = name.match(matcher)?.[1] ?? name; + const dynamicType = combinedInputData[name][0]; + dynamicInputs.push({name, baseName, matcher, dynamic, dynamicType}); + } + } + if (dynamicInputs.length === 0) { + return; + } + + /** + * Utility: Check if an input is dynamic. + * @param {string} inputName - Name of the input to check. + */ + const isDynamicInput = (inputName) => + dynamicInputs.some((di) => di.matcher.test(inputName)); + + /** + * Utility: Update inputs' slot indices after reordering. + * @param {Array} inputs - List of inputs for the node. + * @param {Object} graph - Graph containing the node. + */ + const updateSlotIndices = (inputs, graph) => { + inputs.forEach((input, index) => { + input.slot_index = index; + if (input.link !== null) { + const link = graph._links.get(input.link); + if (link) + link.target_slot = index; + 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 + 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 + return newInput; + }; + + // flag to prevent loops. It is ok to be "global" as the code is not + // running in parallel. + let isProcessingConnection = false; + + // Override onConnectionsChange: Handle connections for dynamic inputs + const onConnectionsChange = nodeType.prototype.onConnectionsChange; + nodeType.prototype.onConnectionsChange = function (type, slotIndex, isConnected, link, ioSlot) { + const result = onConnectionsChange?.apply(this, arguments); + + if (type !== TypeSlot.Input || isProcessingConnection) { + return result; + } + + 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); + + // Handle connection event + if (isConnected === TypeSlotEvent.Connect) { + const hasEmptyDynamic = dynamicSlots.some(di => !di.connected); + + 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 inputInRange = true; + + 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}`; + } + + 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); + } + } + } else if (isConnected === TypeSlotEvent.Disconnect) { + let hasEmptyDynamic = false; + 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); + continue; + } + if (isDynamic && input.link === null) { + hasEmptyDynamic = true; + } + } + this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic); + } + + this.setDirtyCanvas(true, true); + } catch (e) { + console.error(e); + debugger; + alert(e); + } finally { + isProcessingConnection = false; + } + + return result; + }; + + // Method to swap two inputs in the "this.inputs" array by their indices + nodeType.prototype.swapInputs = function(indexA, indexB) { + // Validate indices + if ( + indexA < 0 || indexB < 0 || + indexA >= this.inputs.length || + indexB >= this.inputs.length || + indexA === indexB + ) { + console.warn("Invalid input indices for swapping:", indexA, indexB); + return; + } + + // Swap the inputs in the array + [this.inputs[indexA], this.inputs[indexB]] = [this.inputs[indexB], this.inputs[indexA]]; + updateSlotIndices(this.inputs, this.graph); // Refresh indices + + // Redraw the node to ensure the graph updates properly + // -> not needed as the calling method must do it! + this.setDirtyCanvas(true, true); + }; + + // Add method to safely renumber dynamic inputs without breaking connections + nodeType.prototype.renumberDynamicInputs = function(baseName, dynamicInputs, dynamic) { + // Get current dynamic inputs info + const dynamicInputInfo = []; + + for (let i = 0; i < this.inputs.length; i++) { + const input = this.inputs[i]; + const isDynamic = isDynamicInput(input.name); + + if (isDynamic) { + dynamicInputInfo.push({ + index: i, + name: input.name, + connected: input.link !== null + }); + } + } + + // 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 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; + } + } + }; + } +});