From e5eec7c74de950193214c283c14e908787d6f9db Mon Sep 17 00:00:00 2001 From: StableLlama Date: Thu, 15 May 2025 21:13:06 +0200 Subject: [PATCH] version 0.2.1 Sort the node names --- pyproject.toml | 2 +- src/basic_data_handling/boolean_nodes.py | 154 ++-- src/basic_data_handling/casting_nodes.py | 72 +- src/basic_data_handling/comparison_nodes.py | 224 +++--- src/basic_data_handling/data_list_nodes.py | 742 +++++++++--------- src/basic_data_handling/dict_nodes.py | 806 +++++++++---------- src/basic_data_handling/float_nodes.py | 210 ++--- src/basic_data_handling/int_nodes.py | 164 ++-- src/basic_data_handling/list_nodes.py | 704 ++++++++--------- src/basic_data_handling/math_nodes.py | 560 +++++++------- src/basic_data_handling/path_nodes.py | 810 ++++++++++---------- src/basic_data_handling/regex_nodes.py | 154 ++-- src/basic_data_handling/set_nodes.py | 374 +++++---- src/basic_data_handling/string_nodes.py | 455 +++++------ web/js/example.js | 1 - 15 files changed, 2704 insertions(+), 2728 deletions(-) delete mode 100644 web/js/example.js diff --git a/pyproject.toml b/pyproject.toml index 9a4fa04..168689f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "0.2.0" +version = "0.2.1" description = """NOTE: Still in development! Expect breaking changes! Basic Python functions for manipulating data that every programmer is used to. Currently supported ComfyUI data types: BOOLEAN, FLOAT, INT, STRING and data lists. diff --git a/src/basic_data_handling/boolean_nodes.py b/src/basic_data_handling/boolean_nodes.py index c0e48bc..8f925f0 100644 --- a/src/basic_data_handling/boolean_nodes.py +++ b/src/basic_data_handling/boolean_nodes.py @@ -25,77 +25,6 @@ class BooleanAnd(ComfyNodeABC): return (input1 and input2,) -class BooleanOr(ComfyNodeABC): - """ - Returns the logical OR result of two boolean values. - - This node takes two boolean inputs and returns their logical OR result. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), - "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - CATEGORY = "Basic/BOOLEAN" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "or_operation" - - def or_operation(self, input1: bool, input2: bool) -> tuple[bool]: - return (input1 or input2,) - - -class BooleanNot(ComfyNodeABC): - """ - Returns the logical NOT result of a boolean value. - - This node takes one boolean input and returns its logical NOT result. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input": (IO.BOOLEAN, {"default": False, "forceInput": True}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - CATEGORY = "Basic/BOOLEAN" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "not_operation" - - def not_operation(self, input: bool) -> tuple[bool]: - return (not input,) - - -class BooleanXor(ComfyNodeABC): - """ - Returns the logical XOR result of two boolean values. - - This node takes two boolean inputs and returns their logical XOR result. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), - "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - CATEGORY = "Basic/BOOLEAN" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "xor_operation" - - def xor_operation(self, input1: bool, input2: bool) -> tuple[bool]: - return (input1 != input2,) - - class BooleanNand(ComfyNodeABC): """ Returns the logical NAND result of two boolean values. @@ -144,20 +73,91 @@ class BooleanNor(ComfyNodeABC): return (not (input1 or input2),) +class BooleanNot(ComfyNodeABC): + """ + Returns the logical NOT result of a boolean value. + + This node takes one boolean input and returns its logical NOT result. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input": (IO.BOOLEAN, {"default": False, "forceInput": True}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + CATEGORY = "Basic/BOOLEAN" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "not_operation" + + def not_operation(self, input: bool) -> tuple[bool]: + return (not input,) + + +class BooleanOr(ComfyNodeABC): + """ + Returns the logical OR result of two boolean values. + + This node takes two boolean inputs and returns their logical OR result. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), + "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + CATEGORY = "Basic/BOOLEAN" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "or_operation" + + def or_operation(self, input1: bool, input2: bool) -> tuple[bool]: + return (input1 or input2,) + + +class BooleanXor(ComfyNodeABC): + """ + Returns the logical XOR result of two boolean values. + + This node takes two boolean inputs and returns their logical XOR result. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), + "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + CATEGORY = "Basic/BOOLEAN" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "xor_operation" + + def xor_operation(self, input1: bool, input2: bool) -> tuple[bool]: + return (input1 != input2,) + + NODE_CLASS_MAPPINGS = { "Basic data handling: BooleanAnd": BooleanAnd, - "Basic data handling: BooleanOr": BooleanOr, - "Basic data handling: BooleanNot": BooleanNot, - "Basic data handling: BooleanXor": BooleanXor, "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, } NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: BooleanAnd": "and", - "Basic data handling: BooleanOr": "or", - "Basic data handling: BooleanNot": "not", - "Basic data handling: BooleanXor": "xor", "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", } diff --git a/src/basic_data_handling/casting_nodes.py b/src/basic_data_handling/casting_nodes.py index 8feb940..15c0b27 100644 --- a/src/basic_data_handling/casting_nodes.py +++ b/src/basic_data_handling/casting_nodes.py @@ -2,9 +2,9 @@ from typing import Any from inspect import cleandoc from comfy.comfy_types.node_typing import IO, ComfyNodeABC -class CastToString(ComfyNodeABC): +class CastToBoolean(ComfyNodeABC): """ - Converts any input to a STRING. Non-string values are converted using str(). + Converts any input to a BOOLEAN. Follows standard Python truthy/falsy rules. """ @classmethod def INPUT_TYPES(cls): @@ -14,18 +14,18 @@ class CastToString(ComfyNodeABC): } } - RETURN_TYPES = (IO.STRING,) + RETURN_TYPES = ("BOOLEAN",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "convert_to_string" + FUNCTION = "convert_to_boolean" - def convert_to_string(self, input: Any) -> tuple[str]: - return (str(input),) + def convert_to_boolean(self, input: Any) -> tuple[bool]: + return (bool(input),) -class CastToInt(ComfyNodeABC): +class CastToDict(ComfyNodeABC): """ - Converts any numeric input to an INT. Non-numeric or invalid inputs raise a ValueError. + Converts compatible inputs to a DICT. Input must be a mapping or a list of key-value pairs. """ @classmethod def INPUT_TYPES(cls): @@ -35,16 +35,16 @@ class CastToInt(ComfyNodeABC): } } - RETURN_TYPES = (IO.INT,) + RETURN_TYPES = ("DICT",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "convert_to_int" + FUNCTION = "convert_to_dict" - def convert_to_int(self, input: Any) -> tuple[int]: + def convert_to_dict(self, input: Any) -> tuple[dict]: try: - return (int(input),) + return (dict(input),) except (ValueError, TypeError): - raise ValueError(f"Cannot convert {input} to an INT.") + raise ValueError(f"Cannot convert {input} to a DICT. Ensure it is a mapping or list of key-value pairs.") class CastToFloat(ComfyNodeABC): @@ -71,9 +71,9 @@ class CastToFloat(ComfyNodeABC): raise ValueError(f"Cannot convert {input} to a FLOAT.") -class CastToBoolean(ComfyNodeABC): +class CastToInt(ComfyNodeABC): """ - Converts any input to a BOOLEAN. Follows standard Python truthy/falsy rules. + Converts any numeric input to an INT. Non-numeric or invalid inputs raise a ValueError. """ @classmethod def INPUT_TYPES(cls): @@ -83,13 +83,16 @@ class CastToBoolean(ComfyNodeABC): } } - RETURN_TYPES = ("BOOLEAN",) + RETURN_TYPES = (IO.INT,) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "convert_to_boolean" + FUNCTION = "convert_to_int" - def convert_to_boolean(self, input: Any) -> tuple[bool]: - return (bool(input),) + def convert_to_int(self, input: Any) -> tuple[int]: + try: + return (int(input),) + except (ValueError, TypeError): + raise ValueError(f"Cannot convert {input} to an INT.") class CastToList(ComfyNodeABC): @@ -140,9 +143,9 @@ class CastToSet(ComfyNodeABC): return ({input,} if not isinstance(input, list) else set(input),) -class CastToDict(ComfyNodeABC): +class CastToString(ComfyNodeABC): """ - Converts compatible inputs to a DICT. Input must be a mapping or a list of key-value pairs. + Converts any input to a STRING. Non-string values are converted using str(). """ @classmethod def INPUT_TYPES(cls): @@ -152,34 +155,31 @@ class CastToDict(ComfyNodeABC): } } - RETURN_TYPES = ("DICT",) + RETURN_TYPES = (IO.STRING,) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "convert_to_dict" + FUNCTION = "convert_to_string" - def convert_to_dict(self, input: Any) -> tuple[dict]: - try: - return (dict(input),) - except (ValueError, TypeError): - raise ValueError(f"Cannot convert {input} to a DICT. Ensure it is a mapping or list of key-value pairs.") + def convert_to_string(self, input: Any) -> tuple[str]: + return (str(input),) NODE_CLASS_MAPPINGS = { - "Basic data handling: CastToString": CastToString, - "Basic data handling: CastToInt": CastToInt, - "Basic data handling: CastToFloat": CastToFloat, "Basic data handling: CastToBoolean": CastToBoolean, + "Basic data handling: CastToDict": CastToDict, + "Basic data handling: CastToFloat": CastToFloat, + "Basic data handling: CastToInt": CastToInt, "Basic data handling: CastToList": CastToList, "Basic data handling: CastToSet": CastToSet, - "Basic data handling: CastToDict": CastToDict, + "Basic data handling: CastToString": CastToString, } NODE_DISPLAY_NAME_MAPPINGS = { - "Basic data handling: CastToString": "to STRING", - "Basic data handling: CastToInt": "to INT", - "Basic data handling: CastToFloat": "to FLOAT", "Basic data handling: CastToBoolean": "to BOOLEAN", + "Basic data handling: CastToDict": "to DICT", + "Basic data handling: CastToFloat": "to FLOAT", + "Basic data handling: CastToInt": "to INT", "Basic data handling: CastToList": "to LIST", "Basic data handling: CastToSet": "to SET", - "Basic data handling: CastToDict": "to DICT", + "Basic data handling: CastToString": "to STRING", } diff --git a/src/basic_data_handling/comparison_nodes.py b/src/basic_data_handling/comparison_nodes.py index 9b1493b..da4f7de 100644 --- a/src/basic_data_handling/comparison_nodes.py +++ b/src/basic_data_handling/comparison_nodes.py @@ -58,32 +58,6 @@ class NotEqual(ComfyNodeABC): return (value1 != value2,) -class GreaterThan: - """ - Checks if the first value is greater than the second. - - This node takes two numerical inputs and returns True if the first value - is greater than the second value, and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value1": (IO.NUMBER, {"widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"widgetType": "FLOAT"}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("result",) - CATEGORY = "Basic/comparison" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "compare" - - def compare(self, value1: float, value2: float) -> tuple[bool]: - return (value1 > value2,) - - class LessThan: """ Checks if the first value is less than the second. @@ -110,32 +84,6 @@ class LessThan: return (value1 < value2,) -class GreaterThanOrEqual: - """ - Checks if the first value is greater than or equal to the second. - - This node takes two numerical inputs and returns True if the first value - is greater than or equal to the second value, and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value1": (IO.NUMBER, {"widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"widgetType": "FLOAT"}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("result",) - CATEGORY = "Basic/comparison" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "compare" - - def compare(self, value1: float, value2: float) -> tuple[bool]: - return (value1 >= value2,) - - class LessThanOrEqual: """ Checks if the first value is less than or equal to the second. @@ -162,21 +110,19 @@ class LessThanOrEqual: return (value1 <= value2,) -class StringComparison: +class GreaterThan: """ - Compares two strings using a selected comparison operator. + Checks if the first value is greater than the second. - This node takes two string inputs and a comparison operator, and returns - a boolean result based on the selected comparison. + This node takes two numerical inputs and returns True if the first value + is greater than the second value, and False otherwise. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "string1": ("STRING", {"default": ""}), - "string2": ("STRING", {"default": ""}), - "operator": (["==", "!=", ">", "<", ">=", "<="], {"default": "=="}), - "case_sensitive": (IO.BOOLEAN, {"default": True}), + "value1": (IO.NUMBER, {"widgetType": "FLOAT"}), + "value2": (IO.NUMBER, {"widgetType": "FLOAT"}), } } @@ -186,25 +132,59 @@ class StringComparison: DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" - def compare(self, string1: str, string2: str, operator: str, case_sensitive: str) -> tuple[bool]: - if case_sensitive == "False": - string1 = string1.lower() - string2 = string2.lower() + def compare(self, value1: float, value2: float) -> tuple[bool]: + return (value1 > value2,) - if operator == "==": - return (string1 == string2,) - elif operator == "!=": - return (string1 != string2,) - elif operator == ">": - return (string1 > string2,) - elif operator == "<": - return (string1 < string2,) - elif operator == ">=": - return (string1 >= string2,) - elif operator == "<=": - return (string1 <= string2,) - else: - raise ValueError(f"Unknown operator: {operator}") + +class GreaterThanOrEqual: + """ + Checks if the first value is greater than or equal to the second. + + This node takes two numerical inputs and returns True if the first value + is greater than or equal to the second value, and False otherwise. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value1": (IO.NUMBER, {"widgetType": "FLOAT"}), + "value2": (IO.NUMBER, {"widgetType": "FLOAT"}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + CATEGORY = "Basic/comparison" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "compare" + + def compare(self, value1: float, value2: float) -> tuple[bool]: + return (value1 >= value2,) + + +class IsNull: + """ + Checks if a value is None/null. + + This node takes any input value and returns True if the value is None, + and False otherwise. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("is_null",) + CATEGORY = "Basic/comparison" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "check_null" + + def check_null(self, value: Any) -> tuple[bool]: + return (value is None,) class NumberInRange: @@ -236,38 +216,13 @@ class NumberInRange: FUNCTION = "check_range" def check_range(self, value: float, min_value: float, max_value: float, - include_min: str = "True", include_max: str = "True") -> tuple[bool]: + include_min: str = "True", include_max: str = "True") -> tuple[bool]: min_check = value >= min_value if include_min == "True" else value > min_value max_check = value <= max_value if include_max == "True" else value < max_value return (min_check and max_check,) -class IsNull: - """ - Checks if a value is None/null. - - This node takes any input value and returns True if the value is None, - and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": (IO.ANY, {}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("is_null",) - CATEGORY = "Basic/comparison" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "check_null" - - def check_null(self, value: Any) -> tuple[bool]: - return (value is None,) - - class CompareLength: """ Compares the length of a container (string, list, etc) with a value. @@ -314,28 +269,73 @@ class CompareLength: raise ValueError(f"Unknown operator: {operator}") +class StringComparison: + """ + Compares two strings using a selected comparison operator. + + This node takes two string inputs and a comparison operator, and returns + a boolean result based on the selected comparison. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string1": ("STRING", {"default": ""}), + "string2": ("STRING", {"default": ""}), + "operator": (["==", "!=", ">", "<", ">=", "<="], {"default": "=="}), + "case_sensitive": (IO.BOOLEAN, {"default": True}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + CATEGORY = "Basic/comparison" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "compare" + + def compare(self, string1: str, string2: str, operator: str, case_sensitive: str) -> tuple[bool]: + if case_sensitive == "False": + string1 = string1.lower() + string2 = string2.lower() + + if operator == "==": + return (string1 == string2,) + elif operator == "!=": + return (string1 != string2,) + elif operator == ">": + return (string1 > string2,) + elif operator == "<": + return (string1 < string2,) + elif operator == ">=": + return (string1 >= string2,) + elif operator == "<=": + return (string1 <= string2,) + else: + raise ValueError(f"Unknown operator: {operator}") + + NODE_CLASS_MAPPINGS = { "Basic data handling: Equal": Equal, "Basic data handling: NotEqual": NotEqual, - "Basic data handling: GreaterThan": GreaterThan, "Basic data handling: LessThan": LessThan, - "Basic data handling: GreaterThanOrEqual": GreaterThanOrEqual, "Basic data handling: LessThanOrEqual": LessThanOrEqual, - "Basic data handling: StringComparison": StringComparison, - "Basic data handling: NumberInRange": NumberInRange, + "Basic data handling: GreaterThan": GreaterThan, + "Basic data handling: GreaterThanOrEqual": GreaterThanOrEqual, "Basic data handling: IsNull": IsNull, + "Basic data handling: NumberInRange": NumberInRange, "Basic data handling: CompareLength": CompareLength, + "Basic data handling: StringComparison": StringComparison, } NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: Equal": "==", "Basic data handling: NotEqual": "!=", - "Basic data handling: GreaterThan": ">", "Basic data handling: LessThan": "<", - "Basic data handling: GreaterThanOrEqual": ">=", "Basic data handling: LessThanOrEqual": "<=", - "Basic data handling: StringComparison": "string compare", - "Basic data handling: NumberInRange": "in range", + "Basic data handling: GreaterThan": ">", + "Basic data handling: GreaterThanOrEqual": ">=", "Basic data handling: IsNull": "is null", + "Basic data handling: NumberInRange": "in range", "Basic data handling: CompareLength": "compare length", + "Basic data handling: StringComparison": "string compare", } diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 928de26..f55c084 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -36,6 +36,85 @@ class DataListAppend(ComfyNodeABC): return (result,) +class DataListContains(ComfyNodeABC): + """ + Checks if a list contains a specified value. + + This node takes a list and a value as inputs, then returns True if the value + is present in the list, and False otherwise. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY,), + "value": (IO.ANY,), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("contains",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "contains" + INPUT_IS_LIST = True + + def contains(self, **kwargs: list[Any]) -> tuple[bool]: + value = kwargs.get('value', []) + if len(value) == 0: + return (False,) + return (value[0] in kwargs.get('list', []),) + + +class DataListCount(ComfyNodeABC): + """ + Counts the number of occurrences of a value in a list. + + This node takes a list and a value as inputs, then returns the number of times + the value appears in the list. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY,), + "value": (IO.ANY,), + } + } + + RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("count",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "count" + INPUT_IS_LIST = True + + def count(self, **kwargs: list[Any]) -> tuple[int]: + value = kwargs.get('value', [None])[0] + 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. @@ -64,12 +143,51 @@ class DataListExtend(ComfyNodeABC): return (kwargs.get('list_a', []) + kwargs.get('list_b', []),) -class DataListInsert(ComfyNodeABC): +class DataListFilter(ComfyNodeABC): """ - Inserts an item at a specified position in a list. + Filters a data list using boolean values. - This node takes a list, an index, and any item as inputs, then returns a new - list with the item inserted at the specified index. + 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 + till the lengths are matching. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {}), + "filter": (IO.BOOLEAN, {"forceInput": True}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("filtered_list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "filter_data" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + def filter_data(self, **kwargs: list[Any]) -> tuple[list[Any]]: + values = kwargs.get('value', []) + filters = kwargs.get('filter', []) + + # Create a new list with only items where the filter is False + result = [_val for _val, _filter in zip(values, filters) if not _filter] + + return (result,) + + +class DataListGetItem(ComfyNodeABC): + """ + Retrieves an item at a specified position in a list. + + This node takes a list and an index as inputs, then returns the item at the specified index. + Negative indices count from the end of the list. + Out of range indices return None. """ @classmethod def INPUT_TYPES(cls): @@ -77,94 +195,22 @@ class DataListInsert(ComfyNodeABC): "required": { "list": (IO.ANY,), "index": (IO.INT, {"default": 0}), - "item": (IO.ANY,), } } RETURN_TYPES = (IO.ANY,) - RETURN_NAMES = ("list",) + RETURN_NAMES = ("item",) CATEGORY = "Basic/data list" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "insert" + FUNCTION = "get_item" INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - def insert(self, **kwargs: list[Any]) -> tuple[list[Any]]: - result = kwargs.get('list', []).copy() - result.insert(kwargs.get('index', [0])[0], kwargs.get('item', [None])[0]) - return (result,) - - -class DataListRemove(ComfyNodeABC): - """ - Removes the first occurrence of a specified value from a list. - - This node takes a list and a value as inputs, then returns a new list with - the first occurrence of the value removed. Raises a ValueError if the value is not present. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": (IO.ANY,), - "value": (IO.ANY,), - } - } - - RETURN_TYPES = (IO.ANY, IO.BOOLEAN,) - RETURN_NAMES = ("list", "success",) - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "remove" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True, False,) - - def remove(self, **kwargs: list[Any]) -> tuple[list[Any], bool]: - result = kwargs.get('list', []).copy() - value = kwargs.get('value', []) + def get_item(self, **kwargs: list[Any]) -> tuple[Any]: + index = kwargs.get('index', [0])[0] try: - result.remove(value[0]) - return result, True - except ValueError: - return result, False - - -class DataListPop(ComfyNodeABC): - """ - Removes and returns an item at a specified position in a list. - - This node takes a list and an index as inputs, then returns both the new list - with the item removed and the removed item. If no index is specified, - removes and returns the last item. - When the list is empty, the item is None. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": (IO.ANY,), - }, - "optional": { - "index": (IO.INT, {"default": -1}), - } - } - - RETURN_TYPES = (IO.ANY, IO.ANY) - RETURN_NAMES = ("list", "item") - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "pop" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True, False) - - def pop(self, **kwargs: list[Any]) -> tuple[list[Any], Any]: - result = kwargs.get('list', []).copy() - index = kwargs.get('index', [-1])[0] - try: - item = result.pop(index) - return result, item + return (kwargs.get('list', [])[index],) except IndexError: - return result, None + return (None,) class DataListIndex(ComfyNodeABC): @@ -209,12 +255,178 @@ class DataListIndex(ComfyNodeABC): return (-1,) -class DataListCount(ComfyNodeABC): +class DataListInsert(ComfyNodeABC): """ - Counts the number of occurrences of a value in a list. + Inserts an item at a specified position in a list. - This node takes a list and a value as inputs, then returns the number of times - the value appears in the list. + This node takes a list, an index, and any item as inputs, then returns a new + list with the item inserted at the specified index. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY,), + "index": (IO.INT, {"default": 0}), + "item": (IO.ANY,), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "insert" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + def insert(self, **kwargs: list[Any]) -> tuple[list[Any]]: + result = kwargs.get('list', []).copy() + result.insert(kwargs.get('index', [0])[0], kwargs.get('item', [None])[0]) + return (result,) + + +class DataListLength(ComfyNodeABC): + """ + Counts the number of items in a list. + + This node takes a list as input and returns its length as an integer. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY,), + } + } + + RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("length",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "length" + INPUT_IS_LIST = True + + def length(self, **kwargs: list[Any]) -> tuple[int]: + return (len(kwargs.get('list', [])),) + + +class DataListMax(ComfyNodeABC): + """ + Finds the maximum value in a list of numbers. + + This node takes a list of numbers (either FLOAT or INT) and returns + the maximum value. Returns None if the list is empty or if it contains + non-numeric values. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.NUMBER, {}), + } + } + + RETURN_TYPES = (IO.NUMBER,) + RETURN_NAMES = ("max",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "find_max" + INPUT_IS_LIST = True + + def find_max(self, **kwargs: list[Any]) -> tuple[Any]: + values = kwargs.get('list', []) + if not values: + return (None,) + + try: + # Return the same type as found in the maximum value + return (max(values),) + except (TypeError, ValueError): + # Handle case where list contains non-comparable elements + return (None,) + + +class DataListMin(ComfyNodeABC): + """ + Finds the minimum value in a list of numbers. + + This node takes a list of numbers (either FLOAT or INT) and returns + the minimum value. Returns None if the list is empty or if it contains + non-numeric values. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.NUMBER, {}), + } + } + + RETURN_TYPES = (IO.NUMBER,) + RETURN_NAMES = ("min",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "find_min" + INPUT_IS_LIST = True + + def find_min(self, **kwargs: list[Any]) -> tuple[Any]: + values = kwargs.get('list', []) + if not values: + return (None,) + + try: + # Return the same type as found in the minimum value + return (min(values),) + except (TypeError, ValueError): + # Handle case where list contains non-comparable elements + return (None,) + + +class DataListPop(ComfyNodeABC): + """ + Removes and returns an item at a specified position in a list. + + This node takes a list and an index as inputs, then returns both the new list + with the item removed and the removed item. If no index is specified, + removes and returns the last item. + When the list is empty, the item is None. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY,), + }, + "optional": { + "index": (IO.INT, {"default": -1}), + } + } + + RETURN_TYPES = (IO.ANY, IO.ANY) + RETURN_NAMES = ("list", "item") + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True, False) + + def pop(self, **kwargs: list[Any]) -> tuple[list[Any], Any]: + result = kwargs.get('list', []).copy() + index = kwargs.get('index', [-1])[0] + try: + item = result.pop(index) + return result, item + except IndexError: + return result, None + + +class DataListRemove(ComfyNodeABC): + """ + Removes the first occurrence of a specified value from a list. + + This node takes a list and a value as inputs, then returns a new list with + the first occurrence of the value removed. Raises a ValueError if the value is not present. """ @classmethod def INPUT_TYPES(cls): @@ -225,50 +437,22 @@ class DataListCount(ComfyNodeABC): } } - RETURN_TYPES = (IO.INT,) - RETURN_NAMES = ("count",) + RETURN_TYPES = (IO.ANY, IO.BOOLEAN,) + RETURN_NAMES = ("list", "success",) CATEGORY = "Basic/data list" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "count" + FUNCTION = "remove" INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True, False,) - def count(self, **kwargs: list[Any]) -> tuple[int]: - value = kwargs.get('value', [None])[0] - return (kwargs.get('list', []).count(value),) - - -class DataListSort(ComfyNodeABC): - """ - Sorts the items in a list. - - This node takes a list as input and returns a new sorted list. - Options include sorting in reverse order and using a key function. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": (IO.ANY,), - }, - "optional": { - "reverse": (["False", "True"], {"default": "False"}), - } - } - - RETURN_TYPES = (IO.ANY,) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "sort" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - - def sort(self, **kwargs: list[Any]) -> tuple[list[Any]]: - # Convert string to boolean - reverse = kwargs.get('reverse', ["False"])[0] == "True" - - result = sorted(kwargs.get('list', []), reverse=reverse) - return (result,) + def remove(self, **kwargs: list[Any]) -> tuple[list[Any], bool]: + result = kwargs.get('list', []).copy() + value = kwargs.get('value', []) + try: + result.remove(value[0]) + return result, True + except ValueError: + return result, False class DataListReverse(ComfyNodeABC): @@ -299,29 +483,41 @@ class DataListReverse(ComfyNodeABC): return (result,) -class DataListLength(ComfyNodeABC): +class DataListSetItem(ComfyNodeABC): """ - Counts the number of items in a list. + Sets an item at a specified position in a list. - This node takes a list as input and returns its length as an integer. + This node takes a list, an index, and a value, then returns a new list with + the item at the specified index replaced by the value. """ @classmethod def INPUT_TYPES(cls): return { "required": { "list": (IO.ANY,), + "index": (IO.INT, {"default": 0}), + "value": (IO.ANY,), } } - RETURN_TYPES = (IO.INT,) - RETURN_NAMES = ("length",) + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("list",) CATEGORY = "Basic/data list" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "length" + FUNCTION = "set_item" INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) - def length(self, **kwargs: list[Any]) -> tuple[int]: - return (len(kwargs.get('list', [])),) + def set_item(self, **kwargs: list[Any]) -> tuple[Any]: + input_list = kwargs.get('list', []) + index = kwargs.get('index', [0])[0] + value = kwargs.get('value', [None])[0] + try: + result = input_list.copy() + result[index] = value + return (result,) + except IndexError: + raise IndexError(f"Index {index} out of range for list of length {len(input_list)}") class DataListSlice(ComfyNodeABC): @@ -362,52 +558,21 @@ class DataListSlice(ComfyNodeABC): return (input_list[start:stop:step],) -class DataListGetItem(ComfyNodeABC): +class DataListSort(ComfyNodeABC): """ - Retrieves an item at a specified position in a list. + Sorts the items in a list. - This node takes a list and an index as inputs, then returns the item at the specified index. - Negative indices count from the end of the list. - Out of range indices return None. + This node takes a list as input and returns a new sorted list. + Options include sorting in reverse order and using a key function. """ @classmethod def INPUT_TYPES(cls): return { "required": { "list": (IO.ANY,), - "index": (IO.INT, {"default": 0}), - } - } - - RETURN_TYPES = (IO.ANY,) - RETURN_NAMES = ("item",) - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_item" - INPUT_IS_LIST = True - - def get_item(self, **kwargs: list[Any]) -> tuple[Any]: - index = kwargs.get('index', [0])[0] - try: - return (kwargs.get('list', [])[index],) - except IndexError: - return (None,) - - -class DataListSetItem(ComfyNodeABC): - """ - Sets an item at a specified position in a list. - - This node takes a list, an index, and a value, then returns a new list with - the item at the specified index replaced by the value. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": (IO.ANY,), - "index": (IO.INT, {"default": 0}), - "value": (IO.ANY,), + }, + "optional": { + "reverse": (["False", "True"], {"default": "False"}), } } @@ -415,71 +580,16 @@ class DataListSetItem(ComfyNodeABC): RETURN_NAMES = ("list",) CATEGORY = "Basic/data list" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "set_item" + FUNCTION = "sort" INPUT_IS_LIST = True OUTPUT_IS_LIST = (True,) - def set_item(self, **kwargs: list[Any]) -> tuple[Any]: - input_list = kwargs.get('list', []) - index = kwargs.get('index', [0])[0] - value = kwargs.get('value', [None])[0] - try: - result = input_list.copy() - result[index] = value - return (result,) - except IndexError: - raise IndexError(f"Index {index} out of range for list of length {len(input_list)}") + def sort(self, **kwargs: list[Any]) -> tuple[list[Any]]: + # Convert string to boolean + reverse = kwargs.get('reverse', ["False"])[0] == "True" - -class DataListContains(ComfyNodeABC): - """ - Checks if a list contains a specified value. - - This node takes a list and a value as inputs, then returns True if the value - is present in the list, and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": (IO.ANY,), - "value": (IO.ANY,), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("contains",) - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "contains" - INPUT_IS_LIST = True - - def contains(self, **kwargs: list[Any]) -> tuple[bool]: - value = kwargs.get('value', []) - if len(value) == 0: - return (False,) - return (value[0] in kwargs.get('list', []),) - - -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 ([],) + result = sorted(kwargs.get('list', []), reverse=reverse) + return (result,) class DataListZip(ComfyNodeABC): @@ -525,116 +635,6 @@ class DataListZip(ComfyNodeABC): return (result,) -class DataListFilter(ComfyNodeABC): - """ - Filters a data list using boolean values. - - This node takes a value data list and a filter data list (containing only boolean values). - It returns 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 - till the lengths are matching. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": (IO.ANY, {}), - "filter": (IO.BOOLEAN, {"forceInput": True}), - } - } - - RETURN_TYPES = (IO.ANY,) - RETURN_NAMES = ("filtered_list",) - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "filter_data" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - - def filter_data(self, **kwargs: list[Any]) -> tuple[list[Any]]: - values = kwargs.get('value', []) - filters = kwargs.get('filter', []) - - # Create a new list with only items where the filter is False - result = [_val for _val, _filter in zip(values, filters) if not _filter] - - return (result,) - - -class DataListMin(ComfyNodeABC): - """ - Finds the minimum value in a list of numbers. - - This node takes a list of numbers (either FLOAT or INT) and returns - the minimum value. Returns None if the list is empty or if it contains - non-numeric values. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": (IO.NUMBER, {}), - } - } - - RETURN_TYPES = (IO.NUMBER,) - RETURN_NAMES = ("min",) - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "find_min" - INPUT_IS_LIST = True - - def find_min(self, **kwargs: list[Any]) -> tuple[Any]: - values = kwargs.get('list', []) - if not values: - return (None,) - - try: - # Return the same type as found in the minimum value - return (min(values),) - except (TypeError, ValueError): - # Handle case where list contains non-comparable elements - return (None,) - - -class DataListMax(ComfyNodeABC): - """ - Finds the maximum value in a list of numbers. - - This node takes a list of numbers (either FLOAT or INT) and returns - the maximum value. Returns None if the list is empty or if it contains - non-numeric values. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": (IO.NUMBER, {}), - } - } - - RETURN_TYPES = (IO.NUMBER,) - RETURN_NAMES = ("max",) - CATEGORY = "Basic/data list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "find_max" - INPUT_IS_LIST = True - - def find_max(self, **kwargs: list[Any]) -> tuple[Any]: - values = kwargs.get('list', []) - if not values: - return (None,) - - try: - # Return the same type as found in the maximum value - return (max(values),) - except (TypeError, ValueError): - # Handle case where list contains non-comparable elements - return (None,) - - class DataListToList(ComfyNodeABC): """ Converts a ComfyUI data list into a LIST object. @@ -687,48 +687,48 @@ class DataListToSet(ComfyNodeABC): NODE_CLASS_MAPPINGS = { "Basic data handling: DataListAppend": DataListAppend, - "Basic data handling: DataListExtend": DataListExtend, - "Basic data handling: DataListInsert": DataListInsert, - "Basic data handling: DataListRemove": DataListRemove, - "Basic data handling: DataListPop": DataListPop, - "Basic data handling: DataListIndex": DataListIndex, - "Basic data handling: DataListCount": DataListCount, - "Basic data handling: DataListSort": DataListSort, - "Basic data handling: DataListReverse": DataListReverse, - "Basic data handling: DataListLength": DataListLength, - "Basic data handling: DataListSlice": DataListSlice, - "Basic data handling: DataListGetItem": DataListGetItem, - "Basic data handling: DataListSetItem": DataListSetItem, "Basic data handling: DataListContains": DataListContains, + "Basic data handling: DataListCount": DataListCount, "Basic data handling: DataListCreateEmpty": DataListCreateEmpty, - "Basic data handling: DataListZip": DataListZip, + "Basic data handling: DataListExtend": DataListExtend, "Basic data handling: DataListFilter": DataListFilter, - "Basic data handling: DataListMin": DataListMin, + "Basic data handling: DataListGetItem": DataListGetItem, + "Basic data handling: DataListIndex": DataListIndex, + "Basic data handling: DataListInsert": DataListInsert, + "Basic data handling: DataListLength": DataListLength, "Basic data handling: DataListMax": DataListMax, + "Basic data handling: DataListMin": DataListMin, + "Basic data handling: DataListPop": DataListPop, + "Basic data handling: DataListRemove": DataListRemove, + "Basic data handling: DataListReverse": DataListReverse, + "Basic data handling: DataListSetItem": DataListSetItem, + "Basic data handling: DataListSlice": DataListSlice, + "Basic data handling: DataListSort": DataListSort, + "Basic data handling: DataListZip": DataListZip, "Basic data handling: DataListToList": DataListToList, "Basic data handling: DataListToSet": DataListToSet, } NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: DataListAppend": "append", - "Basic data handling: DataListExtend": "extend", - "Basic data handling: DataListInsert": "insert", - "Basic data handling: DataListRemove": "remove", - "Basic data handling: DataListPop": "pop", - "Basic data handling: DataListIndex": "index", - "Basic data handling: DataListCount": "count", - "Basic data handling: DataListSort": "sort", - "Basic data handling: DataListReverse": "reverse", - "Basic data handling: DataListLength": "length", - "Basic data handling: DataListSlice": "slice", - "Basic data handling: DataListGetItem": "get item", - "Basic data handling: DataListSetItem": "set item", "Basic data handling: DataListContains": "contains", + "Basic data handling: DataListCount": "count", "Basic data handling: DataListCreateEmpty": "create empty", - "Basic data handling: DataListZip": "zip", + "Basic data handling: DataListExtend": "extend", "Basic data handling: DataListFilter": "filter", - "Basic data handling: DataListMin": "min", + "Basic data handling: DataListGetItem": "get item", + "Basic data handling: DataListIndex": "index", + "Basic data handling: DataListInsert": "insert", + "Basic data handling: DataListLength": "length", "Basic data handling: DataListMax": "max", + "Basic data handling: DataListMin": "min", + "Basic data handling: DataListPop": "pop", + "Basic data handling: DataListRemove": "remove", + "Basic data handling: DataListReverse": "reverse", + "Basic data handling: DataListSetItem": "set item", + "Basic data handling: DataListSlice": "slice", + "Basic data handling: DataListSort": "sort", + "Basic data handling: DataListZip": "zip", "Basic data handling: DataListToList": "convert to LIST", "Basic data handling: DataListToSet": "convert to SET", } diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index baf1f2e..f9c22f0 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -21,38 +21,6 @@ class DictCreate(ComfyNodeABC): return ({},) -class DictCreateFromItemsList(ComfyNodeABC): - """ - Creates a dictionary from a list of key-value pairs. - - This node takes a list of key-value pairs (tuples) and builds a dictionary from them. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "items": ("LIST", {}), - } - } - - RETURN_TYPES = ("DICT",) - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "create_from_items" - - def create_from_items(self, items: list) -> tuple[dict]: - try: - # Check if items are valid (key-value pairs) - for item in items: - print(f'item: "{item}", len(item): {len(item)}, items: "{items}", len(items): {len(items)}') - if not isinstance(item, tuple) and len(item) != 2: - raise ValueError("Each item must be a (key, value) pair") - - return (dict(items),) - except Exception as e: - raise ValueError(f"Error creating dictionary from items: {str(e)}") - - class DictCreateFromItemsDataList(ComfyNodeABC): """ Creates a dictionary from a list of key-value pairs. @@ -87,6 +55,211 @@ class DictCreateFromItemsDataList(ComfyNodeABC): raise ValueError(f"Error creating dictionary from items: {str(e)}") +class DictCreateFromItemsList(ComfyNodeABC): + """ + Creates a dictionary from a list of key-value pairs. + + This node takes a list of key-value pairs (tuples) and builds a dictionary from them. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "items": ("LIST", {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_from_items" + + def create_from_items(self, items: list) -> tuple[dict]: + try: + # Check if items are valid (key-value pairs) + for item in items: + print(f'item: "{item}", len(item): {len(item)}, items: "{items}", len(items): {len(items)}') + if not isinstance(item, tuple) and len(item) != 2: + raise ValueError("Each item must be a (key, value) pair") + + return (dict(items),) + except Exception as e: + raise ValueError(f"Error creating dictionary from items: {str(e)}") + + +class DictCreateFromLists(ComfyNodeABC): + """ + Creates a dictionary from separate lists of keys and values. + + This node takes a list of keys and a list of values, then creates a dictionary + by pairing corresponding elements from each list. If the lists are of different + lengths, only pairs up to the length of the shorter list are used. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "keys": ("LIST", {}), + "values": ("LIST", {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_from_lists" + + def create_from_lists(self, keys: list, values: list) -> tuple[dict]: + # Pair keys with values up to the length of the shorter list + result = dict(zip(keys, values)) + return (result,) + + +class DictCompare(ComfyNodeABC): + """ + Compares two dictionaries and reports differences. + + This node takes two dictionaries and compares them, returning information about + their equality, any keys that exist in only one dictionary, and any keys with + different values. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "dict1": ("DICT", {}), + "dict2": ("DICT", {}), + } + } + + RETURN_TYPES = (IO.BOOLEAN, "LIST", "LIST", "LIST") + RETURN_NAMES = ("are_equal", "only_in_dict1", "only_in_dict2", "different_values") + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "compare" + + def compare(self, dict1: dict, dict2: dict) -> tuple[bool, list, list, list]: + are_equal = (dict1 == dict2) + + keys1 = set(dict1.keys()) + keys2 = set(dict2.keys()) + + only_in_dict1 = list(keys1 - keys2) + only_in_dict2 = list(keys2 - keys1) + + different_values = [] + for key in keys1 & keys2: + if dict1[key] != dict2[key]: + different_values.append(key) + + return are_equal, only_in_dict1, only_in_dict2, different_values + + +class DictContainsKey(ComfyNodeABC): + """ + Checks if a key exists in a dictionary. + + This node takes a dictionary and a key as inputs, then returns True if the key + exists in the dictionary, and False otherwise. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + "key": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "contains_key" + + def contains_key(self, input_dict: dict, key: str) -> tuple[bool]: + return (key in input_dict,) + + +class DictExcludeKeys(ComfyNodeABC): + """ + Creates a new dictionary excluding the specified keys. + + This node takes a dictionary and a list of keys, then returns a new dictionary + containing all key-value pairs except those with keys in the provided list. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + "keys_to_exclude": ("LIST", {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "exclude_keys" + + def exclude_keys(self, input_dict: dict, keys_to_exclude: list) -> tuple[dict]: + result = {k: v for k, v in input_dict.items() if k not in keys_to_exclude} + return (result,) + + +class DictFilterByKeys(ComfyNodeABC): + """ + Creates a new dictionary with only the specified keys. + + This node takes a dictionary and a list of keys, then returns a new dictionary + containing only the key-value pairs for the keys that exist in the original dictionary. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + "keys": ("LIST", {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "filter_by_keys" + + def filter_by_keys(self, input_dict: dict, keys: list) -> tuple[dict]: + result = {k: input_dict[k] for k in keys if k in input_dict} + return (result,) + + +class DictFromKeys(ComfyNodeABC): + """ + Creates a dictionary from a list of keys and a default value. + + This node takes a list of keys and an optional value, then creates a new + dictionary where each key is associated with the value. If no value is + provided, None is used. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "keys": ("LIST", {}), + }, + "optional": { + "value": (IO.ANY, {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "from_keys" + + def from_keys(self, keys: list, value=None) -> tuple[dict]: + return (dict.fromkeys(keys, value),) + + class DictGet(ComfyNodeABC): """ Retrieves a value from a dictionary using the specified key. @@ -117,62 +290,71 @@ class DictGet(ComfyNodeABC): return (input_dict.get(key, default),) -class DictSet(ComfyNodeABC): +class DictGetKeysValues(ComfyNodeABC): """ - Adds or updates a key-value pair in a dictionary. + Returns keys and values as separate lists. - This node takes a dictionary, key, and value as inputs, then returns - a modified dictionary with the new key-value pair. + This node takes a dictionary and returns two lists: one containing + all keys and another containing all corresponding values. """ @classmethod def INPUT_TYPES(cls): return { "required": { "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), - "value": (IO.ANY, {}), } } - RETURN_TYPES = ("DICT",) + RETURN_TYPES = ("LIST", "LIST") + RETURN_NAMES = ("keys", "values") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "set" + FUNCTION = "get_keys_values" - def set(self, input_dict: dict, key: str, value: Any) -> tuple[dict]: - result = input_dict.copy() - result[key] = value - return (result,) + def get_keys_values(self, input_dict: dict) -> tuple[list, list]: + keys = list(input_dict.keys()) + values = list(input_dict.values()) + return keys, values -class DictKeys(ComfyNodeABC): +class DictGetMultiple(ComfyNodeABC): """ - Returns all keys in a dictionary. + Retrieves multiple values from a dictionary using a list of keys. - This node takes a dictionary and returns a list containing all of its keys. + This node takes a dictionary and a list of keys, then returns a list + containing the corresponding values. If a key is not found, the default + value is used for that position. """ @classmethod def INPUT_TYPES(cls): return { "required": { "input_dict": ("DICT", {}), + "keys": ("LIST", {}), + }, + "optional": { + "default": (IO.ANY, {}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("values",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "keys" + FUNCTION = "get_multiple" - def keys(self, input_dict: dict) -> tuple[list]: - return (list(input_dict.keys()),) + def get_multiple(self, input_dict: dict, keys: list, default=None) -> tuple[list]: + values = [input_dict.get(key, default) for key in keys] + return (values,) -class DictValues(ComfyNodeABC): +class DictInvert(ComfyNodeABC): """ - Returns all values in a dictionary. + Creates a new dictionary with keys and values swapped. - This node takes a dictionary and returns a list containing all of its values. + This node takes a dictionary as input and returns a new dictionary where + the keys become values and values become keys. Note that values must be + hashable to be used as keys in the new dictionary. """ @classmethod def INPUT_TYPES(cls): @@ -182,13 +364,19 @@ class DictValues(ComfyNodeABC): } } - RETURN_TYPES = ("LIST",) + RETURN_TYPES = ("DICT", IO.BOOLEAN) + RETURN_NAMES = ("inverted_dict", "success") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "values" + FUNCTION = "invert" - def values(self, input_dict: dict) -> tuple[list]: - return (list(input_dict.values()),) + def invert(self, input_dict: dict) -> tuple[dict, bool]: + try: + inverted = {v: k for k, v in input_dict.items()} + return inverted, True + except Exception: + # Return original dictionary if inversion fails (e.g., unhashable values) + return input_dict, False class DictItems(ComfyNodeABC): @@ -215,57 +403,91 @@ class DictItems(ComfyNodeABC): return (list(input_dict.items()),) -class DictContainsKey(ComfyNodeABC): +class DictKeys(ComfyNodeABC): """ - Checks if a key exists in a dictionary. + Returns all keys in a dictionary. - This node takes a dictionary and a key as inputs, then returns True if the key - exists in the dictionary, and False otherwise. + This node takes a dictionary and returns a list containing all of its keys. """ @classmethod def INPUT_TYPES(cls): return { "required": { "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), } } - RETURN_TYPES = (IO.BOOLEAN,) + RETURN_TYPES = ("LIST",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "contains_key" + FUNCTION = "keys" - def contains_key(self, input_dict: dict, key: str) -> tuple[bool]: - return (key in input_dict,) + def keys(self, input_dict: dict) -> tuple[list]: + return (list(input_dict.keys()),) -class DictFromKeys(ComfyNodeABC): +class DictLength(ComfyNodeABC): """ - Creates a dictionary from a list of keys and a default value. + Returns the number of key-value pairs in a dictionary. - This node takes a list of keys and an optional value, then creates a new - dictionary where each key is associated with the value. If no value is - provided, None is used. + This node takes a dictionary as input and returns its length (number of items). """ @classmethod def INPUT_TYPES(cls): return { "required": { - "keys": ("LIST", {}), + "input_dict": ("DICT", {}), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("length",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "length" + + def length(self, input_dict: dict) -> tuple[int]: + return (len(input_dict),) + + +class DictMerge(ComfyNodeABC): + """ + Merges multiple dictionaries into a single dictionary. + + This node takes multiple dictionaries as input and combines them into a single + dictionary. If there are duplicate keys, values from later dictionaries take precedence. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "dict1": ("DICT", {}), }, "optional": { - "value": (IO.ANY, {}), + "dict2": ("DICT", {}), + "dict3": ("DICT", {}), + "dict4": ("DICT", {}), } } RETURN_TYPES = ("DICT",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "from_keys" + FUNCTION = "merge" - def from_keys(self, keys: list, value=None) -> tuple[dict]: - return (dict.fromkeys(keys, value),) + def merge(self, dict1: dict, dict2=None, dict3=None, dict4=None) -> tuple[dict]: + result = dict1.copy() + + if dict2 is not None: + result.update(dict2) + + if dict3 is not None: + result.update(dict3) + + if dict4 is not None: + result.update(dict4) + + return (result,) class DictPop(ComfyNodeABC): @@ -342,6 +564,65 @@ class DictPopItem(ComfyNodeABC): return result, "", None, False +class DictRemove(ComfyNodeABC): + """ + Removes a key-value pair from a dictionary. + + This node takes a dictionary and a key as inputs, then returns a new + dictionary with the specified key removed. If the key doesn't exist, + the dictionary remains unchanged. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + "key": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = ("DICT", IO.BOOLEAN) + RETURN_NAMES = ("dict", "key_removed") + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "remove" + + def remove(self, input_dict: dict, key: str) -> tuple[dict, bool]: + result = input_dict.copy() + if key in result: + del result[key] + return result, True + return result, False + + +class DictSet(ComfyNodeABC): + """ + Adds or updates a key-value pair in a dictionary. + + This node takes a dictionary, key, and value as inputs, then returns + a modified dictionary with the new key-value pair. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + "key": (IO.STRING, {"default": ""}), + "value": (IO.ANY, {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "set" + + def set(self, input_dict: dict, key: str, value: Any) -> tuple[dict]: + result = input_dict.copy() + result[key] = value + return (result,) + + class DictSetDefault(ComfyNodeABC): """ Returns the value for a key, setting a default if the key doesn't exist. @@ -400,11 +681,11 @@ class DictUpdate(ComfyNodeABC): return (result,) -class DictLength(ComfyNodeABC): +class DictValues(ComfyNodeABC): """ - Returns the number of key-value pairs in a dictionary. + Returns all values in a dictionary. - This node takes a dictionary as input and returns its length (number of items). + This node takes a dictionary and returns a list containing all of its values. """ @classmethod def INPUT_TYPES(cls): @@ -414,346 +695,65 @@ class DictLength(ComfyNodeABC): } } - RETURN_TYPES = ("INT",) - RETURN_NAMES = ("length",) - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "length" - - def length(self, input_dict: dict) -> tuple[int]: - return (len(input_dict),) - - -class DictMerge(ComfyNodeABC): - """ - Merges multiple dictionaries into a single dictionary. - - This node takes multiple dictionaries as input and combines them into a single - dictionary. If there are duplicate keys, values from later dictionaries take precedence. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "dict1": ("DICT", {}), - }, - "optional": { - "dict2": ("DICT", {}), - "dict3": ("DICT", {}), - "dict4": ("DICT", {}), - } - } - - RETURN_TYPES = ("DICT",) - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "merge" - - def merge(self, dict1: dict, dict2=None, dict3=None, dict4=None) -> tuple[dict]: - result = dict1.copy() - - if dict2 is not None: - result.update(dict2) - - if dict3 is not None: - result.update(dict3) - - if dict4 is not None: - result.update(dict4) - - return (result,) - - -class DictGetKeysValues(ComfyNodeABC): - """ - Returns keys and values as separate lists. - - This node takes a dictionary and returns two lists: one containing - all keys and another containing all corresponding values. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input_dict": ("DICT", {}), - } - } - - RETURN_TYPES = ("LIST", "LIST") - RETURN_NAMES = ("keys", "values") - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_keys_values" - - def get_keys_values(self, input_dict: dict) -> tuple[list, list]: - keys = list(input_dict.keys()) - values = list(input_dict.values()) - return keys, values - - -class DictRemove(ComfyNodeABC): - """ - Removes a key-value pair from a dictionary. - - This node takes a dictionary and a key as inputs, then returns a new - dictionary with the specified key removed. If the key doesn't exist, - the dictionary remains unchanged. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = ("DICT", IO.BOOLEAN) - RETURN_NAMES = ("dict", "key_removed") - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "remove" - - def remove(self, input_dict: dict, key: str) -> tuple[dict, bool]: - result = input_dict.copy() - if key in result: - del result[key] - return result, True - return result, False - - -class DictFilterByKeys(ComfyNodeABC): - """ - Creates a new dictionary with only the specified keys. - - This node takes a dictionary and a list of keys, then returns a new dictionary - containing only the key-value pairs for the keys that exist in the original dictionary. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input_dict": ("DICT", {}), - "keys": ("LIST", {}), - } - } - - RETURN_TYPES = ("DICT",) - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "filter_by_keys" - - def filter_by_keys(self, input_dict: dict, keys: list) -> tuple[dict]: - result = {k: input_dict[k] for k in keys if k in input_dict} - return (result,) - - -class DictExcludeKeys(ComfyNodeABC): - """ - Creates a new dictionary excluding the specified keys. - - This node takes a dictionary and a list of keys, then returns a new dictionary - containing all key-value pairs except those with keys in the provided list. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input_dict": ("DICT", {}), - "keys_to_exclude": ("LIST", {}), - } - } - - RETURN_TYPES = ("DICT",) - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "exclude_keys" - - def exclude_keys(self, input_dict: dict, keys_to_exclude: list) -> tuple[dict]: - result = {k: v for k, v in input_dict.items() if k not in keys_to_exclude} - return (result,) - - -class DictGetMultiple(ComfyNodeABC): - """ - Retrieves multiple values from a dictionary using a list of keys. - - This node takes a dictionary and a list of keys, then returns a list - containing the corresponding values. If a key is not found, the default - value is used for that position. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input_dict": ("DICT", {}), - "keys": ("LIST", {}), - }, - "optional": { - "default": (IO.ANY, {}), - } - } - RETURN_TYPES = ("LIST",) - RETURN_NAMES = ("values",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_multiple" + FUNCTION = "values" - def get_multiple(self, input_dict: dict, keys: list, default=None) -> tuple[list]: - values = [input_dict.get(key, default) for key in keys] - return (values,) - - -class DictInvert(ComfyNodeABC): - """ - Creates a new dictionary with keys and values swapped. - - This node takes a dictionary as input and returns a new dictionary where - the keys become values and values become keys. Note that values must be - hashable to be used as keys in the new dictionary. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "input_dict": ("DICT", {}), - } - } - - RETURN_TYPES = ("DICT", IO.BOOLEAN) - RETURN_NAMES = ("inverted_dict", "success") - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "invert" - - def invert(self, input_dict: dict) -> tuple[dict, bool]: - try: - inverted = {v: k for k, v in input_dict.items()} - return inverted, True - except Exception: - # Return original dictionary if inversion fails (e.g., unhashable values) - return input_dict, False - - -class DictCreateFromLists(ComfyNodeABC): - """ - Creates a dictionary from separate lists of keys and values. - - This node takes a list of keys and a list of values, then creates a dictionary - by pairing corresponding elements from each list. If the lists are of different - lengths, only pairs up to the length of the shorter list are used. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "keys": ("LIST", {}), - "values": ("LIST", {}), - } - } - - RETURN_TYPES = ("DICT",) - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "create_from_lists" - - def create_from_lists(self, keys: list, values: list) -> tuple[dict]: - # Pair keys with values up to the length of the shorter list - result = dict(zip(keys, values)) - return (result,) - - -class DictCompare(ComfyNodeABC): - """ - Compares two dictionaries and reports differences. - - This node takes two dictionaries and compares them, returning information about - their equality, any keys that exist in only one dictionary, and any keys with - different values. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "dict1": ("DICT", {}), - "dict2": ("DICT", {}), - } - } - - RETURN_TYPES = (IO.BOOLEAN, "LIST", "LIST", "LIST") - RETURN_NAMES = ("are_equal", "only_in_dict1", "only_in_dict2", "different_values") - CATEGORY = "Basic/DICT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "compare" - - def compare(self, dict1: dict, dict2: dict) -> tuple[bool, list, list, list]: - are_equal = (dict1 == dict2) - - keys1 = set(dict1.keys()) - keys2 = set(dict2.keys()) - - only_in_dict1 = list(keys1 - keys2) - only_in_dict2 = list(keys2 - keys1) - - different_values = [] - for key in keys1 & keys2: - if dict1[key] != dict2[key]: - different_values.append(key) - - return are_equal, only_in_dict1, only_in_dict2, different_values + def values(self, input_dict: dict) -> tuple[list]: + return (list(input_dict.values()),) NODE_CLASS_MAPPINGS = { "Basic data handling: DictCreate": DictCreate, - "Basic data handling: DictCreateFromItemsList": DictCreateFromItemsList, "Basic data handling: DictCreateFromItemsDataList": DictCreateFromItemsDataList, - "Basic data handling: DictGet": DictGet, - "Basic data handling: DictSet": DictSet, - "Basic data handling: DictKeys": DictKeys, - "Basic data handling: DictValues": DictValues, - "Basic data handling: DictItems": DictItems, - "Basic data handling: DictContainsKey": DictContainsKey, - "Basic data handling: DictFromKeys": DictFromKeys, - "Basic data handling: DictPop": DictPop, - "Basic data handling: DictPopItem": DictPopItem, - "Basic data handling: DictSetDefault": DictSetDefault, - "Basic data handling: DictUpdate": DictUpdate, - "Basic data handling: DictLength": DictLength, - "Basic data handling: DictMerge": DictMerge, - "Basic data handling: DictGetKeysValues": DictGetKeysValues, - "Basic data handling: DictRemove": DictRemove, - "Basic data handling: DictFilterByKeys": DictFilterByKeys, - "Basic data handling: DictExcludeKeys": DictExcludeKeys, - "Basic data handling: DictGetMultiple": DictGetMultiple, - "Basic data handling: DictInvert": DictInvert, + "Basic data handling: DictCreateFromItemsList": DictCreateFromItemsList, "Basic data handling: DictCreateFromLists": DictCreateFromLists, "Basic data handling: DictCompare": DictCompare, + "Basic data handling: DictContainsKey": DictContainsKey, + "Basic data handling: DictExcludeKeys": DictExcludeKeys, + "Basic data handling: DictFilterByKeys": DictFilterByKeys, + "Basic data handling: DictFromKeys": DictFromKeys, + "Basic data handling: DictGet": DictGet, + "Basic data handling: DictGetKeysValues": DictGetKeysValues, + "Basic data handling: DictGetMultiple": DictGetMultiple, + "Basic data handling: DictInvert": DictInvert, + "Basic data handling: DictItems": DictItems, + "Basic data handling: DictKeys": DictKeys, + "Basic data handling: DictLength": DictLength, + "Basic data handling: DictMerge": DictMerge, + "Basic data handling: DictPop": DictPop, + "Basic data handling: DictPopItem": DictPopItem, + "Basic data handling: DictRemove": DictRemove, + "Basic data handling: DictSet": DictSet, + "Basic data handling: DictSetDefault": DictSetDefault, + "Basic data handling: DictUpdate": DictUpdate, + "Basic data handling: DictValues": DictValues, } NODE_DISPLAY_NAME_MAPPINGS = { - "Basic data handling: DictCreate": "create", - "Basic data handling: DictCreateFromItemsList": "from items (LIST)", - "Basic data handling: DictCreateFromItemsDataList": "from items (data list)", - "Basic data handling: DictGet": "get", - "Basic data handling: DictSet": "set", - "Basic data handling: DictKeys": "keys", - "Basic data handling: DictValues": "values", - "Basic data handling: DictItems": "items", - "Basic data handling: DictContainsKey": "contains_key", + "Basic data handling: DictCreate": "create empty", + "Basic data handling: DictCreateFromItemsDataList": "create from items (data list)", + "Basic data handling: DictCreateFromItemsList": "create from items (LIST)", + "Basic data handling: DictCreateFromLists": "create from LISTs", + "Basic data handling: DictCompare": "compare", + "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: DictPop": "pop", - "Basic data handling: DictPopItem": "popitem", - "Basic data handling: DictSetDefault": "setdefault", - "Basic data handling: DictUpdate": "update", + "Basic data handling: DictGet": "get", + "Basic data handling: DictGetKeysValues": "get keys values", + "Basic data handling: DictGetMultiple": "get multiple", + "Basic data handling: DictInvert": "invert", + "Basic data handling: DictItems": "items", + "Basic data handling: DictKeys": "keys", "Basic data handling: DictLength": "length", "Basic data handling: DictMerge": "merge", - "Basic data handling: DictGetKeysValues": "get_keys_values", + "Basic data handling: DictPop": "pop", + "Basic data handling: DictPopItem": "popitem", "Basic data handling: DictRemove": "remove", - "Basic data handling: DictFilterByKeys": "filter_by_keys", - "Basic data handling: DictExcludeKeys": "exclude_keys", - "Basic data handling: DictGetMultiple": "get_multiple", - "Basic data handling: DictInvert": "invert", - "Basic data handling: DictCreateFromLists": "from_lists", - "Basic data handling: DictCompare": "compare", + "Basic data handling: DictSet": "set", + "Basic data handling: DictSetDefault": "setdefault", + "Basic data handling: DictUpdate": "update", + "Basic data handling: DictValues": "values", } diff --git a/src/basic_data_handling/float_nodes.py b/src/basic_data_handling/float_nodes.py index eb7060c..b075b0c 100644 --- a/src/basic_data_handling/float_nodes.py +++ b/src/basic_data_handling/float_nodes.py @@ -128,6 +128,103 @@ class FloatDivideSafe(ComfyNodeABC): return (float1 / float2,) +class FloatAsIntegerRatio(ComfyNodeABC): + """ + Returns the integer ratio of a floating-point number. + + This node takes a floating-point number and returns two integers, + which represent the ratio as numerator and denominator. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "float_value": (IO.FLOAT, {"default": 0.0}), + } + } + + RETURN_TYPES = (IO.INT, IO.INT) + RETURN_NAMES = ("numerator", "denominator") + CATEGORY = "Basic/FLOAT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "as_integer_ratio" + + def as_integer_ratio(self, float_value: float) -> tuple[int, int]: + # Decompose the float into numerator and denominator + numerator, denominator = float_value.as_integer_ratio() + return numerator, denominator + + +class FloatFromHex(ComfyNodeABC): + """ + Converts a hexadecimal string to its corresponding floating-point number. + + This node takes a hexadecimal float string as input and returns the float. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "hex_value": (IO.STRING, {"default": "0x0.0p+0"}), + } + } + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/FLOAT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "from_hex" + + def from_hex(self, hex_value: str) -> tuple[float]: + return (float.fromhex(hex_value),) + + +class FloatHex(ComfyNodeABC): + """ + Converts a floating-point number to its hexadecimal representation. + + This node takes a float as input and returns its hexadecimal string representation. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "float_value": (IO.FLOAT, {"default": 0.0}), + } + } + + RETURN_TYPES = (IO.STRING,) + CATEGORY = "Basic/FLOAT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "to_hex" + + def to_hex(self, float_value: float) -> tuple[str]: + return (float_value.hex(),) + + +class FloatIsInteger(ComfyNodeABC): + """ + Checks if a floating-point number is an integer. + + This node takes a floating-point number as input and returns True if the number + is an integer, otherwise False. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "float_value": (IO.FLOAT, {"default": 0.0}), + } + } + + RETURN_TYPES = ("BOOLEAN",) + CATEGORY = "Basic/FLOAT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "is_integer" + + def is_integer(self, float_value: float) -> tuple[bool]: + return (float_value.is_integer(),) + + class FloatPower(ComfyNodeABC): """ Raises one floating-point number to the power of another. @@ -178,115 +275,18 @@ class FloatRound(ComfyNodeABC): return (round(float_value, decimal_places),) -class FloatIsInteger(ComfyNodeABC): - """ - Checks if a floating-point number is an integer. - - This node takes a floating-point number as input and returns True if the number - is an integer, otherwise False. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "float_value": (IO.FLOAT, {"default": 0.0}), - } - } - - RETURN_TYPES = ("BOOLEAN",) - CATEGORY = "Basic/FLOAT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "is_integer" - - def is_integer(self, float_value: float) -> tuple[bool]: - return (float_value.is_integer(),) - - -class FloatAsIntegerRatio(ComfyNodeABC): - """ - Returns the integer ratio of a floating-point number. - - This node takes a floating-point number and returns two integers, - which represent the ratio as numerator and denominator. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "float_value": (IO.FLOAT, {"default": 0.0}), - } - } - - RETURN_TYPES = (IO.INT, IO.INT) - RETURN_NAMES = ("numerator", "denominator") - CATEGORY = "Basic/FLOAT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "as_integer_ratio" - - def as_integer_ratio(self, float_value: float) -> tuple[int, int]: - # Decompose the float into numerator and denominator - numerator, denominator = float_value.as_integer_ratio() - return numerator, denominator - - -class FloatHex(ComfyNodeABC): - """ - Converts a floating-point number to its hexadecimal representation. - - This node takes a float as input and returns its hexadecimal string representation. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "float_value": (IO.FLOAT, {"default": 0.0}), - } - } - - RETURN_TYPES = (IO.STRING,) - CATEGORY = "Basic/FLOAT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "to_hex" - - def to_hex(self, float_value: float) -> tuple[str]: - return (float_value.hex(),) - - -class FloatFromHex(ComfyNodeABC): - """ - Converts a hexadecimal string to its corresponding floating-point number. - - This node takes a hexadecimal float string as input and returns the float. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "hex_value": (IO.STRING, {"default": "0x0.0p+0"}), - } - } - - RETURN_TYPES = (IO.FLOAT,) - CATEGORY = "Basic/FLOAT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "from_hex" - - def from_hex(self, hex_value: str) -> tuple[float]: - return (float.fromhex(hex_value),) - - NODE_CLASS_MAPPINGS = { "Basic data handling: FloatAdd": FloatAdd, "Basic data handling: FloatSubtract": FloatSubtract, "Basic data handling: FloatMultiply": FloatMultiply, "Basic data handling: FloatDivide": FloatDivide, "Basic data handling: FloatDivideSafe": FloatDivideSafe, + "Basic data handling: FloatAsIntegerRatio": FloatAsIntegerRatio, + "Basic data handling: FloatFromHex": FloatFromHex, + "Basic data handling: FloatHex": FloatHex, + "Basic data handling: FloatIsInteger": FloatIsInteger, "Basic data handling: FloatPower": FloatPower, "Basic data handling: FloatRound": FloatRound, - "Basic data handling: FloatIsInteger": FloatIsInteger, - "Basic data handling: FloatAsIntegerRatio": FloatAsIntegerRatio, - "Basic data handling: FloatHex": FloatHex, - "Basic data handling: FloatFromHex": FloatFromHex, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -295,10 +295,10 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: FloatMultiply": "multiply", "Basic data handling: FloatDivide": "divide", "Basic data handling: FloatDivideSafe": "divide (division by zero safe)", + "Basic data handling: FloatAsIntegerRatio": "integer ratio", + "Basic data handling: FloatFromHex": "from hex", + "Basic data handling: FloatHex": "to hex", + "Basic data handling: FloatIsInteger": "is integer", "Basic data handling: FloatPower": "power", "Basic data handling: FloatRound": "round", - "Basic data handling: FloatIsInteger": "is_integer", - "Basic data handling: FloatAsIntegerRatio": "as_integer_ratio", - "Basic data handling: FloatHex": "to_hex", - "Basic data handling: FloatFromHex": "from_hex", } diff --git a/src/basic_data_handling/int_nodes.py b/src/basic_data_handling/int_nodes.py index 669e136..d044d7b 100644 --- a/src/basic_data_handling/int_nodes.py +++ b/src/basic_data_handling/int_nodes.py @@ -130,6 +130,81 @@ class IntDivideSafe(ComfyNodeABC): return (int1 // int2,) +class IntBitCount(ComfyNodeABC): + """ + Returns the number of 1 bits in the binary representation of an integer. + + This node takes an integer as input and returns the count of set bits. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "int_value": (IO.INT, {"default": 0}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/INT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "bit_count" + + def bit_count(self, int_value: int) -> tuple[int]: + # Introduced in Python 3.10 + return (int_value.bit_count(),) + + +class IntBitLength(ComfyNodeABC): + """ + Returns the number of bits required to represent an integer in binary. + + This node takes an integer as input and returns the number of bits needed + to represent it, excluding the sign and leading zeros. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "int_value": (IO.INT, {"default": 0}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/INT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "bit_length" + + def bit_length(self, int_value: int) -> tuple[int]: + return (int_value.bit_length(),) + + +class IntFromBytes(ComfyNodeABC): + """ + Converts a bytes object to an integer. + + This class method takes bytes, byte order, and signed flag as inputs and + returns an integer. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "bytes_value": ("BYTES", {}), + "byteorder": (["big", "little"], {"default": "big"}), + "signed": (["True", "False"], {"default": "False"}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/INT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "from_bytes" + + def from_bytes(self, bytes_value, byteorder: Literal["big", "little"], signed: Literal["True", "False"]) -> tuple[int]: + signed_bool = (signed == "True") + return (int.from_bytes(bytes_value, byteorder=byteorder, signed=signed_bool),) + + class IntModulus(ComfyNodeABC): """ Returns the modulus of two integers. @@ -182,30 +257,6 @@ class IntPower(ComfyNodeABC): return (base**exponent,) -class IntBitLength(ComfyNodeABC): - """ - Returns the number of bits required to represent an integer in binary. - - This node takes an integer as input and returns the number of bits needed - to represent it, excluding the sign and leading zeros. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "int_value": (IO.INT, {"default": 0}), - } - } - - RETURN_TYPES = (IO.INT,) - CATEGORY = "Basic/INT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "bit_length" - - def bit_length(self, int_value: int) -> tuple[int]: - return (int_value.bit_length(),) - - class IntToBytes(ComfyNodeABC): """ Converts an integer to its byte representation. @@ -234,69 +285,18 @@ class IntToBytes(ComfyNodeABC): return (int_value.to_bytes(length, byteorder=byteorder, signed=signed_bool),) -class IntFromBytes(ComfyNodeABC): - """ - Converts a bytes object to an integer. - - This class method takes bytes, byte order, and signed flag as inputs and - returns an integer. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "bytes_value": ("BYTES", {}), - "byteorder": (["big", "little"], {"default": "big"}), - "signed": (["True", "False"], {"default": "False"}), - } - } - - RETURN_TYPES = (IO.INT,) - CATEGORY = "Basic/INT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "from_bytes" - - def from_bytes(self, bytes_value, byteorder: Literal["big", "little"], signed: Literal["True", "False"]) -> tuple[int]: - signed_bool = (signed == "True") - return (int.from_bytes(bytes_value, byteorder=byteorder, signed=signed_bool),) - - -class IntBitCount(ComfyNodeABC): - """ - Returns the number of 1 bits in the binary representation of an integer. - - This node takes an integer as input and returns the count of set bits. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "int_value": (IO.INT, {"default": 0}), - } - } - - RETURN_TYPES = (IO.INT,) - CATEGORY = "Basic/INT" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "bit_count" - - def bit_count(self, int_value: int) -> tuple[int]: - # Introduced in Python 3.10 - return (int_value.bit_count(),) - - NODE_CLASS_MAPPINGS = { "Basic data handling: IntAdd": IntAdd, "Basic data handling: IntSubtract": IntSubtract, "Basic data handling: IntMultiply": IntMultiply, "Basic data handling: IntDivide": IntDivide, "Basic data handling: IntDivideSafe": IntDivideSafe, + "Basic data handling: IntBitCount": IntBitCount, + "Basic data handling: IntBitLength": IntBitLength, + "Basic data handling: IntFromBytes": IntFromBytes, "Basic data handling: IntModulus": IntModulus, "Basic data handling: IntPower": IntPower, - "Basic data handling: IntBitLength": IntBitLength, "Basic data handling: IntToBytes": IntToBytes, - "Basic data handling: IntFromBytes": IntFromBytes, - "Basic data handling: IntBitCount": IntBitCount, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -305,11 +305,11 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: IntMultiply": "multiply", "Basic data handling: IntDivide": "divide", "Basic data handling: IntDivideSafe": "divide (division by zero safe)", + "Basic data handling: IntBitCount": "bit count", + "Basic data handling: IntBitLength": "bit length", + "Basic data handling: IntFromBytes": "from bytes", "Basic data handling: IntModulus": "modulus", "Basic data handling: IntPower": "power", - "Basic data handling: IntBitLength": "bit_length", - "Basic data handling: IntToBytes": "to_bytes", - "Basic data handling: IntFromBytes": "from_bytes", - "Basic data handling: IntBitCount": "bit_count", + "Basic data handling: IntToBytes": "to bytes", } diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 0938a14..b5d1a7f 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -4,6 +4,25 @@ 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): + """ + Create an empty LIST. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": {} + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self) -> tuple[list[Any]]: + return ([],) + + class ListAppend(ComfyNodeABC): """ Adds an item to the end of a LIST. @@ -31,23 +50,56 @@ class ListAppend(ComfyNodeABC): return (result,) -class ListEmpty(ComfyNodeABC): +class ListContains(ComfyNodeABC): """ - Create an empty LIST. + Checks if a LIST contains a specified value. + + This node takes a LIST and a value as inputs, then returns True if the value + is present in the LIST, and False otherwise. """ @classmethod def INPUT_TYPES(cls): return { - "required": {} + "required": { + "list": ("LIST", {}), + "value": (IO.ANY, {}), + } } - RETURN_TYPES = ("LIST",) + RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("contains",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "create" + FUNCTION = "contains" - def create(self) -> tuple[list[Any]]: - return ([],) + def contains(self, list: list[Any], value: Any) -> tuple[bool]: + return (value in list,) + + +class ListCount(ComfyNodeABC): + """ + Counts the number of occurrences of a value in a LIST. + + This node takes a LIST and a value as inputs, then returns the number of times + the value appears in the LIST. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + "value": (IO.ANY, {}), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("count",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "count" + + def count(self, list: list[Any], value: Any) -> tuple[int]: + return (list.count(value),) class ListExtend(ComfyNodeABC): @@ -77,12 +129,13 @@ class ListExtend(ComfyNodeABC): return (result,) -class ListInsert(ComfyNodeABC): +class ListGetItem(ComfyNodeABC): """ - Inserts an item at a specified position in a LIST. + Retrieves an item at a specified position in a LIST. - This node takes a LIST, an index, and any item as inputs, then returns a new - LIST with the item inserted at the specified index. + This node takes a LIST and an index as inputs, then returns the item at the specified index. + Negative indices count from the end of the LIST. + Out of range indices return None. """ @classmethod def INPUT_TYPES(cls): @@ -90,86 +143,20 @@ class ListInsert(ComfyNodeABC): "required": { "list": ("LIST", {}), "index": ("INT", {"default": 0}), - "item": (IO.ANY, {}), } } - RETURN_TYPES = ("LIST",) + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("item",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "insert" + FUNCTION = "get_item" - def insert(self, list: list[Any], index: int, item: Any) -> tuple[list[Any]]: - result = list.copy() - result.insert(index, item) - return (result,) - - -class ListRemove(ComfyNodeABC): - """ - Removes the first occurrence of a specified value from a LIST. - - This node takes a LIST and a value as inputs, then returns a new LIST with - the first occurrence of the value removed and a success indicator. If the value - is not present, the original LIST is returned with success set to False. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - "value": (IO.ANY, {}), - } - } - - RETURN_TYPES = ("LIST", "BOOLEAN") - RETURN_NAMES = ("list", "success") - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "remove" - - def remove(self, list: list[Any], value: Any) -> tuple[list[Any], bool]: - result = list.copy() + def get_item(self, list: list[Any], index: int) -> tuple[Any]: try: - result.remove(value) - return result, True - except ValueError: - return result, False - - -class ListPop(ComfyNodeABC): - """ - Removes and returns an item at a specified position in a LIST. - - This node takes a LIST and an index as inputs, then returns both the new LIST - with the item removed and the removed item. If no index is specified, - removes and returns the last item. - When the LIST is empty, the item is None. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - }, - "optional": { - "index": ("INT", {"default": -1}), - } - } - - RETURN_TYPES = ("LIST", IO.ANY) - RETURN_NAMES = ("list", "item") - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "pop" - - def pop(self, list: list[Any], index: int = -1) -> tuple[list[Any], Any]: - result = list.copy() - try: - item = result.pop(index) - return result, item + return (list[index],) except IndexError: - return result, None + return (None,) class ListIndex(ComfyNodeABC): @@ -209,12 +196,164 @@ class ListIndex(ComfyNodeABC): return (-1,) -class ListCount(ComfyNodeABC): +class ListInsert(ComfyNodeABC): """ - Counts the number of occurrences of a value in a LIST. + Inserts an item at a specified position in a LIST. - This node takes a LIST and a value as inputs, then returns the number of times - the value appears in the LIST. + This node takes a LIST, an index, and any item as inputs, then returns a new + LIST with the item inserted at the specified index. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + "index": ("INT", {"default": 0}), + "item": (IO.ANY, {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "insert" + + def insert(self, list: list[Any], index: int, item: Any) -> tuple[list[Any]]: + result = list.copy() + result.insert(index, item) + return (result,) + + +class ListLength(ComfyNodeABC): + """ + Returns the number of items in a LIST. + + This node takes a LIST as input and returns its length as an integer. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("length",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "length" + + def length(self, list: list[Any]) -> tuple[int]: + return (len(list),) + + +class ListMax(ComfyNodeABC): + """ + Returns the maximum value in a LIST. + + This node takes a LIST of comparable items and returns the maximum value. + Returns None if the LIST is empty or if items are not comparable. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("max_value",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "find_max" + + def find_max(self, list: list[Any]) -> tuple[Any]: + if not list: + return (None,) + + try: + return (max(list),) + except (TypeError, ValueError): + # Handle case where list contains non-comparable elements + return (None,) + + +class ListMin(ComfyNodeABC): + """ + Returns the minimum value in a LIST. + + This node takes a LIST of comparable items and returns the minimum value. + Returns None if the LIST is empty or if items are not comparable. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("min_value",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "find_min" + + def find_min(self, list: list[Any]) -> tuple[Any]: + if not list: + return (None,) + + try: + return (min(list),) + except (TypeError, ValueError): + # Handle case where list contains non-comparable elements + return (None,) + + +class ListPop(ComfyNodeABC): + """ + Removes and returns an item at a specified position in a LIST. + + This node takes a LIST and an index as inputs, then returns both the new LIST + with the item removed and the removed item. If no index is specified, + removes and returns the last item. + When the LIST is empty, the item is None. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + }, + "optional": { + "index": ("INT", {"default": -1}), + } + } + + RETURN_TYPES = ("LIST", IO.ANY) + RETURN_NAMES = ("list", "item") + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop" + + def pop(self, list: list[Any], index: int = -1) -> tuple[list[Any], Any]: + result = list.copy() + try: + item = result.pop(index) + return result, item + except IndexError: + return result, None + + +class ListRemove(ComfyNodeABC): + """ + Removes the first occurrence of a specified value from a LIST. + + This node takes a LIST and a value as inputs, then returns a new LIST with + the first occurrence of the value removed and a success indicator. If the value + is not present, the original LIST is returned with success set to False. """ @classmethod def INPUT_TYPES(cls): @@ -225,14 +364,105 @@ class ListCount(ComfyNodeABC): } } - RETURN_TYPES = ("INT",) - RETURN_NAMES = ("count",) + RETURN_TYPES = ("LIST", "BOOLEAN") + RETURN_NAMES = ("list", "success") CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "count" + FUNCTION = "remove" - def count(self, list: list[Any], value: Any) -> tuple[int]: - return (list.count(value),) + def remove(self, list: list[Any], value: Any) -> tuple[list[Any], bool]: + result = list.copy() + try: + result.remove(value) + return result, True + except ValueError: + return result, False + + +class ListReverse(ComfyNodeABC): + """ + Reverses the order of items in a LIST. + + This node takes a LIST as input and returns a new LIST with the items in reversed order. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "reverse" + + def reverse(self, list: list[Any]) -> tuple[list[Any]]: + result = list.copy() + result.reverse() + return (result,) + + +class ListSetItem(ComfyNodeABC): + """ + Sets an item at a specified position in a LIST. + + This node takes a LIST, an index, and a value, then returns a new LIST with + the item at the specified index replaced by the value. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + "index": ("INT", {"default": 0}), + "value": (IO.ANY, {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "set_item" + + def set_item(self, list: list[Any], index: int, value: Any) -> tuple[list[Any]]: + result = list.copy() + try: + result[index] = value + return (result,) + except IndexError: + raise IndexError(f"Index {index} out of range for LIST of length {len(list)}") + + +class ListSlice(ComfyNodeABC): + """ + Creates a slice of a LIST. + + This node takes a LIST and start/stop/step parameters, and returns a new LIST + containing the specified slice of the original LIST. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + }, + "optional": { + "start": ("INT", {"default": 0}), + "stop": ("INT", {"default": INT_MAX}), + "step": ("INT", {"default": 1}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "slice" + + def slice(self, list: list[Any], start: int = 0, stop: int = INT_MAX, + step: int = 1) -> tuple[list[Any]]: + return (list[start:stop:step],) class ListSort(ComfyNodeABC): @@ -271,172 +501,6 @@ class ListSort(ComfyNodeABC): return (list.copy(),) -class ListReverse(ComfyNodeABC): - """ - Reverses the order of items in a LIST. - - This node takes a LIST as input and returns a new LIST with the items in reversed order. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - } - } - - RETURN_TYPES = ("LIST",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "reverse" - - def reverse(self, list: list[Any]) -> tuple[list[Any]]: - result = list.copy() - result.reverse() - return (result,) - - -class ListLength(ComfyNodeABC): - """ - Returns the number of items in a LIST. - - This node takes a LIST as input and returns its length as an integer. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - } - } - - RETURN_TYPES = ("INT",) - RETURN_NAMES = ("length",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "length" - - def length(self, list: list[Any]) -> tuple[int]: - return (len(list),) - - -class ListSlice(ComfyNodeABC): - """ - Creates a slice of a LIST. - - This node takes a LIST and start/stop/step parameters, and returns a new LIST - containing the specified slice of the original LIST. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - }, - "optional": { - "start": ("INT", {"default": 0}), - "stop": ("INT", {"default": INT_MAX}), - "step": ("INT", {"default": 1}), - } - } - - RETURN_TYPES = ("LIST",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "slice" - - def slice(self, list: list[Any], start: int = 0, stop: int = INT_MAX, - step: int = 1) -> tuple[list[Any]]: - return (list[start:stop:step],) - - -class ListGetItem(ComfyNodeABC): - """ - Retrieves an item at a specified position in a LIST. - - This node takes a LIST and an index as inputs, then returns the item at the specified index. - Negative indices count from the end of the LIST. - Out of range indices return None. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - "index": ("INT", {"default": 0}), - } - } - - RETURN_TYPES = (IO.ANY,) - RETURN_NAMES = ("item",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_item" - - def get_item(self, list: list[Any], index: int) -> tuple[Any]: - try: - return (list[index],) - except IndexError: - return (None,) - - -class ListSetItem(ComfyNodeABC): - """ - Sets an item at a specified position in a LIST. - - This node takes a LIST, an index, and a value, then returns a new LIST with - the item at the specified index replaced by the value. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - "index": ("INT", {"default": 0}), - "value": (IO.ANY, {}), - } - } - - RETURN_TYPES = ("LIST",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "set_item" - - def set_item(self, list: list[Any], index: int, value: Any) -> tuple[list[Any]]: - result = list.copy() - try: - result[index] = value - return (result,) - except IndexError: - raise IndexError(f"Index {index} out of range for LIST of length {len(list)}") - - -class ListContains(ComfyNodeABC): - """ - Checks if a LIST contains a specified value. - - This node takes a LIST and a value as inputs, then returns True if the value - is present in the LIST, and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - "value": (IO.ANY, {}), - } - } - - RETURN_TYPES = ("BOOLEAN",) - RETURN_NAMES = ("contains",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "contains" - - def contains(self, list: list[Any], value: Any) -> tuple[bool]: - return (value in list,) - - class ListToDataList(ComfyNodeABC): """ Converts a LIST object into a ComfyUI data list. @@ -487,110 +551,46 @@ class ListToSet(ComfyNodeABC): return (set(list),) -class ListMin(ComfyNodeABC): - """ - Returns the minimum value in a LIST. - - This node takes a LIST of comparable items and returns the minimum value. - Returns None if the LIST is empty or if items are not comparable. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - } - } - - RETURN_TYPES = (IO.ANY,) - RETURN_NAMES = ("min_value",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "find_min" - - def find_min(self, list: list[Any]) -> tuple[Any]: - if not list: - return (None,) - - try: - return (min(list),) - except (TypeError, ValueError): - # Handle case where list contains non-comparable elements - return (None,) - - -class ListMax(ComfyNodeABC): - """ - Returns the maximum value in a LIST. - - This node takes a LIST of comparable items and returns the maximum value. - Returns None if the LIST is empty or if items are not comparable. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("LIST", {}), - } - } - - RETURN_TYPES = (IO.ANY,) - RETURN_NAMES = ("max_value",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "find_max" - - def find_max(self, list: list[Any]) -> tuple[Any]: - if not list: - return (None,) - - try: - return (max(list),) - except (TypeError, ValueError): - # Handle case where list contains non-comparable elements - return (None,) - - NODE_CLASS_MAPPINGS = { - "Basic data handling: ListAppend": ListAppend, "Basic data handling: ListEmpty": ListEmpty, - "Basic data handling: ListExtend": ListExtend, - "Basic data handling: ListInsert": ListInsert, - "Basic data handling: ListRemove": ListRemove, - "Basic data handling: ListPop": ListPop, - "Basic data handling: ListIndex": ListIndex, - "Basic data handling: ListCount": ListCount, - "Basic data handling: ListSort": ListSort, - "Basic data handling: ListReverse": ListReverse, - "Basic data handling: ListLength": ListLength, - "Basic data handling: ListSlice": ListSlice, - "Basic data handling: ListGetItem": ListGetItem, - "Basic data handling: ListSetItem": ListSetItem, + "Basic data handling: ListAppend": ListAppend, "Basic data handling: ListContains": ListContains, + "Basic data handling: ListCount": ListCount, + "Basic data handling: ListExtend": ListExtend, + "Basic data handling: ListGetItem": ListGetItem, + "Basic data handling: ListIndex": ListIndex, + "Basic data handling: ListInsert": ListInsert, + "Basic data handling: ListLength": ListLength, + "Basic data handling: ListMax": ListMax, + "Basic data handling: ListMin": ListMin, + "Basic data handling: ListPop": ListPop, + "Basic data handling: ListRemove": ListRemove, + "Basic data handling: ListReverse": ListReverse, + "Basic data handling: ListSetItem": ListSetItem, + "Basic data handling: ListSlice": ListSlice, + "Basic data handling: ListSort": ListSort, "Basic data handling: ListToDataList": ListToDataList, "Basic data handling: ListToSet": ListToSet, - "Basic data handling: ListMin": ListMin, - "Basic data handling: ListMax": ListMax, } NODE_DISPLAY_NAME_MAPPINGS = { + "Basic data handling: ListEmpty": "create empty", "Basic data handling: ListAppend": "append", - "Basic data handling: ListEmpty": "empty", - "Basic data handling: ListExtend": "extend", - "Basic data handling: ListInsert": "insert", - "Basic data handling: ListRemove": "remove", - "Basic data handling: ListPop": "pop", - "Basic data handling: ListIndex": "index", - "Basic data handling: ListCount": "count", - "Basic data handling: ListSort": "sort", - "Basic data handling: ListReverse": "reverse", - "Basic data handling: ListLength": "length", - "Basic data handling: ListSlice": "slice", - "Basic data handling: ListGetItem": "get item", - "Basic data handling: ListSetItem": "set item", "Basic data handling: ListContains": "contains", + "Basic data handling: ListCount": "count", + "Basic data handling: ListExtend": "extend", + "Basic data handling: ListGetItem": "get item", + "Basic data handling: ListIndex": "index", + "Basic data handling: ListInsert": "insert", + "Basic data handling: ListLength": "length", + "Basic data handling: ListMax": "max", + "Basic data handling: ListMin": "min", + "Basic data handling: ListPop": "pop", + "Basic data handling: ListRemove": "remove", + "Basic data handling: ListReverse": "reverse", + "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: ListToSet": "convert to SET", - "Basic data handling: ListMin": "min", - "Basic data handling: ListMax": "max", } diff --git a/src/basic_data_handling/math_nodes.py b/src/basic_data_handling/math_nodes.py index 2ff4b66..ca2b502 100644 --- a/src/basic_data_handling/math_nodes.py +++ b/src/basic_data_handling/math_nodes.py @@ -3,111 +3,17 @@ from inspect import cleandoc from typing import Literal, Union from comfy.comfy_types.node_typing import IO, ComfyNodeABC -class MathSin(ComfyNodeABC): +class MathAbs(ComfyNodeABC): """ - Calculates the sine of an angle in radians or degrees. + Returns the absolute value of a number. - This node takes an angle value and unit type, and returns the sine of that angle. - The sine of an angle is the ratio of the length of the opposite side to the length - of the hypotenuse in a right-angled triangle. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), - } - } - - RETURN_TYPES = (IO.FLOAT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: - if unit == "degrees": - # Convert degrees to radians - angle = math.radians(angle) - return (math.sin(angle),) - - -class MathCos(ComfyNodeABC): - """ - Calculates the cosine of an angle in radians or degrees. - - This node takes an angle value and unit type, and returns the cosine of that angle. - The cosine of an angle is the ratio of the length of the adjacent side to the length - of the hypotenuse in a right-angled triangle. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), - } - } - - RETURN_TYPES = (IO.FLOAT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: - if unit == "degrees": - # Convert degrees to radians - angle = math.radians(angle) - return (math.cos(angle),) - - -class MathTan(ComfyNodeABC): - """ - Calculates the tangent of an angle in radians or degrees. - - This node takes an angle value and unit type, and returns the tangent of that angle. - The tangent of an angle is the ratio of the length of the opposite side to the length - of the adjacent side in a right-angled triangle. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), - } - } - - RETURN_TYPES = (IO.FLOAT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: - if unit == "degrees": - # Convert degrees to radians - angle = math.radians(angle) - - # Handle specific angles that would result in division by zero - if abs(math.cos(angle)) < 1e-10: - raise ValueError("Tangent is undefined at this angle (division by zero)") - - return (math.tan(angle),) - - -class MathAsin(ComfyNodeABC): - """ - Calculates the arc sine (inverse sine) of a value. - - This node takes a value between -1 and 1, and returns the arc sine in radians or degrees. - The arc sine is the inverse operation of sine, returning the angle whose sine is the input value. + This node takes a number and returns its absolute value (magnitude without sign). """ @classmethod def INPUT_TYPES(cls): return { "required": { "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -116,11 +22,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) - if unit == "degrees": - result = math.degrees(result) - return (result,) + def calculate(self, value: Union[float, int]) -> tuple[Union[float, int]]: + return (abs(value),) class MathAcos(ComfyNodeABC): @@ -151,6 +54,34 @@ class MathAcos(ComfyNodeABC): return (result,) +class MathAsin(ComfyNodeABC): + """ + Calculates the arc sine (inverse sine) of a value. + + This node takes a value between -1 and 1, and returns the arc sine in radians or degrees. + The arc sine is the inverse operation of sine, returning the angle whose sine is the input value. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "unit": (["radians", "degrees"], {"default": "degrees"}), + } + } + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + result = math.asin(value) + if unit == "degrees": + result = math.degrees(result) + return (result,) + + class MathAtan(ComfyNodeABC): """ Calculates the arc tangent (inverse tangent) of a value. @@ -209,17 +140,43 @@ class MathAtan2(ComfyNodeABC): return (result,) -class MathSqrt(ComfyNodeABC): +class MathCeil(ComfyNodeABC): """ - Calculates the square root of a non-negative number. + Returns the ceiling of a number. - This node takes a non-negative number and returns its square root. + This node takes a number and returns the smallest integer greater than or equal to the input value. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "FLOAT"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float) -> tuple[int]: + return (math.ceil(value),) + + +class MathCos(ComfyNodeABC): + """ + Calculates the cosine of an angle in radians or degrees. + + This node takes an angle value and unit type, and returns the cosine of that angle. + The cosine of an angle is the ratio of the length of the adjacent side to the length + of the hypotenuse in a right-angled triangle. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "unit": (["radians", "degrees"], {"default": "degrees"}), } } @@ -228,8 +185,53 @@ class MathSqrt(ComfyNodeABC): DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, value: float) -> tuple[float]: - return (math.sqrt(value),) + def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + if unit == "degrees": + # Convert degrees to radians + angle = math.radians(angle) + return (math.cos(angle),) + + +class MathDegrees(ComfyNodeABC): + """ + Converts an angle from radians to degrees. + + This node takes an angle in radians and returns the equivalent angle in degrees. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "radians": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + } + } + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, radians: float) -> tuple[float]: + return (math.degrees(radians),) + + +class MathE(ComfyNodeABC): + """ + Returns the mathematical constant e. + + This node returns the value of e (Euler's number), which is approximately 2.71828. + """ + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self) -> tuple[float]: + return (math.e,) class MathExp(ComfyNodeABC): @@ -255,6 +257,29 @@ class MathExp(ComfyNodeABC): return (math.exp(value),) +class MathFloor(ComfyNodeABC): + """ + Returns the floor of a number. + + This node takes a number and returns the largest integer less than or equal to the input value. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float) -> tuple[int]: + return (math.floor(value),) + + class MathLog(ComfyNodeABC): """ Calculates the natural logarithm (base e) of a number. @@ -308,157 +333,29 @@ class MathLog10(ComfyNodeABC): return (math.log10(value),) -class MathDegrees(ComfyNodeABC): +class MathMax(ComfyNodeABC): """ - Converts an angle from radians to degrees. + Returns the maximum value among the inputs. - This node takes an angle in radians and returns the equivalent angle in degrees. + This node takes two or more values and returns the maximum value among them. + The function works with both FLOAT and INT types. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "radians": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), } } - RETURN_TYPES = (IO.FLOAT,) + RETURN_TYPES = ("*",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" - def calculate(self, radians: float) -> tuple[float]: - return (math.degrees(radians),) - - -class MathRadians(ComfyNodeABC): - """ - Converts an angle from degrees to radians. - - This node takes an angle in degrees and returns the equivalent angle in radians. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "degrees": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - } - } - - RETURN_TYPES = (IO.FLOAT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self, degrees: float) -> tuple[float]: - return (math.radians(degrees),) - - -class MathFloor(ComfyNodeABC): - """ - Returns the floor of a number. - - This node takes a number and returns the largest integer less than or equal to the input value. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - } - } - - RETURN_TYPES = (IO.INT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self, value: float) -> tuple[int]: - return (math.floor(value),) - - -class MathCeil(ComfyNodeABC): - """ - Returns the ceiling of a number. - - This node takes a number and returns the smallest integer greater than or equal to the input value. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - } - } - - RETURN_TYPES = (IO.INT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self, value: float) -> tuple[int]: - return (math.ceil(value),) - - -class MathAbs(ComfyNodeABC): - """ - Returns the absolute value of a number. - - This node takes a number and returns its absolute value (magnitude without sign). - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - } - } - - RETURN_TYPES = (IO.FLOAT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self, value: Union[float, int]) -> tuple[Union[float, int]]: - return (abs(value),) - - -class MathPi(ComfyNodeABC): - """ - Returns the mathematical constant π (pi). - - This node returns the value of π, which is approximately 3.14159. - """ - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = (IO.FLOAT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self) -> tuple[float]: - return (math.pi,) - - -class MathE(ComfyNodeABC): - """ - Returns the mathematical constant e. - - This node returns the value of e (Euler's number), which is approximately 2.71828. - """ - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = (IO.FLOAT,) - CATEGORY = "Basic/maths" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "calculate" - - def calculate(self) -> tuple[float]: - return (math.e,) + def calculate(self, value1: Union[float, int], value2: Union[float, int]) -> tuple[Union[float, int]]: + return (max(value1, value2),) class MathMin(ComfyNodeABC): @@ -486,73 +383,176 @@ class MathMin(ComfyNodeABC): return (min(value1, value2),) -class MathMax(ComfyNodeABC): +class MathPi(ComfyNodeABC): """ - Returns the maximum value among the inputs. + Returns the mathematical constant π (pi). - This node takes two or more values and returns the maximum value among them. - The function works with both FLOAT and INT types. + This node returns the value of π, which is approximately 3.14159. + """ + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self) -> tuple[float]: + return (math.pi,) + + +class MathRadians(ComfyNodeABC): + """ + Converts an angle from degrees to radians. + + This node takes an angle in degrees and returns the equivalent angle in radians. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "degrees": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), } } - RETURN_TYPES = ("*",) + RETURN_TYPES = (IO.FLOAT,) CATEGORY = "Basic/maths" 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, degrees: float) -> tuple[float]: + return (math.radians(degrees),) + + +class MathSin(ComfyNodeABC): + """ + Calculates the sine of an angle in radians or degrees. + + This node takes an angle value and unit type, and returns the sine of that angle. + The sine of an angle is the ratio of the length of the opposite side to the length + of the hypotenuse in a right-angled triangle. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "unit": (["radians", "degrees"], {"default": "degrees"}), + } + } + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + if unit == "degrees": + # Convert degrees to radians + angle = math.radians(angle) + return (math.sin(angle),) + + +class MathSqrt(ComfyNodeABC): + """ + Calculates the square root of a non-negative number. + + This node takes a non-negative number and returns its square root. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "FLOAT"}), + } + } + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, value: float) -> tuple[float]: + return (math.sqrt(value),) + + +class MathTan(ComfyNodeABC): + """ + Calculates the tangent of an angle in radians or degrees. + + This node takes an angle value and unit type, and returns the tangent of that angle. + The tangent of an angle is the ratio of the length of the opposite side to the length + of the adjacent side in a right-angled triangle. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}), + "unit": (["radians", "degrees"], {"default": "degrees"}), + } + } + + RETURN_TYPES = (IO.FLOAT,) + CATEGORY = "Basic/maths" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "calculate" + + def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]: + if unit == "degrees": + # Convert degrees to radians + angle = math.radians(angle) + + # Handle specific angles that would result in division by zero + if abs(math.cos(angle)) < 1e-10: + raise ValueError("Tangent is undefined at this angle (division by zero)") + + return (math.tan(angle),) NODE_CLASS_MAPPINGS = { - "Basic data handling: MathSin": MathSin, - "Basic data handling: MathCos": MathCos, - "Basic data handling: MathTan": MathTan, - "Basic data handling: MathAsin": MathAsin, + "Basic data handling: MathAbs": MathAbs, "Basic data handling: MathAcos": MathAcos, + "Basic data handling: MathAsin": MathAsin, "Basic data handling: MathAtan": MathAtan, "Basic data handling: MathAtan2": MathAtan2, - "Basic data handling: MathSqrt": MathSqrt, + "Basic data handling: MathCeil": MathCeil, + "Basic data handling: MathCos": MathCos, + "Basic data handling: MathDegrees": MathDegrees, + "Basic data handling: MathE": MathE, "Basic data handling: MathExp": MathExp, + "Basic data handling: MathFloor": MathFloor, "Basic data handling: MathLog": MathLog, "Basic data handling: MathLog10": MathLog10, - "Basic data handling: MathDegrees": MathDegrees, - "Basic data handling: MathRadians": MathRadians, - "Basic data handling: MathFloor": MathFloor, - "Basic data handling: MathCeil": MathCeil, - "Basic data handling: MathAbs": MathAbs, - "Basic data handling: MathPi": MathPi, - "Basic data handling: MathE": MathE, - "Basic data handling: MathMin": MathMin, "Basic data handling: MathMax": MathMax, + "Basic data handling: MathMin": MathMin, + "Basic data handling: MathPi": MathPi, + "Basic data handling: MathRadians": MathRadians, + "Basic data handling: MathSin": MathSin, + "Basic data handling: MathSqrt": MathSqrt, + "Basic data handling: MathTan": MathTan, } NODE_DISPLAY_NAME_MAPPINGS = { - "Basic data handling: MathSin": "sin", - "Basic data handling: MathCos": "cos", - "Basic data handling: MathTan": "tan", - "Basic data handling: MathAsin": "asin", + "Basic data handling: MathAbs": "abs", "Basic data handling: MathAcos": "acos", + "Basic data handling: MathAsin": "asin", "Basic data handling: MathAtan": "atan", "Basic data handling: MathAtan2": "atan2", - "Basic data handling: MathSqrt": "sqrt", + "Basic data handling: MathCeil": "ceil", + "Basic data handling: MathCos": "cos", + "Basic data handling: MathDegrees": "degrees", + "Basic data handling: MathE": "e (2.71828)", "Basic data handling: MathExp": "exp", + "Basic data handling: MathFloor": "floor", "Basic data handling: MathLog": "log", "Basic data handling: MathLog10": "log10", - "Basic data handling: MathDegrees": "degrees", - "Basic data handling: MathRadians": "radians", - "Basic data handling: MathFloor": "floor", - "Basic data handling: MathCeil": "ceil", - "Basic data handling: MathAbs": "abs", - "Basic data handling: MathPi": "pi", - "Basic data handling: MathE": "e", - "Basic data handling: MathMin": "min", "Basic data handling: MathMax": "max", + "Basic data handling: MathMin": "min", + "Basic data handling: MathPi": "pi (3.14159)", + "Basic data handling: MathRadians": "radians", + "Basic data handling: MathSin": "sin", + "Basic data handling: MathSqrt": "sqrt", + "Basic data handling: MathTan": "tan", } diff --git a/src/basic_data_handling/path_nodes.py b/src/basic_data_handling/path_nodes.py index dc19a70..ae7a2a1 100644 --- a/src/basic_data_handling/path_nodes.py +++ b/src/basic_data_handling/path_nodes.py @@ -4,6 +4,341 @@ import glob from comfy.comfy_types.node_typing import IO, ComfyNodeABC +class PathAbspath(ComfyNodeABC): + """ + Returns the absolute path of a file or directory. + + This node takes a path and returns its absolute (full) path + by resolving any relative path components and symbolic links. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("absolute_path",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_abspath" + + def get_abspath(self, path: str) -> tuple[str]: + return (os.path.abspath(path),) + + +class PathBasename(ComfyNodeABC): + """ + Returns the base name of a path. + + This node extracts the filename component from a path, + removing any directory information. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("basename",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_basename" + + def get_basename(self, path: str) -> tuple[str]: + return (os.path.basename(path),) + + +class PathCommonPrefix(ComfyNodeABC): + """ + Finds the common prefix of multiple paths. + + This node returns the longest common leading component of the given paths. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path1": (IO.STRING, {"default": ""}), + }, + "optional": { + "path2": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("common_prefix",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_common_prefix" + + def get_common_prefix(self, path1: str, path2: str = "") -> tuple[str]: + paths = [p for p in [path1, path2] if p] + return (os.path.commonprefix(paths),) + + +class PathDirname(ComfyNodeABC): + """ + Returns the directory name of a path. + + This node extracts the directory component from a path, + removing the filename. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("dirname",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_dirname" + + def get_dirname(self, path: str) -> tuple[str]: + return (os.path.dirname(path),) + + +class PathExists(ComfyNodeABC): + """ + Checks if a path exists in the filesystem. + + This node returns True if the path exists (either as a file or a directory), + and False otherwise. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("exists",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "check_exists" + + def check_exists(self, path: str) -> tuple[bool]: + return (os.path.exists(path),) + + +class PathExpandVars(ComfyNodeABC): + """ + Expands environment variables in a path. + + This node replaces environment variables in a path with their values. + For example, $HOME or ${HOME} on Unix, or %USERPROFILE% on Windows. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("expanded_path",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "expand_vars" + + def expand_vars(self, path: str) -> tuple[str]: + return (os.path.expandvars(path),) + + +class PathGetCwd(ComfyNodeABC): + """ + Returns the current working directory. + + This node returns the current working directory as an absolute path. + """ + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("current_directory",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_cwd" + + def get_cwd(self) -> tuple[str]: + return (os.getcwd(),) + + +class PathGetExtension(ComfyNodeABC): + """ + Returns the extension of a file. + + This node extracts the file extension from a path, + including the dot (e.g., '.txt'). + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("extension",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_extension" + + def get_extension(self, path: str) -> tuple[str]: + return (os.path.splitext(path)[1],) + + +class PathGetSize(ComfyNodeABC): + """ + Returns the size of a file in bytes. + + This node returns the size in bytes of the file at the given path. + Raises an error if the path doesn't exist or isn't a file. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("size_bytes",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_size" + + def get_size(self, path: str) -> tuple[int]: + if not os.path.exists(path): + raise FileNotFoundError(f"Basic data handling: Path does not exist: {path}") + if not os.path.isfile(path): + raise ValueError(f"Basic data handling: Path is not a file: {path}") + return (os.path.getsize(path),) + + +class PathGlob(ComfyNodeABC): + """ + Finds paths matching a pattern. + + This node returns a list of paths matching the given pattern. + The pattern follows shell-style wildcard rules: + * - matches any number of characters + ? - matches a single character + [seq] - matches any character in seq + [!seq] - matches any character not in seq + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "pattern": (IO.STRING, {"default": "*.txt"}), + }, + "optional": { + "recursive": (IO.BOOLEAN, {"default": False}), + } + } + + RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("matching_paths",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "glob_paths" + OUTPUT_IS_LIST = (True,) + + def glob_paths(self, pattern: str, recursive: bool = False) -> tuple[list[str]]: + return (glob.glob(pattern, recursive=recursive),) + + +class PathIsAbsolute(ComfyNodeABC): + """ + Checks if a path is absolute. + + This node returns True if the path is absolute (begins at the root directory), + and False if it's relative. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("is_absolute",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "check_is_absolute" + + def check_is_absolute(self, path: str) -> tuple[bool]: + return (os.path.isabs(path),) + + +class PathIsDir(ComfyNodeABC): + """ + Checks if a path points to a directory. + + This node returns True if the path exists and is a directory, + and False otherwise. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("is_dir",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "check_is_dir" + + def check_is_dir(self, path: str) -> tuple[bool]: + return (os.path.isdir(path),) + + +class PathIsFile(ComfyNodeABC): + """ + Checks if a path points to a file. + + This node returns True if the path exists and is a regular file, + and False otherwise. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("is_file",) + CATEGORY = "Basic/path" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "check_is_file" + + def check_is_file(self, path: str) -> tuple[bool]: + return (os.path.isfile(path),) + + class PathJoin(ComfyNodeABC): """ Joins multiple path components into a single path. @@ -34,258 +369,51 @@ class PathJoin(ComfyNodeABC): return (str(os.path.join(*paths)),) -class PathAbspath(ComfyNodeABC): +class PathListDir(ComfyNodeABC): """ - Returns the absolute path of a file or directory. + Lists the contents of a directory. - This node takes a path and returns its absolute (full) path - by resolving any relative path components and symbolic links. + This node returns a list of files and directories in the specified path. + If 'files_only' is True, it only returns files. + If 'dirs_only' is True, it only returns directories. + If both are False, it returns all contents. """ @classmethod def INPUT_TYPES(cls): return { "required": { "path": (IO.STRING, {"default": ""}), + }, + "optional": { + "files_only": (IO.BOOLEAN, {"default": False}), + "dirs_only": (IO.BOOLEAN, {"default": False}), } } RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("absolute_path",) + RETURN_NAMES = ("entries",) CATEGORY = "Basic/path" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_abspath" + FUNCTION = "list_directory" + OUTPUT_IS_LIST = (True,) - def get_abspath(self, path: str) -> tuple[str]: - return (os.path.abspath(path),) + def list_directory(self, path: str, files_only: str = False, dirs_only: str = False) -> tuple[list[str]]: + if not path: + path = os.getcwd() - -class PathExists(ComfyNodeABC): - """ - Checks if a path exists in the filesystem. - - This node returns True if the path exists (either as a file or a directory), - and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("exists",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "check_exists" - - def check_exists(self, path: str) -> tuple[bool]: - return (os.path.exists(path),) - - -class PathIsFile(ComfyNodeABC): - """ - Checks if a path points to a file. - - This node returns True if the path exists and is a regular file, - and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("is_file",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "check_is_file" - - def check_is_file(self, path: str) -> tuple[bool]: - return (os.path.isfile(path),) - - -class PathIsDir(ComfyNodeABC): - """ - Checks if a path points to a directory. - - This node returns True if the path exists and is a directory, - and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("is_dir",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "check_is_dir" - - def check_is_dir(self, path: str) -> tuple[bool]: - return (os.path.isdir(path),) - - -class PathGetSize(ComfyNodeABC): - """ - Returns the size of a file in bytes. - - This node returns the size in bytes of the file at the given path. - Raises an error if the path doesn't exist or isn't a file. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.INT,) - RETURN_NAMES = ("size_bytes",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_size" - - def get_size(self, path: str) -> tuple[int]: if not os.path.exists(path): - raise FileNotFoundError(f"Basic data handling: Path does not exist: {path}") - if not os.path.isfile(path): - raise ValueError(f"Basic data handling: Path is not a file: {path}") - return (os.path.getsize(path),) + raise FileNotFoundError(f"Directory does not exist: {path}") + if not os.path.isdir(path): + raise NotADirectoryError(f"Basic data handling: Path is not a directory: {path}") + entries = os.listdir(path) -class PathSplit(ComfyNodeABC): - """ - Splits a path into directory and filename components. + if files_only: + entries = [e for e in entries if os.path.isfile(os.path.join(path, e))] + elif dirs_only: + entries = [e for e in entries if os.path.isdir(os.path.join(path, e))] - This node takes a path and returns a tuple containing the directory path - and the filename. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.STRING, IO.STRING) - RETURN_NAMES = ("directory", "filename") - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "split_path" - - def split_path(self, path: str) -> tuple[str, str]: - return os.path.split(path) - - -class PathSplitExt(ComfyNodeABC): - """ - Splits a path into name and extension components. - - This node takes a path and returns a tuple containing the path without - the extension and the extension (including the dot). - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.STRING, IO.STRING) - RETURN_NAMES = ("path_without_ext", "extension") - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "split_ext" - - def split_ext(self, path: str) -> tuple[str, str]: - return os.path.splitext(path) - - -class PathBasename(ComfyNodeABC): - """ - Returns the base name of a path. - - This node extracts the filename component from a path, - removing any directory information. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("basename",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_basename" - - def get_basename(self, path: str) -> tuple[str]: - return (os.path.basename(path),) - - -class PathDirname(ComfyNodeABC): - """ - Returns the directory name of a path. - - This node extracts the directory component from a path, - removing the filename. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("dirname",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_dirname" - - def get_dirname(self, path: str) -> tuple[str]: - return (os.path.dirname(path),) - - -class PathGetExtension(ComfyNodeABC): - """ - Returns the extension of a file. - - This node extracts the file extension from a path, - including the dot (e.g., '.txt'). - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("extension",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_extension" - - def get_extension(self, path: str) -> tuple[str]: - return (os.path.splitext(path)[1],) + return (entries,) class PathNormalize(ComfyNodeABC): @@ -344,45 +472,12 @@ class PathRelative(ComfyNodeABC): return (os.path.relpath(path, start),) -class PathGlob(ComfyNodeABC): +class PathSplit(ComfyNodeABC): """ - Finds paths matching a pattern. + Splits a path into directory and filename components. - This node returns a list of paths matching the given pattern. - The pattern follows shell-style wildcard rules: - * - matches any number of characters - ? - matches a single character - [seq] - matches any character in seq - [!seq] - matches any character not in seq - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "pattern": (IO.STRING, {"default": "*.txt"}), - }, - "optional": { - "recursive": (IO.BOOLEAN, {"default": False}), - } - } - - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("matching_paths",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "glob_paths" - OUTPUT_IS_LIST = (True,) - - def glob_paths(self, pattern: str, recursive: bool = False) -> tuple[list[str]]: - return (glob.glob(pattern, recursive=recursive),) - - -class PathExpandVars(ComfyNodeABC): - """ - Expands environment variables in a path. - - This node replaces environment variables in a path with their values. - For example, $HOME or ${HOME} on Unix, or %USERPROFILE% on Windows. + This node takes a path and returns a tuple containing the directory path + and the filename. """ @classmethod def INPUT_TYPES(cls): @@ -392,89 +487,22 @@ class PathExpandVars(ComfyNodeABC): } } - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("expanded_path",) + RETURN_TYPES = (IO.STRING, IO.STRING) + RETURN_NAMES = ("directory", "filename") CATEGORY = "Basic/path" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "expand_vars" + FUNCTION = "split_path" - def expand_vars(self, path: str) -> tuple[str]: - return (os.path.expandvars(path),) + def split_path(self, path: str) -> tuple[str, str]: + return os.path.split(path) -class PathGetCwd(ComfyNodeABC): +class PathSplitExt(ComfyNodeABC): """ - Returns the current working directory. + Splits a path into name and extension components. - This node returns the current working directory as an absolute path. - """ - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("current_directory",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_cwd" - - def get_cwd(self) -> tuple[str]: - return (os.getcwd(),) - - -class PathListDir(ComfyNodeABC): - """ - Lists the contents of a directory. - - This node returns a list of files and directories in the specified path. - If 'files_only' is True, it only returns files. - If 'dirs_only' is True, it only returns directories. - If both are False, it returns all contents. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path": (IO.STRING, {"default": ""}), - }, - "optional": { - "files_only": (IO.BOOLEAN, {"default": False}), - "dirs_only": (IO.BOOLEAN, {"default": False}), - } - } - - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("entries",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "list_directory" - OUTPUT_IS_LIST = (True,) - - def list_directory(self, path: str, files_only: str = False, dirs_only: str = False) -> tuple[list[str]]: - if not path: - path = os.getcwd() - - if not os.path.exists(path): - raise FileNotFoundError(f"Directory does not exist: {path}") - if not os.path.isdir(path): - raise NotADirectoryError(f"Basic data handling: Path is not a directory: {path}") - - entries = os.listdir(path) - - if files_only: - entries = [e for e in entries if os.path.isfile(os.path.join(path, e))] - elif dirs_only: - entries = [e for e in entries if os.path.isdir(os.path.join(path, e))] - - return (entries,) - - -class PathIsAbsolute(ComfyNodeABC): - """ - Checks if a path is absolute. - - This node returns True if the path is absolute (begins at the root directory), - and False if it's relative. + This node takes a path and returns a tuple containing the path without + the extension and the extension (including the dot). """ @classmethod def INPUT_TYPES(cls): @@ -484,84 +512,56 @@ class PathIsAbsolute(ComfyNodeABC): } } - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("is_absolute",) + RETURN_TYPES = (IO.STRING, IO.STRING) + RETURN_NAMES = ("path_without_ext", "extension") CATEGORY = "Basic/path" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "check_is_absolute" + FUNCTION = "split_ext" - def check_is_absolute(self, path: str) -> tuple[bool]: - return (os.path.isabs(path),) - - -class PathCommonPrefix(ComfyNodeABC): - """ - Finds the common prefix of multiple paths. - - This node returns the longest common leading component of the given paths. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "path1": (IO.STRING, {"default": ""}), - }, - "optional": { - "path2": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.STRING,) - RETURN_NAMES = ("common_prefix",) - CATEGORY = "Basic/path" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "get_common_prefix" - - def get_common_prefix(self, path1: str, path2: str = "") -> tuple[str]: - paths = [p for p in [path1, path2] if p] - return (os.path.commonprefix(paths),) + def split_ext(self, path: str) -> tuple[str, str]: + return os.path.splitext(path) NODE_CLASS_MAPPINGS = { - "Basic data handling: PathJoin": PathJoin, "Basic data handling: PathAbspath": PathAbspath, - "Basic data handling: PathExists": PathExists, - "Basic data handling: PathIsFile": PathIsFile, - "Basic data handling: PathIsDir": PathIsDir, - "Basic data handling: PathGetSize": PathGetSize, - "Basic data handling: PathSplit": PathSplit, - "Basic data handling: PathSplitExt": PathSplitExt, "Basic data handling: PathBasename": PathBasename, + "Basic data handling: PathCommonPrefix": PathCommonPrefix, "Basic data handling: PathDirname": PathDirname, - "Basic data handling: PathGetExtension": PathGetExtension, - "Basic data handling: PathNormalize": PathNormalize, - "Basic data handling: PathRelative": PathRelative, - "Basic data handling: PathGlob": PathGlob, + "Basic data handling: PathExists": PathExists, "Basic data handling: PathExpandVars": PathExpandVars, "Basic data handling: PathGetCwd": PathGetCwd, - "Basic data handling: PathListDir": PathListDir, + "Basic data handling: PathGetExtension": PathGetExtension, + "Basic data handling: PathGetSize": PathGetSize, + "Basic data handling: PathGlob": PathGlob, "Basic data handling: PathIsAbsolute": PathIsAbsolute, - "Basic data handling: PathCommonPrefix": PathCommonPrefix, + "Basic data handling: PathIsDir": PathIsDir, + "Basic data handling: PathIsFile": PathIsFile, + "Basic data handling: PathJoin": PathJoin, + "Basic data handling: PathListDir": PathListDir, + "Basic data handling: PathNormalize": PathNormalize, + "Basic data handling: PathRelative": PathRelative, + "Basic data handling: PathSplit": PathSplit, + "Basic data handling: PathSplitExt": PathSplitExt, } NODE_DISPLAY_NAME_MAPPINGS = { - "Basic data handling: PathJoin": "join", "Basic data handling: PathAbspath": "abspath", - "Basic data handling: PathExists": "exists", - "Basic data handling: PathIsFile": "is_file", - "Basic data handling: PathIsDir": "is_dir", - "Basic data handling: PathGetSize": "get_size", - "Basic data handling: PathSplit": "split", - "Basic data handling: PathSplitExt": "splitext", "Basic data handling: PathBasename": "basename", + "Basic data handling: PathCommonPrefix": "common prefix", "Basic data handling: PathDirname": "dirname", - "Basic data handling: PathGetExtension": "get_extension", + "Basic data handling: PathExists": "exists", + "Basic data handling: PathExpandVars": "expand vars", + "Basic data handling: PathGetCwd": "get current working directory", + "Basic data handling: PathGetExtension": "get extension", + "Basic data handling: PathGetSize": "get size", + "Basic data handling: PathGlob": "glob", + "Basic data handling: PathIsAbsolute": "is absolute", + "Basic data handling: PathIsDir": "is dir", + "Basic data handling: PathIsFile": "is file", + "Basic data handling: PathJoin": "join", + "Basic data handling: PathListDir": "list dir", "Basic data handling: PathNormalize": "normalize", "Basic data handling: PathRelative": "relative", - "Basic data handling: PathGlob": "glob", - "Basic data handling: PathExpandVars": "expand_vars", - "Basic data handling: PathGetCwd": "get_cwd", - "Basic data handling: PathListDir": "list_dir", - "Basic data handling: PathIsAbsolute": "is_absolute", - "Basic data handling: PathCommonPrefix": "common_prefix", + "Basic data handling: PathSplit": "split", + "Basic data handling: PathSplitExt": "splitext", } diff --git a/src/basic_data_handling/regex_nodes.py b/src/basic_data_handling/regex_nodes.py index 7ff3cfa..267b745 100644 --- a/src/basic_data_handling/regex_nodes.py +++ b/src/basic_data_handling/regex_nodes.py @@ -2,6 +2,77 @@ from inspect import cleandoc import re from comfy.comfy_types.node_typing import IO, ComfyNodeABC +class RegexFindallDataList(ComfyNodeABC): + """ + Returns all non-overlapping matches of a pattern in the string as a list of strings. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {}), + "pattern": (IO.STRING, {}), + } + } + + RETURN_TYPES = (IO.STRING,) + CATEGORY = "Basic/STRING/regex" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "findall" + OUTPUT_IS_LIST = (True,) + + def findall(self, pattern: str, string: str) -> tuple[list[str]]: + return (re.findall(pattern, string),) + + +class RegexFindallList(ComfyNodeABC): + """ + Returns all non-overlapping matches of a pattern in the string as a list of strings. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {}), + "pattern": (IO.STRING, {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/STRING/regex" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "findall" + + def findall(self, pattern: str, string: str) -> tuple[list[str]]: + return (re.findall(pattern, string),) + + +class RegexGroupDict(ComfyNodeABC): + """ + Searches the string with the given pattern and returns a DICT of named groups. + If no match is found, it returns an empty DICT. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {}), + "pattern": (IO.STRING, {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/STRING/regex" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "groupdict" + + def groupdict(self, pattern: str, string: str) -> tuple[dict]: + match = re.search(pattern, string) + if match: + return (match.groupdict(),) + return ({},) + + class RegexSearchGroupsDataList(ComfyNodeABC): """ Searches the string for a match to the pattern and returns a LIST of match groups. @@ -55,77 +126,6 @@ class RegexSearchGroupsList(ComfyNodeABC): return ([],) -class RegexGroupDict(ComfyNodeABC): - """ - Searches the string with the given pattern and returns a DICT of named groups. - If no match is found, it returns an empty DICT. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), - } - } - - RETURN_TYPES = ("DICT",) - CATEGORY = "Basic/STRING/regex" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "groupdict" - - def groupdict(self, pattern: str, string: str) -> tuple[dict]: - match = re.search(pattern, string) - if match: - return (match.groupdict(),) - return ({},) - - -class RegexFindallDataList(ComfyNodeABC): - """ - Returns all non-overlapping matches of a pattern in the string as a list of strings. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), - } - } - - RETURN_TYPES = (IO.STRING,) - CATEGORY = "Basic/STRING/regex" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "findall" - OUTPUT_IS_LIST = (True,) - - def findall(self, pattern: str, string: str) -> tuple[list[str]]: - return (re.findall(pattern, string),) - - -class RegexFindallList(ComfyNodeABC): - """ - Returns all non-overlapping matches of a pattern in the string as a list of strings. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), - } - } - - RETURN_TYPES = ("LIST",) - CATEGORY = "Basic/STRING/regex" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "findall" - - def findall(self, pattern: str, string: str) -> tuple[list[str]]: - return (re.findall(pattern, string),) - - class RegexSplitDataList(ComfyNodeABC): """ Splits the string at each match of the pattern and returns a list of substrings. @@ -220,11 +220,11 @@ class RegexTest(ComfyNodeABC): NODE_CLASS_MAPPINGS = { - "Basic data handling: RegexSearchGroupsDataList": RegexSearchGroupsDataList, - "Basic data handling: RegexSearchGroupsList": RegexSearchGroupsList, - "Basic data handling: RegexGroupDict": RegexGroupDict, "Basic data handling: RegexFindallDataList": RegexFindallDataList, "Basic data handling: RegexFindallList": RegexFindallList, + "Basic data handling: RegexGroupDict": RegexGroupDict, + "Basic data handling: RegexSearchGroupsDataList": RegexSearchGroupsDataList, + "Basic data handling: RegexSearchGroupsList": RegexSearchGroupsList, "Basic data handling: RegexSplitDataList": RegexSplitDataList, "Basic data handling: RegexSplitList": RegexSplitList, "Basic data handling: RegexSub": RegexSub, @@ -232,11 +232,11 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "Basic data handling: RegexSearchGroupsDataList": "search groups (data list)", - "Basic data handling: RegexSearchGroupsList": "search groups (LIST)", - "Basic data handling: RegexGroupDict": "search named groups", "Basic data handling: RegexFindallDataList": "find all (data list)", "Basic data handling: RegexFindallList": "find all (LIST)", + "Basic data handling: RegexGroupDict": "search named groups", + "Basic data handling: RegexSearchGroupsDataList": "search groups (data list)", + "Basic data handling: RegexSearchGroupsList": "search groups (LIST)", "Basic data handling: RegexSplitDataList": "split (data list)", "Basic data handling: RegexSplitList": "split (LIST)", "Basic data handling: RegexSub": "substitute", diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index 27fc2e2..23f10ac 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -29,36 +29,57 @@ class SetAdd(ComfyNodeABC): return (result,) -class SetRemove(ComfyNodeABC): +class SetContains(ComfyNodeABC): """ - Removes an item from a SET. + Checks if a SET contains a specified value. - This node takes a SET and any item as inputs, then returns a new SET - with the item removed and a success indicator. If the item is not present, - the original SET is returned with success set to False. + This node takes a SET and a value as inputs, then returns True if the value + is present in the SET, and False otherwise. """ @classmethod def INPUT_TYPES(cls): return { "required": { "set": ("SET", {}), - "item": (IO.ANY, {}), + "value": (IO.ANY, {}), } } - RETURN_TYPES = ("SET", IO.BOOLEAN) - RETURN_NAMES = ("set", "success") + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("contains",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "remove" + FUNCTION = "contains" - def remove(self, set: set[Any], item: Any) -> tuple[set[Any], bool]: - result = set.copy() - try: - result.remove(item) - return result, True - except KeyError: - return result, False + def contains(self, set: set[Any], value: Any) -> tuple[bool]: + return (value in set,) + + +class SetDifference(ComfyNodeABC): + """ + Returns the difference between two SETs. + + This node takes two SETs as input and returns a new SET containing + elements in the first SET but not in the second SET. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set1": ("SET", {}), + "set2": ("SET", {}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "difference" + + def difference(self, set1: set[Any], set2: set[Any]) -> tuple[set[Any]]: + result = set1.copy() + result.difference_update(set2) + return (result,) class SetDiscard(ComfyNodeABC): @@ -88,75 +109,6 @@ class SetDiscard(ComfyNodeABC): return (result,) -class SetPop(ComfyNodeABC): - """ - Removes and returns an arbitrary item from a SET. - - This node takes a SET as input and returns both the new SET - with an arbitrary item removed and the removed item. - When the SET is empty, the item is None. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "set": ("SET", {}), - } - } - - RETURN_TYPES = ("SET", IO.ANY) - RETURN_NAMES = ("set", "item") - CATEGORY = "Basic/SET" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "pop" - - def pop(self, set: set[Any]) -> tuple[set[Any], Any]: - result = set.copy() - try: - item = result.pop() - return result, item - except KeyError: - return result, None - - -class SetUnion(ComfyNodeABC): - """ - Returns the union of two or more SETs. - - This node takes multiple SETs as input and returns a new SET containing - all elements from all the input SETs. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), - }, - "optional": { - "set3": ("SET", {}), - "set4": ("SET", {}), - } - } - - RETURN_TYPES = ("SET",) - CATEGORY = "Basic/SET" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "union" - - def union(self, set1: set[Any], set2: set[Any], set3=None, set4=None) -> tuple[set[Any]]: - result = set1.copy() - result.update(set2) - - if set3 is not None: - result.update(set3) - - if set4 is not None: - result.update(set4) - - return (result,) - - class SetIntersection(ComfyNodeABC): """ Returns the intersection of two or more SETs. @@ -195,12 +147,11 @@ class SetIntersection(ComfyNodeABC): return (result,) -class SetDifference(ComfyNodeABC): +class SetIsDisjoint(ComfyNodeABC): """ - Returns the difference between two SETs. + Checks if two SETs have no elements in common. - This node takes two SETs as input and returns a new SET containing - elements in the first SET but not in the second SET. + This node takes two SETs as input and returns True if they have no elements in common. """ @classmethod def INPUT_TYPES(cls): @@ -211,42 +162,14 @@ class SetDifference(ComfyNodeABC): } } - RETURN_TYPES = ("SET",) + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("is_disjoint",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "difference" + FUNCTION = "is_disjoint" - def difference(self, set1: set[Any], set2: set[Any]) -> tuple[set[Any]]: - result = set1.copy() - result.difference_update(set2) - return (result,) - - -class SetSymmetricDifference(ComfyNodeABC): - """ - Returns the symmetric difference between two SETs. - - This node takes two SETs as input and returns a new SET containing - elements in either SET but not in both. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), - } - } - - RETURN_TYPES = ("SET",) - CATEGORY = "Basic/SET" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "symmetric_difference" - - def symmetric_difference(self, set1: set[Any], set2: set[Any]) -> tuple[set[Any]]: - result = set1.copy() - result.symmetric_difference_update(set2) - return (result,) + def is_disjoint(self, set1: set[Any], set2: set[Any]) -> tuple[bool]: + return (set1.isdisjoint(set2),) class SetIsSubset(ComfyNodeABC): @@ -301,57 +224,6 @@ class SetIsSuperset(ComfyNodeABC): return (set1.issuperset(set2),) -class SetIsDisjoint(ComfyNodeABC): - """ - Checks if two SETs have no elements in common. - - This node takes two SETs as input and returns True if they have no elements in common. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("is_disjoint",) - CATEGORY = "Basic/SET" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "is_disjoint" - - def is_disjoint(self, set1: set[Any], set2: set[Any]) -> tuple[bool]: - return (set1.isdisjoint(set2),) - - -class SetContains(ComfyNodeABC): - """ - Checks if a SET contains a specified value. - - This node takes a SET and a value as inputs, then returns True if the value - is present in the SET, and False otherwise. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "set": ("SET", {}), - "value": (IO.ANY, {}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - RETURN_NAMES = ("contains",) - CATEGORY = "Basic/SET" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "contains" - - def contains(self, set: set[Any], value: Any) -> tuple[bool]: - return (value in set,) - - class SetLength(ComfyNodeABC): """ Returns the number of items in a SET. @@ -376,28 +248,132 @@ class SetLength(ComfyNodeABC): return (len(set),) -class ListToSet(ComfyNodeABC): +class SetPop(ComfyNodeABC): """ - Converts a LIST into a SET. + Removes and returns an arbitrary item from a SET. - This node takes a LIST input and creates a new SET containing all unique elements - from the LIST, removing any duplicates. + This node takes a SET as input and returns both the new SET + with an arbitrary item removed and the removed item. + When the SET is empty, the item is None. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "set": ("SET", {}), + } + } + + RETURN_TYPES = ("SET", IO.ANY) + RETURN_NAMES = ("set", "item") + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop" + + def pop(self, set: set[Any]) -> tuple[set[Any], Any]: + result = set.copy() + try: + item = result.pop() + return result, item + except KeyError: + return result, None + + +class SetRemove(ComfyNodeABC): + """ + Removes an item from a SET. + + This node takes a SET and any item as inputs, then returns a new SET + with the item removed and a success indicator. If the item is not present, + the original SET is returned with success set to False. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set": ("SET", {}), + "item": (IO.ANY, {}), + } + } + + RETURN_TYPES = ("SET", IO.BOOLEAN) + RETURN_NAMES = ("set", "success") + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "remove" + + def remove(self, set: set[Any], item: Any) -> tuple[set[Any], bool]: + result = set.copy() + try: + result.remove(item) + return result, True + except KeyError: + return result, False + + +class SetSymmetricDifference(ComfyNodeABC): + """ + Returns the symmetric difference between two SETs. + + This node takes two SETs as input and returns a new SET containing + elements in either SET but not in both. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set1": ("SET", {}), + "set2": ("SET", {}), } } RETURN_TYPES = ("SET",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "convert" + FUNCTION = "symmetric_difference" - def convert(self, list: list[Any]) -> tuple[set[Any]]: - return (set(list),) + def symmetric_difference(self, set1: set[Any], set2: set[Any]) -> tuple[set[Any]]: + result = set1.copy() + result.symmetric_difference_update(set2) + return (result,) + + +class SetUnion(ComfyNodeABC): + """ + Returns the union of two or more SETs. + + This node takes multiple SETs as input and returns a new SET containing + all elements from all the input SETs. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set1": ("SET", {}), + "set2": ("SET", {}), + }, + "optional": { + "set3": ("SET", {}), + "set4": ("SET", {}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "union" + + def union(self, set1: set[Any], set2: set[Any], set3=None, set4=None) -> tuple[set[Any]]: + result = set1.copy() + result.update(set2) + + if set3 is not None: + result.update(set3) + + if set4 is not None: + result.update(set4) + + return (result,) class SetToDataList(ComfyNodeABC): @@ -453,36 +429,36 @@ class SetToList(ComfyNodeABC): NODE_CLASS_MAPPINGS = { "Basic data handling: SetAdd": SetAdd, - "Basic data handling: SetRemove": SetRemove, - "Basic data handling: SetDiscard": SetDiscard, - "Basic data handling: SetPop": SetPop, - "Basic data handling: SetUnion": SetUnion, - "Basic data handling: SetIntersection": SetIntersection, + "Basic data handling: SetContains": SetContains, "Basic data handling: SetDifference": SetDifference, - "Basic data handling: SetSymmetricDifference": SetSymmetricDifference, + "Basic data handling: SetDiscard": SetDiscard, + "Basic data handling: SetIntersection": SetIntersection, + "Basic data handling: SetIsDisjoint": SetIsDisjoint, "Basic data handling: SetIsSubset": SetIsSubset, "Basic data handling: SetIsSuperset": SetIsSuperset, - "Basic data handling: SetIsDisjoint": SetIsDisjoint, - "Basic data handling: SetContains": SetContains, "Basic data handling: SetLength": SetLength, + "Basic data handling: SetPop": SetPop, + "Basic data handling: SetRemove": SetRemove, + "Basic data handling: SetSymmetricDifference": SetSymmetricDifference, + "Basic data handling: SetUnion": SetUnion, "Basic data handling: SetToDataList": SetToDataList, "Basic data handling: SetToList": SetToList, } NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: SetAdd": "add", - "Basic data handling: SetRemove": "remove", - "Basic data handling: SetDiscard": "discard", - "Basic data handling: SetPop": "pop", - "Basic data handling: SetUnion": "union", - "Basic data handling: SetIntersection": "intersection", - "Basic data handling: SetDifference": "difference", - "Basic data handling: SetSymmetricDifference": "symmetric_difference", - "Basic data handling: SetIsSubset": "is_subset", - "Basic data handling: SetIsSuperset": "is_superset", - "Basic data handling: SetIsDisjoint": "is_disjoint", "Basic data handling: SetContains": "contains", + "Basic data handling: SetDifference": "difference", + "Basic data handling: SetDiscard": "discard", + "Basic data handling: SetIntersection": "intersection", + "Basic data handling: SetIsDisjoint": "is disjoint", + "Basic data handling: SetIsSubset": "is subset", + "Basic data handling: SetIsSuperset": "is superset", "Basic data handling: SetLength": "length", + "Basic data handling: SetPop": "pop", + "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: SetToList": "convert to LIST", } diff --git a/src/basic_data_handling/string_nodes.py b/src/basic_data_handling/string_nodes.py index 1ff636b..7e6ff42 100644 --- a/src/basic_data_handling/string_nodes.py +++ b/src/basic_data_handling/string_nodes.py @@ -314,32 +314,6 @@ class StringFind(ComfyNodeABC): return (string.find(substring, start, end),) -class StringIn(ComfyNodeABC): - """ - Checks if a string contains a specified substring. - - This node implements the 'in' operator for strings. It checks whether the given substring - exists within the input string. Returns True if the substring is found, - otherwise False. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {"default": "", "defaultInput": True}), - "substring": (IO.STRING, {"default": ""}), - } - } - - RETURN_TYPES = (IO.BOOLEAN,) - CATEGORY = "Basic/STRING" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "contains" - - def contains(self, string, substring): - return (substring in string,) - - class StringFormatMap(ComfyNodeABC): """ Formats a string using values from a dictionary. @@ -373,6 +347,32 @@ class StringFormatMap(ComfyNodeABC): return (f"Formatting error: {str(e)}",) +class StringIn(ComfyNodeABC): + """ + Checks if a string contains a specified substring. + + This node implements the 'in' operator for strings. It checks whether the given substring + exists within the input string. Returns True if the substring is found, + otherwise False. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {"default": "", "defaultInput": True}), + "substring": (IO.STRING, {"default": ""}), + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + CATEGORY = "Basic/STRING" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "contains" + + def contains(self, string, substring): + return (substring in string,) + + class StringIsAlnum(ComfyNodeABC): """ Checks if all characters in the string are alphanumeric. @@ -666,6 +666,85 @@ class StringIsUpper(ComfyNodeABC): return (string.isupper(),) +class StringLength(ComfyNodeABC): + """ + Returns the length of a string. + + This node calculates and returns the number of characters in the input string. + It works the same way as Python's built-in len() function for strings. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {"default": "", "defaultInput": True}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/STRING" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "length" + + def length(self, string): + return (len(string),) + + +class StringDataListJoin(ComfyNodeABC): + """ + Joins strings from a data list with a specified separator. + + This node takes a data list of strings and concatenates them, with the specified + separator string between each element. The separator is inserted between the strings, + not at the beginning or end of the result. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "strings": (IO.STRING, {"forceInput": True}), + "sep": (IO.STRING, {"default": " "}), + } + } + + RETURN_TYPES = (IO.STRING,) + CATEGORY = "Basic/STRING" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "join" + INPUT_IS_LIST = True # The "strings" input accepts a data list + + def join(self, sep, strings): + separator = sep[0] # everything comes as a list, so sep is list[str] + return (separator.join(strings),) + + +class StringListJoin(ComfyNodeABC): + """ + Joins strings from a LIST with a specified separator. + + This node takes a data list of strings and concatenates them, with the specified + separator string between each element. The separator is inserted between the strings, + not at the beginning or end of the result. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "strings": ("LIST", {"forceInput": True}), + "sep": (IO.STRING, {"default": " "}), + } + } + + RETURN_TYPES = (IO.STRING,) + CATEGORY = "Basic/STRING" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "join" + + def join(self, sep, strings): + separator = sep[0] # everything comes as a list, so sep is list[str] + return (separator.join(strings),) + + class StringLjust(ComfyNodeABC): """ Left-aligns the string within a field of a given width. @@ -697,6 +776,29 @@ class StringLjust(ComfyNodeABC): return (string.ljust(width, fillchar),) +class StringLower(ComfyNodeABC): + """ + Converts the string to lowercase. + + This node returns a copy of the string with all uppercase characters converted to lowercase. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {"default": "", "defaultInput": True}), + } + } + + RETURN_TYPES = (IO.STRING,) + CATEGORY = "Basic/STRING" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "lower" + + def lower(self, string): + return (string.lower(),) + + class StringLstrip(ComfyNodeABC): """ Removes leading characters from the string. @@ -777,6 +879,101 @@ class StringRemovesuffix(ComfyNodeABC): return (string.removesuffix(suffix),) +class StringReplace(ComfyNodeABC): + """ + Replaces occurrences of a substring with another substring. + + This node returns a copy of the string with all occurrences of substring + old replaced by new. If the optional argument count is given, only the + first count occurrences are replaced. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {"default": "", "defaultInput": True}), + "old": (IO.STRING, {"default": ""}), + "new": (IO.STRING, {"default": ""}), + }, + "optional": { + "count": (IO.INT, {"default": -1, "min": -1}), + } + } + + RETURN_TYPES = (IO.STRING,) + CATEGORY = "Basic/STRING" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "replace" + + def replace(self, string, old, new, count=-1): + return (string.replace(old, new, count),) + + +class StringRfind(ComfyNodeABC): + """ + Finds the highest index of the substring in the string. + + This node searches for the last occurrence of the specified substring within the input string. + Returns the highest index where the substring is found. If the substring is not found, it returns -1. + Optional start and end parameters allow you to limit the search to a specific portion of the string. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {"default": "", "defaultInput": True}), + "substring": (IO.STRING, {"default": ""}), + }, + "optional": { + "start": (IO.INT, {"default": 0, "min": 0}), + "end": (IO.INT, {"default": 0, "min": 0}), + } + } + + RETURN_TYPES = (IO.INT,) + CATEGORY = "Basic/STRING" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "rfind" + + def rfind(self, string, substring, start=0, end=0): + # If end is 0 or negative, search to the end of the string + if end <= 0: + end = len(string) + + return (string.rfind(substring, start, end),) + + +class StringRjust(ComfyNodeABC): + """ + Right-aligns the string within a field of a given width. + + This node returns a string right-aligned in a field of the specified width. + If fillchar is provided, it is used as the padding character instead of a space. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": (IO.STRING, {"default": "", "defaultInput": True}), + "width": (IO.INT, {"default": 10, "min": 0}), + }, + "optional": { + "fillchar": (IO.STRING, {"default": " "}), + } + } + + RETURN_TYPES = (IO.STRING,) + CATEGORY = "Basic/STRING" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "rjust" + + def rjust(self, string, width, fillchar=" "): + # Ensure fillchar is only one character + if len(fillchar) > 0: + fillchar = fillchar[0] + return (string.rjust(width, fillchar),) + + class StringRsplitDataList(ComfyNodeABC): """ Splits the string at the specified separator from the right. @@ -842,179 +1039,6 @@ class StringRsplitList(ComfyNodeABC): return (string.rsplit(sep, maxsplit),) -class StringLower(ComfyNodeABC): - """ - Converts the string to lowercase. - - This node returns a copy of the string with all uppercase characters converted to lowercase. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {"default": "", "defaultInput": True}), - } - } - - RETURN_TYPES = (IO.STRING,) - CATEGORY = "Basic/STRING" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "lower" - - def lower(self, string): - return (string.lower(),) - - -class StringReplace(ComfyNodeABC): - """ - Replaces occurrences of a substring with another substring. - - This node returns a copy of the string with all occurrences of substring - old replaced by new. If the optional argument count is given, only the - first count occurrences are replaced. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {"default": "", "defaultInput": True}), - "old": (IO.STRING, {"default": ""}), - "new": (IO.STRING, {"default": ""}), - }, - "optional": { - "count": (IO.INT, {"default": -1, "min": -1}), - } - } - - RETURN_TYPES = (IO.STRING,) - CATEGORY = "Basic/STRING" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "replace" - - def replace(self, string, old, new, count=-1): - return (string.replace(old, new, count),) - - -class StringRjust(ComfyNodeABC): - """ - Right-aligns the string within a field of a given width. - - This node returns a string right-aligned in a field of the specified width. - If fillchar is provided, it is used as the padding character instead of a space. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {"default": "", "defaultInput": True}), - "width": (IO.INT, {"default": 10, "min": 0}), - }, - "optional": { - "fillchar": (IO.STRING, {"default": " "}), - } - } - - RETURN_TYPES = (IO.STRING,) - CATEGORY = "Basic/STRING" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "rjust" - - def rjust(self, string, width, fillchar=" "): - # Ensure fillchar is only one character - if len(fillchar) > 0: - fillchar = fillchar[0] - return (string.rjust(width, fillchar),) - - -class StringDataListJoin(ComfyNodeABC): - """ - Joins strings from a data list with a specified separator. - - This node takes a data list of strings and concatenates them, with the specified - separator string between each element. The separator is inserted between the strings, - not at the beginning or end of the result. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "strings": (IO.STRING, {"forceInput": True}), - "sep": (IO.STRING, {"default": " "}), - } - } - - RETURN_TYPES = (IO.STRING,) - CATEGORY = "Basic/STRING" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "join" - INPUT_IS_LIST = True # The "strings" input accepts a data list - - def join(self, sep, strings): - separator = sep[0] # everything comes as a list, so sep is list[str] - return (separator.join(strings),) - - -class StringListJoin(ComfyNodeABC): - """ - Joins strings from a LIST with a specified separator. - - This node takes a data list of strings and concatenates them, with the specified - separator string between each element. The separator is inserted between the strings, - not at the beginning or end of the result. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "strings": ("LIST", {"forceInput": True}), - "sep": (IO.STRING, {"default": " "}), - } - } - - RETURN_TYPES = (IO.STRING,) - CATEGORY = "Basic/STRING" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "join" - - def join(self, sep, strings): - separator = sep[0] # everything comes as a list, so sep is list[str] - return (separator.join(strings),) - - -class StringRfind(ComfyNodeABC): - """ - Finds the highest index of the substring in the string. - - This node searches for the last occurrence of the specified substring within the input string. - Returns the highest index where the substring is found. If the substring is not found, it returns -1. - Optional start and end parameters allow you to limit the search to a specific portion of the string. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {"default": "", "defaultInput": True}), - "substring": (IO.STRING, {"default": ""}), - }, - "optional": { - "start": (IO.INT, {"default": 0, "min": 0}), - "end": (IO.INT, {"default": 0, "min": 0}), - } - } - - RETURN_TYPES = (IO.INT,) - CATEGORY = "Basic/STRING" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "rfind" - - def rfind(self, string, substring, start=0, end=0): - # If end is 0 or negative, search to the end of the string - if end <= 0: - end = len(string) - - return (string.rfind(substring, start, end),) - - class StringRstrip(ComfyNodeABC): """ Removes trailing characters from the string. @@ -1340,30 +1364,6 @@ class StringZfill(ComfyNodeABC): return (string.zfill(width),) -class StringLength(ComfyNodeABC): - """ - Returns the length of a string. - - This node calculates and returns the number of characters in the input string. - It works the same way as Python's built-in len() function for strings. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "string": (IO.STRING, {"default": "", "defaultInput": True}), - } - } - - RETURN_TYPES = (IO.INT,) - CATEGORY = "Basic/STRING" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "length" - - def length(self, string): - return (len(string),) - - NODE_CLASS_MAPPINGS = { "Basic data handling: StringCapitalize": StringCapitalize, "Basic data handling: StringCasefold": StringCasefold, @@ -1372,10 +1372,11 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: StringCount": StringCount, "Basic data handling: StringDecode": StringDecode, "Basic data handling: StringEncode": StringEncode, - "Basic data handling: StringExpandtabs": StringExpandtabs, "Basic data handling: StringEndswith": StringEndswith, + "Basic data handling: StringExpandtabs": StringExpandtabs, "Basic data handling: StringFind": StringFind, "Basic data handling: StringFormatMap": StringFormatMap, + "Basic data handling: StringIn": StringIn, "Basic data handling: StringIsAlnum": StringIsAlnum, "Basic data handling: StringIsAlpha": StringIsAlpha, "Basic data handling: StringIsAscii": StringIsAscii, @@ -1388,6 +1389,7 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: StringIsSpace": StringIsSpace, "Basic data handling: StringIsTitle": StringIsTitle, "Basic data handling: StringIsUpper": StringIsUpper, + "Basic data handling: StringLength": StringLength, "Basic data handling: StringDataListJoin": StringDataListJoin, "Basic data handling: StringListJoin": StringListJoin, "Basic data handling: StringLjust": StringLjust, @@ -1411,7 +1413,6 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: StringTitle": StringTitle, "Basic data handling: StringUpper": StringUpper, "Basic data handling: StringZfill": StringZfill, - "Basic data handling: StringLength": StringLength, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -1422,8 +1423,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: StringCount": "count", "Basic data handling: StringDecode": "decode", "Basic data handling: StringEncode": "encode", - "Basic data handling: StringExpandtabs": "expandtabs", "Basic data handling: StringEndswith": "endswith", + "Basic data handling: StringExpandtabs": "expandtabs", "Basic data handling: StringFind": "find", "Basic data handling: StringFormatMap": "format_map", "Basic data handling: StringIn": "in (contains)", @@ -1439,6 +1440,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: StringIsSpace": "isspace", "Basic data handling: StringIsTitle": "istitle", "Basic data handling: StringIsUpper": "isupper", + "Basic data handling: StringLength": "length", "Basic data handling: StringDataListJoin": "join (from data list)", "Basic data handling: StringListJoin": "join (from LIST)", "Basic data handling: StringLjust": "ljust", @@ -1462,5 +1464,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: StringTitle": "title", "Basic data handling: StringUpper": "upper", "Basic data handling: StringZfill": "zfill", - "Basic data handling: StringLength": "length", } diff --git a/web/js/example.js b/web/js/example.js deleted file mode 100644 index b2ebc8a..0000000 --- a/web/js/example.js +++ /dev/null @@ -1 +0,0 @@ -console.log(app);