diff --git a/README.md b/README.md index 0b06ccf..ae797e5 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,30 @@ These nodes are very lightweight and require no additional dependencies. - Case conversion: lower, upper, swapcase, title, capitalize - Validation: isalnum, isalpha, isdigit, isnumeric, etc. -- **LIST**: List manipulation nodes - - Creation: create, create_empty +- **LIST**: Python list manipulation nodes (as a single variable) + - Conversion: convert to data list + - Creation: any to LIST - Modification: append, extend, insert, remove, pop, clear, set_item - Access: get_item, slice, index, contains - Information: length, count - - Operations: sort, reverse, copy, join, zip, flatten - - Unpacking: unpack + - Operations: sort, reverse + +- **data list**: ComfyUI list manipulation nodes (for processing individual items) + - Conversion: convert to LIST + - Creation: create_empty + - Modification: append, extend, insert, remove, pop, clear, set_item + - Access: get_item, slice, index, contains + - Information: length, count + - Operations: sort, reverse, copy, zip + +- **DICT**: Dictionary manipulation nodes + - Creation: create, from_items, from_lists, fromkeys, any_to_DICT + - Access: get, get_multiple, keys, values, items + - Modification: set, update, setdefault, merge + - Removal: pop, popitem, remove, clear + - Information: length, contains_key + - Operations: copy, filter_by_keys, exclude_keys, invert, compare + - Conversion: get_keys_values - **INT**: Integer operation nodes - Basic operations: add, subtract, multiply, divide, modulus, power @@ -41,6 +58,48 @@ These nodes are very lightweight and require no additional dependencies. - **BOOLEAN**: Boolean logic nodes - Logic operations: and, or, not, xor, nand, nor +## Understanding LIST vs. data list + +ComfyUI has two different list data types that serve different purposes: + +### 1. LIST datatype +- A Python list represented as a **single variable** in the workflow +- Treated as a self-contained object that can be passed between nodes +- Cannot directly connect to nodes that expect individual items +- Best for: + - Working with collections of data as a single unit + - Storing intermediate results that need to be processed as a whole + - Passing collections between different parts of your workflow + - Complex data storage that shouldn't be split apart + +### 2. data list +- A native ComfyUI list where **items are processed individually** +- Acts like a standard array/list in most programming contexts +- Items can be accessed individually by compatible nodes +- Supports built-in ComfyUI iteration over each item +- Best for: + - Working directly with multiple items in parallel + - Batch processing scenarios + - When you need to apply the same operation to multiple inputs + - When your operation needs to work with individual items separately + +### Converting between types +- Use `convert to data list` node to transform a LIST into a ComfyUI data list +- Use `convert to LIST` node to transform a ComfyUI data list into a LIST object + +### When to use which type +- Use **LIST** when you need to: + - Keep items grouped together as a single entity + - Perform operations on the entire collection (sorting, reversing, etc.) + - Pass a collection between workflow sections + - Prevent automatic iteration over items + +- Use **data list** when you need to: + - Process each item individually through ComfyUI nodes + - Allow nodes to iterate through your items automatically + - Connect to nodes that expect individual inputs + - Perform batch processing operations + ## Tests This repo contains unit tests written in Pytest in the `tests/` directory. It is recommended to unit test your custom node. diff --git a/pyproject.toml b/pyproject.toml index b43d3c4..5c7d898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,10 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "0.0.4" +version = "0.0.5" description = """Basic Python functions for manipulating data that every programmer is used to. -Currently supported: BOOLEAN, FLOAT, INT, STRING and data lists.""" +Currently supported ComfyUI data types: BOOLEAN, FLOAT, INT, STRING and data lists. +Additionally supported Python data types as custom data types: DICT, LIST.""" authors = [ {name = "Basic data handling"} ] diff --git a/src/basic_data_handling/__init__.py b/src/basic_data_handling/__init__.py index da7ebf2..5bc6e53 100644 --- a/src/basic_data_handling/__init__.py +++ b/src/basic_data_handling/__init__.py @@ -1,7 +1,8 @@ -from . import boolean_nodes, dict_nodes, float_nodes, int_nodes, list_nodes, string_nodes +from . import boolean_nodes, data_list_nodes, dict_nodes, float_nodes, int_nodes, list_nodes, string_nodes NODE_CLASS_MAPPINGS = {} NODE_CLASS_MAPPINGS.update(boolean_nodes.NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(data_list_nodes.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(dict_nodes.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(float_nodes.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(int_nodes.NODE_CLASS_MAPPINGS) @@ -10,6 +11,7 @@ NODE_CLASS_MAPPINGS.update(string_nodes.NODE_CLASS_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS.update(boolean_nodes.NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(data_list_nodes.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(dict_nodes.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(float_nodes.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(int_nodes.NODE_DISPLAY_NAME_MAPPINGS) diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py new file mode 100644 index 0000000..d5f3cc6 --- /dev/null +++ b/src/basic_data_handling/data_list_nodes.py @@ -0,0 +1,722 @@ +from typing import Any + +from inspect import cleandoc + +class DataListConvertToList: + """ + Converts a ComfyUI data list into a LIST object. + + This node takes a data list input (which is typically a list of items with the same type) + and converts it to a LIST object (a Python list as a single variable). + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("*", {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "convert" + INPUT_IS_LIST = True + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def convert(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + return (kwargs.get('list', []).copy(),) + + +class DataListAppend: + """ + Adds an item to the end of a list. + + This node takes a list and any item as inputs, then returns the modified + list with the new item appended. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "list":("*",{}), + "item":("*",{}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "append" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def append(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + result = kwargs.get('list', []).copy() + item = kwargs.get('item', []) + if len(item) > 0: + result.append(item[0]) + return (result,) + + +class DataListExtend: + """ + Extends a list by appending elements from another list. + + This node takes two lists as input and returns a new list that contains + all elements from both lists. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "list_a": ("*",{}), + "list_b": ("*",{}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "extend" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def extend(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + return (kwargs.get('list_a', []) + kwargs.get('list_b', []),) + + +class DataListInsert: + """ + Inserts 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. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("*",), + "index": ("INT", {"default": 0}), + "item": ("*",), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "insert" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def insert(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + result = kwargs.get('list', []).copy() + result.insert(kwargs.get('index', [0])[0], kwargs.get('item', [None])[0]) + return (result,) + + +class DataListRemove: + """ + 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": ("*",), + "value": ("*",), + } + } + + RETURN_TYPES = ("*", "BOOLEAN",) + RETURN_NAMES = ("list", "success",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "remove" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True, False,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def remove(self, **kwargs: dict[str, list]) -> 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 DataListPop: + """ + 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": ("*",), + }, + "optional": { + "index": ("INT", {"default": -1}), + } + } + + RETURN_TYPES = ("*", "*") + RETURN_NAMES = ("list", "item") + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True, False) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str: + if input_types["index"] not in ("INT"): + return "index must be an INT type" + return True + + def pop(self, **kwargs: dict[str, list]) -> 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 DataListClear: + """ + Removes all items from a list. + + This node takes a list as input and returns a new empty list. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "optional": { + "list": ("*",), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "clear" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def clear(self, **kwargs: dict[str, list]) -> tuple[[]]: + # Return a new empty list rather than modifying the input + return ([],) + + +class DataListIndex: + """ + Returns the index of the first occurrence of a value in a list. + + This node takes a list and a value as inputs, then returns the index of the first + occurrence of the value. Optional start and end parameters limit the search to a slice + of the list. Returns -1 if the value is not present. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("*",), + "value": ("*",), + }, + "optional": { + "start": ("INT", {"default": 0}), + "end": ("INT", {"default": -1}), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("index",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "list_index" + INPUT_IS_LIST = True + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def list_index(self, **kwargs: dict[str, list]) -> tuple[int]: + input_list = kwargs.get('list', []) + value = kwargs.get('value', [None])[0] + start = kwargs.get('start', [0])[0] + end = kwargs.get('end', [-1])[0] + if end == -1: + end = len(input_list) + + try: + return (input_list.index(value, start, end),) + except ValueError: + return (-1,) + + +class DataListCount: + """ + 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": ("*",), + "value": ("*",), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("count",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "count" + INPUT_IS_LIST = True + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def count(self, **kwargs: dict[str, list]) -> tuple[int]: + value = kwargs.get('value', [None])[0] + return (kwargs.get('list', []).count(value),) + + +class DataListSort: + """ + 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": ("*",), + }, + "optional": { + "reverse": (["False", "True"], {"default": "False"}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "sort" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def sort(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + # Convert string to boolean + reverse = kwargs.get('reverse', ["False"])[0] == "True" + + result = sorted(kwargs.get('list', []), reverse=reverse) + return (result,) + + +class DataListReverse: + """ + 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": ("*",), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "reverse" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def reverse(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + result = kwargs.get('list', []).copy() + result.reverse() + return (result,) + + +class DataListCopy: + """ + Creates a shallow copy of a list. + + This node takes a list as input and returns a new list that is a shallow copy of the original. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("*",), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "copy" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def copy(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + return (kwargs.get('list', []).copy(),) + + +class DataListLength: + """ + 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": ("*",), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("length",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "length" + INPUT_IS_LIST = True + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def length(self, **kwargs: dict[str, list]) -> tuple[int]: + return (len(kwargs.get('list', [])),) + + +class DataListSlice: + """ + 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": ("*",), + }, + "optional": { + "start": ("INT", {"default": 0}), + "stop": ("INT", {"default": -1}), + "step": ("INT", {"default": 1}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "slice" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def slice(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + input_list = kwargs.get('list', []) + start = kwargs.get('start', [0])[0] + stop = kwargs.get('stop', [-1])[0] + step = kwargs.get('step', [1])[0] + + if stop == -1: + stop = len(input_list) + + return (input_list[start:stop:step],) + + +class DataListGetItem: + """ + 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": { + "input_list": ("*",), + "index": ("INT", {"default": 0}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("item",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_item" + INPUT_IS_LIST = True + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def get_item(self, **kwargs: dict[str, list]) -> tuple[Any]: + index = kwargs.get('index', [0])[0] + try: + return (kwargs.get('list', [])[index],) + except IndexError: + return (None,) + + +class DataListSetItem: + """ + 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": ("*",), + "index": ("INT", {"default": 0}), + "value": ("*",), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "set_item" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def set_item(self, **kwargs: dict[str, list]) -> 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 DataListContains: + """ + 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": { + "input_list": ("*",), + "value": ("*",), + } + } + + RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("contains",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "contains" + INPUT_IS_LIST = True + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def contains(self, **kwargs: dict[str, list]) -> tuple[bool]: + value = kwargs.get('value', []) + if len(value) == 0: + return (False,) + return (value[0] in kwargs.get('list', []),) + + +class DataListCreateEmpty: + """ + Creates a new empty list. + + This node creates and returns an empty list. + """ + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create_empty" + OUTPUT_IS_LIST = (True,) + + def create_empty(self) -> tuple[[]]: + return ([],) + + +class DataListZip: + """ + Combines multiple lists element-wise. + + This node takes multiple data lists as input and returns a new data list + where each item is a list containing the corresponding elements from the input lists. + The length of the output list will be equal to the length of the shortest input list. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list1": ("*",), + "list2": ("*",), + }, + "optional": { + "list3": ("*",), + "list4": ("*",), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/data list" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "zip_lists" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + @classmethod + def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: + return True + + def zip_lists(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + lists = [kwargs.get('list1', []), kwargs.get('list2', [])] + + if 'list3' in kwargs: + lists.append(kwargs['list3']) + + if 'list4' in kwargs: + lists.append(kwargs['list4']) + + # Zip the lists together and convert each tuple to a list + result = [list(item) for item in zip(*lists)] + return (result,) + + +NODE_CLASS_MAPPINGS = { + "DataListConvertToList": DataListConvertToList, + "DataListAppend": DataListAppend, + "DataListExtend": DataListExtend, + "DataListInsert": DataListInsert, + "DataListRemove": DataListRemove, + "DataListPop": DataListPop, + "DataListClear": DataListClear, + "DataListIndex": DataListIndex, + "DataListCount": DataListCount, + "DataListSort": DataListSort, + "DataListReverse": DataListReverse, + "DataListCopy": DataListCopy, + "DataListLength": DataListLength, + "DataListSlice": DataListSlice, + "DataListGetItem": DataListGetItem, + "DataListSetItem": DataListSetItem, + "DataListContains": DataListContains, + "DataListCreateEmpty": DataListCreateEmpty, + "DataListZip": DataListZip, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DataListConvertToList": "convert to LIST", + "DataListAppend": "append", + "DataListExtend": "extend", + "DataListInsert": "insert", + "DataListRemove": "remove", + "DataListPop": "pop", + "DataListClear": "clear", + "DataListIndex": "index", + "DataListCount": "count", + "DataListSort": "sort", + "DataListReverse": "reverse", + "DataListCopy": "copy", + "DataListLength": "length", + "DataListSlice": "slice", + "DataListGetItem": "get_item", + "DataListSetItem": "set_item", + "DataListContains": "contains", + "DataListCreateEmpty": "create_empty", + "DataListZip": "zip", +} diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index eabd2c3..63fc7f9 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -1,27 +1,85 @@ +from typing import Any from inspect import cleandoc -class DictGet: +class DictCreate: """ - Retrieves a value from a dictionary using the specified key. + Creates a new empty dictionary. - If the key is not found, the node will return None. + This node creates and returns a new empty dictionary object. + """ + @classmethod + def INPUT_TYPES(cls): + return {"required": {}} + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "create" + + def create(self) -> tuple[dict]: + return ({},) + + +class DictCreateFromItems: + """ + 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": { - "input_dict": ("DICT",), - "key": ("STRING", {"default": ""}), + "items": ("LIST", {}), } } - RETURN_TYPES = ("ANY",) + 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: + if not isinstance(item, tuple) or 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 DictGet: + """ + Retrieves a value from a dictionary using the specified key. + + This node gets the value for the specified key from a dictionary. + If the key is not found and a default value is provided, that default is returned. + Otherwise, it returns None. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + "key": ("STRING", {"default": ""}), + }, + "optional": { + "default_value": ("*", {}), + } + } + + RETURN_TYPES = ("*",) + RETURN_NAMES = ("value",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get" - def get(self, input_dict, key): - return (input_dict.get(key, None),) + def get(self, input_dict: dict, key: str, default_value=None) -> tuple[Any]: + return (input_dict.get(key, default_value),) + class DictSet: """ @@ -34,9 +92,9 @@ class DictSet: def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT",), + "input_dict": ("DICT", {}), "key": ("STRING", {"default": ""}), - "value": ("ANY",), + "value": ("*", {}), } } @@ -45,16 +103,729 @@ class DictSet: DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "set" - def set(self, input_dict, key, value): - input_dict[key] = value - return (input_dict,) + def set(self, input_dict: dict, key: str, value: Any) -> tuple[dict]: + result = input_dict.copy() + result[key] = value + return (result,) + + +class DictKeys: + """ + Returns all keys in a dictionary. + + This node takes a dictionary and returns a list containing all of its keys. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "keys" + + def keys(self, input_dict: dict) -> tuple[list]: + return (list(input_dict.keys()),) + + +class DictValues: + """ + Returns all values in a dictionary. + + This node takes a dictionary and returns a list containing all of its values. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "values" + + def values(self, input_dict: dict) -> tuple[list]: + return (list(input_dict.values()),) + + +class DictItems: + """ + Returns all key-value pairs in a dictionary. + + This node takes a dictionary and returns a list of tuples, where each tuple + contains a key-value pair from the dictionary. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "items" + + def items(self, input_dict: dict) -> tuple[list]: + return (list(input_dict.items()),) + + +class DictContainsKey: + """ + 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": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES = ("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 DictClear: + """ + Removes all items from a dictionary. + + This node takes a dictionary as input and returns a new empty dictionary. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "clear" + + def clear(self, input_dict: dict) -> tuple[dict]: + return ({},) + + +class DictCopy: + """ + Creates a shallow copy of a dictionary. + + This node takes a dictionary as input and returns a new dictionary that is + a shallow copy of the original. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "copy" + + def copy(self, input_dict: dict) -> tuple[dict]: + return (input_dict.copy(),) + + +class DictFromKeys: + """ + 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": ("*", {}), + } + } + + 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 DictPop: + """ + Removes and returns a key-value pair from a dictionary. + + This node takes a dictionary and a key as inputs, removes the specified key + from the dictionary, and returns both the modified dictionary and the value + associated with the key. If the key is not found and a default value is provided, + that default is returned. Otherwise, an error is raised. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + "key": ("STRING", {"default": ""}), + }, + "optional": { + "default_value": ("*", {}), + "has_default": (["False", "True"], {"default": "False"}), + } + } + + RETURN_TYPES = ("DICT", "*", "BOOLEAN") + RETURN_NAMES = ("dict", "value", "key_found") + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop" + + def pop(self, input_dict: dict, key: str, default_value=None, has_default: str = "False") -> tuple[dict, Any, bool]: + result = input_dict.copy() + has_default_bool = (has_default == "True") + + try: + if key in result: + value = result.pop(key) + return result, value, True + elif has_default_bool: + return result, default_value, False + else: + raise KeyError(f"Key '{key}' not found in dictionary") + except Exception as e: + raise ValueError(f"Error popping key from dictionary: {str(e)}") + + +class DictPopItem: + """ + Removes and returns an arbitrary key-value pair from a dictionary. + + This node takes a dictionary as input, removes an arbitrary key-value pair, + and returns the modified dictionary along with the removed key and value. + If the dictionary is empty, returns an error. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + } + } + + RETURN_TYPES = ("DICT", "STRING", "*", "BOOLEAN") + RETURN_NAMES = ("dict", "key", "value", "success") + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "popitem" + + def popitem(self, input_dict: dict) -> tuple[dict, str, Any, bool]: + result = input_dict.copy() + try: + if result: + key, value = result.popitem() + return result, key, value, True + else: + return result, "", None, False + except Exception as e: + return result, "", None, False + + +class DictSetDefault: + """ + Returns the value for a key, setting a default if the key doesn't exist. + + This node takes a dictionary, a key, and a default value. If the key exists + in the dictionary, the corresponding value is returned. If the key doesn't + exist, the default value is inserted for the key and returned. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + "key": ("STRING", {"default": ""}), + "default_value": ("*", {}), + } + } + + RETURN_TYPES = ("DICT", "*") + RETURN_NAMES = ("dict", "value") + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "setdefault" + + def setdefault(self, input_dict: dict, key: str, default_value=None) -> tuple[dict, Any]: + result = input_dict.copy() + value = result.setdefault(key, default_value) + return result, value + + +class DictUpdate: + """ + Updates a dictionary with key-value pairs from another dictionary. + + This node takes two dictionaries as inputs and returns a new dictionary that + contains all key-value pairs from both dictionaries. If there are duplicate + keys, the values from the second dictionary take precedence. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "dict1": ("DICT", {}), + "dict2": ("DICT", {}), + } + } + + RETURN_TYPES = ("DICT",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "update" + + def update(self, dict1: dict, dict2: dict) -> tuple[dict]: + result = dict1.copy() + result.update(dict2) + return (result,) + + +class DictLength: + """ + Returns the number of key-value pairs in a dictionary. + + This node takes a dictionary as input and returns its length (number of items). + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "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: + """ + 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: + """ + 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: + """ + 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": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES = ("DICT", "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 AnyToDict: + """ + Converts compatible data structures to a dictionary. + + This node attempts to convert an input value to a dictionary. Compatible + inputs include sequences of key-value pairs, mapping objects, and objects + with a to_dict() or as_dict() method. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input": ("*", {}), + } + } + + RETURN_TYPES = ("DICT", "BOOLEAN") + RETURN_NAMES = ("dict", "success") + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "convert" + + def convert(self, input: Any) -> tuple[dict, bool]: + try: + if isinstance(input, dict): + return input.copy(), True + + # Try converting from items + if hasattr(input, "items"): + return dict(input.items()), True + + # Try converting from sequence of pairs + if hasattr(input, "__iter__") and not isinstance(input, str): + try: + result = dict(input) + return result, True + except (TypeError, ValueError): + pass + + # Check for to_dict or as_dict methods + if hasattr(input, "to_dict") and callable(getattr(input, "to_dict")): + return input.to_dict(), True + + if hasattr(input, "as_dict") and callable(getattr(input, "as_dict")): + return input.as_dict(), True + + # Failed to convert + return {}, False + + except Exception: + return {}, False + + +class DictFilterByKeys: + """ + 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: + """ + 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: + """ + 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_value": ("*", {}), + } + } + + RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("values",) + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_multiple" + + def get_multiple(self, input_dict: dict, keys: list, default_value=None) -> tuple[list]: + values = [input_dict.get(key, default_value) for key in keys] + return (values,) + + +class DictInvert: + """ + 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", "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: + """ + 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: + """ + 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 = ("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 + NODE_CLASS_MAPPINGS = { + "DictCreate": DictCreate, + "DictCreateFromItems": DictCreateFromItems, "DictGet": DictGet, "DictSet": DictSet, + "DictKeys": DictKeys, + "DictValues": DictValues, + "DictItems": DictItems, + "DictContainsKey": DictContainsKey, + "DictClear": DictClear, + "DictCopy": DictCopy, + "DictFromKeys": DictFromKeys, + "DictPop": DictPop, + "DictPopItem": DictPopItem, + "DictSetDefault": DictSetDefault, + "DictUpdate": DictUpdate, + "DictLength": DictLength, + "DictMerge": DictMerge, + "DictGetKeysValues": DictGetKeysValues, + "DictRemove": DictRemove, + "AnyToDict": AnyToDict, + "DictFilterByKeys": DictFilterByKeys, + "DictExcludeKeys": DictExcludeKeys, + "DictGetMultiple": DictGetMultiple, + "DictInvert": DictInvert, + "DictCreateFromLists": DictCreateFromLists, + "DictCompare": DictCompare, } NODE_DISPLAY_NAME_MAPPINGS = { + "DictCreate": "create", + "DictCreateFromItems": "from_items", "DictGet": "get", "DictSet": "set", + "DictKeys": "keys", + "DictValues": "values", + "DictItems": "items", + "DictContainsKey": "contains_key", + "DictClear": "clear", + "DictCopy": "copy", + "DictFromKeys": "fromkeys", + "DictPop": "pop", + "DictPopItem": "popitem", + "DictSetDefault": "setdefault", + "DictUpdate": "update", + "DictLength": "length", + "DictMerge": "merge", + "DictGetKeysValues": "get_keys_values", + "DictRemove": "remove", + "AnyToDict": "any_to_DICT", + "DictFilterByKeys": "filter_by_keys", + "DictExcludeKeys": "exclude_keys", + "DictGetMultiple": "get_multiple", + "DictInvert": "invert", + "DictCreateFromLists": "from_lists", + "DictCompare": "compare", } diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 8542f52..9d21e6c 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -1,143 +1,141 @@ from typing import Any - from inspect import cleandoc -class ListAppend: +class ListConvertToDataList: """ - Adds an item to the end of a list. + Converts a LIST object into a ComfyUI data list. - This node takes a list and any item as inputs, then returns the modified - list with the new item appended. + This node takes a LIST object (Python list as a single variable) and + converts it to a ComfyUI data list, allowing its items to be processed + individually by nodes that accept data lists. """ @classmethod def INPUT_TYPES(cls): return { - "optional": { - "list":("*",{}), - "item":("*",{}), + "required": { + "list": ("LIST", {}), } } RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "append" - INPUT_IS_LIST = True + FUNCTION = "convert" OUTPUT_IS_LIST = (True,) - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True + def convert(self, list) -> tuple[list[Any]]: + return (list,) - def append(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: - result = kwargs.get('list', []).copy() - item = kwargs.get('item', []) - if len(item) > 0: - result.append(item[0]) + +class ListAppend: + """ + Adds an item to the end of a LIST. + + This node takes a LIST and any item as inputs, then returns a new LIST + with the item appended to the end. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + "item": ("*", {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "append" + + def append(self, list: list[Any], item: Any) -> tuple[list[Any]]: + result = list.copy() + result.append(item) return (result,) class ListExtend: """ - Extends a list by appending elements from another list. + Extends a LIST by appending elements from another LIST. - This node takes two lists as input and returns a new list that contains + This node takes two LIST objects as input and returns a new LIST that contains all elements from both lists. """ @classmethod def INPUT_TYPES(cls): return { - "optional": { - "list_a": ("*",{}), - "list_b": ("*",{}), + "required": { + "list1": ("LIST", {}), + "list2": ("LIST", {}), } } - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "extend" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def extend(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: - return (kwargs.get('list_a', []) + kwargs.get('list_b', []),) + def extend(self, list1: list[Any], list2: list[Any]) -> tuple[list[Any]]: + result = list1.copy() + result.extend(list2) + return (result,) class ListInsert: """ - Inserts an item at a specified position in a list. + Inserts 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, 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": ("LIST", {}), "index": ("INT", {"default": 0}), - "item": ("*",), + "item": ("*", {}), } } - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "insert" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def insert(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: - result = kwargs.get('list', []).copy() - result.insert(kwargs.get('index', [0])[0], kwargs.get('item', [None])[0]) + def insert(self, list: list[Any], index: int, item: Any) -> tuple[list[Any]]: + result = list.copy() + result.insert(index, item) return (result,) class ListRemove: """ - Removes the first occurrence of a specified value from a list. + 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. + 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": ("*",), - "value": ("*",), + "list": ("LIST", {}), + "value": ("*", {}), } } - RETURN_TYPES = ("*", "BOOLEAN",) - RETURN_NAMES = ("list", "success",) - CATEGORY = "Basic/list" + RETURN_TYPES = ("LIST", "BOOLEAN") + RETURN_NAMES = ("list", "success") + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "remove" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True, False,) - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def remove(self, **kwargs: dict[str, list]) -> tuple[list[Any], bool]: - result = kwargs.get('list', []).copy() - value = kwargs.get('value', []) + def remove(self, list: list[Any], value: Any) -> tuple[list[Any], bool]: + result = list.copy() try: - result.remove(value[0]) + result.remove(value) return result, True except ValueError: return result, False @@ -145,41 +143,32 @@ class ListRemove: class ListPop: """ - Removes and returns an item at a specified position in a list. + 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 + 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. + When the LIST is empty, the item is None. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "list": ("*",), + "list": ("LIST", {}), }, "optional": { "index": ("INT", {"default": -1}), } } - RETURN_TYPES = ("*", "*") + RETURN_TYPES = ("LIST", "*") RETURN_NAMES = ("list", "item") - CATEGORY = "Basic/list" + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True, False) - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - if input_types["index"] not in ("INT"): - return "index must be an INT type" - return True - - def pop(self, **kwargs: dict[str, list]) -> tuple[list[Any], Any]: - result = kwargs.get('list', []).copy() - index = kwargs.get('index', [-1])[0] + def pop(self, list: list[Any], index: int = -1) -> tuple[list[Any], Any]: + result = list.copy() try: item = result.pop(index) return result, item @@ -189,49 +178,41 @@ class ListPop: class ListClear: """ - Removes all items from a list. + Removes all items from a LIST. - This node takes a list as input and returns a new empty list. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "optional": { - "list": ("*",), - } - } - - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "clear" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def clear(self, **kwargs: dict[str, list]) -> tuple[[]]: - # Return a new empty list rather than modifying the input - return ([],) - - -class ListIndex: - """ - Returns the index of the first occurrence of a value in a list. - - This node takes a list and a value as inputs, then returns the index of the first - occurrence of the value. Optional start and end parameters limit the search to a slice - of the list. Returns -1 if the value is not present. + This node takes a LIST as input and returns a new empty LIST. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "list": ("*",), - "value": ("*",), + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "clear" + + def clear(self, list: list[Any]) -> tuple[list[Any]]: + return ([],) + + +class ListIndex: + """ + Returns the index of the first occurrence of a value in a LIST. + + This node takes a LIST and a value as inputs, then returns the index of the first + occurrence of the value. Optional start and end parameters limit the search to a slice + of the LIST. Returns -1 if the value is not present. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + "value": ("*", {}), }, "optional": { "start": ("INT", {"default": 0}), @@ -241,202 +222,143 @@ class ListIndex: RETURN_TYPES = ("INT",) RETURN_NAMES = ("index",) - CATEGORY = "Basic/list" + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "list_index" - INPUT_IS_LIST = True + FUNCTION = "index" - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def list_index(self, **kwargs: dict[str, list]) -> tuple[int]: - input_list = kwargs.get('list', []) - value = kwargs.get('value', [None])[0] - start = kwargs.get('start', [0])[0] - end = kwargs.get('end', [-1])[0] + def index(self, list: list[Any], value: Any, start: int = 0, end: int = -1) -> tuple[int]: if end == -1: - end = len(input_list) + end = len(list) try: - return (input_list.index(value, start, end),) + return (list.index(value, start, end),) except ValueError: return (-1,) class ListCount: """ - Counts the number of occurrences of a value in a list. + 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. + 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": ("*",), - "value": ("*",), + "list": ("LIST", {}), + "value": ("*", {}), } } RETURN_TYPES = ("INT",) RETURN_NAMES = ("count",) - CATEGORY = "Basic/list" + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "count" - INPUT_IS_LIST = True - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def count(self, **kwargs: dict[str, list]) -> tuple[int]: - value = kwargs.get('value', [None])[0] - return (kwargs.get('list', []).count(value),) + def count(self, list: list[Any], value: Any) -> tuple[int]: + return (list.count(value),) class ListSort: """ - Sorts the items in a list. + 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. + This node takes a LIST as input and returns a new sorted LIST. + Option includes sorting in reverse order. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "list": ("*",), + "list": ("LIST", {}), }, "optional": { "reverse": (["False", "True"], {"default": "False"}), } } - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "sort" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def sort(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: + def sort(self, list: list[Any], reverse: str = "False") -> tuple[list[Any]]: # Convert string to boolean - reverse = kwargs.get('reverse', ["False"])[0] == "True" + reverse_bool = (reverse == "True") - result = sorted(kwargs.get('list', []), reverse=reverse) - return (result,) + # Use sorted to create a new sorted list + try: + result = sorted(list, reverse=reverse_bool) + return (result,) + except TypeError: + # If list contains mixed types that can't be compared, return original list + return (list.copy(),) class ListReverse: """ - Reverses the order of items in a list. + 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. + 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": ("LIST", {}), } } - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "reverse" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def reverse(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: - result = kwargs.get('list', []).copy() + def reverse(self, list: list[Any]) -> tuple[list[Any]]: + result = list.copy() result.reverse() return (result,) -class ListCopy: - """ - Creates a shallow copy of a list. - - This node takes a list as input and returns a new list that is a shallow copy of the original. - """ - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "list": ("*",), - } - } - - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "copy" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def copy(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: - return (kwargs.get('list', []).copy(),) - - class ListLength: """ - Counts the number of items in a list. + Returns the number of items in a LIST. - This node takes a list as input and returns its length as an integer. + This node takes a LIST as input and returns its length as an integer. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "list": ("*",), + "list": ("LIST", {}), } } RETURN_TYPES = ("INT",) RETURN_NAMES = ("length",) - CATEGORY = "Basic/list" + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "length" - INPUT_IS_LIST = True - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def length(self, **kwargs: dict[str, list]) -> tuple[int]: - return (len(kwargs.get('list', [])),) + def length(self, list: list[Any]) -> tuple[int]: + return (len(list),) class ListSlice: """ - Creates a slice of a list. + 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. + 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": ("LIST", {}), }, "optional": { "start": ("INT", {"default": 0}), @@ -445,210 +367,132 @@ class ListSlice: } } - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "slice" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def slice(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: - input_list = kwargs.get('list', []) - start = kwargs.get('start', [0])[0] - stop = kwargs.get('stop', [-1])[0] - step = kwargs.get('step', [1])[0] + def slice(self, list: list[Any], start: int = 0, stop: int = -1, step: int = 1) -> tuple[list[Any]]: if stop == -1: - stop = len(input_list) + stop = len(list) - return (input_list[start:stop:step],) + return (list[start:stop:step],) class ListGetItem: """ - Retrieves an item at a specified position in a list. + 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. + 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": { - "input_list": ("*",), + "list": ("LIST", {}), "index": ("INT", {"default": 0}), } } RETURN_TYPES = ("*",) RETURN_NAMES = ("item",) - CATEGORY = "Basic/list" + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_item" - INPUT_IS_LIST = True - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def get_item(self, **kwargs: dict[str, list]) -> tuple[Any]: - index = kwargs.get('index', [0])[0] + def get_item(self, list: list[Any], index: int) -> tuple[Any]: try: - return (kwargs.get('list', [])[index],) + return (list[index],) except IndexError: return (None,) class ListSetItem: """ - Sets an item at a specified position in a list. + 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 + 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": ("LIST", {}), "index": ("INT", {"default": 0}), - "value": ("*",), + "value": ("*", {}), } } - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "set_item" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def set_item(self, **kwargs: dict[str, list]) -> tuple[Any]: - input_list = kwargs.get('list', []) - index = kwargs.get('index', [0])[0] - value = kwargs.get('value', [None])[0] + def set_item(self, list: list[Any], index: int, value: Any) -> tuple[list[Any]]: + result = list.copy() 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)}") + raise IndexError(f"Index {index} out of range for LIST of length {len(list)}") class ListContains: """ - Checks if a list contains a specified value. + 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. + 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": { - "input_list": ("*",), - "value": ("*",), + "list": ("LIST", {}), + "value": ("*", {}), } } RETURN_TYPES = ("BOOLEAN",) RETURN_NAMES = ("contains",) - CATEGORY = "Basic/list" + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "contains" - INPUT_IS_LIST = True - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def contains(self, **kwargs: dict[str, list]) -> tuple[bool]: - value = kwargs.get('value', []) - if len(value) == 0: - return (False,) - return (value[0] in kwargs.get('list', []),) + def contains(self, list: list[Any], value: Any) -> tuple[bool]: + return (value in list,) -class ListCreateEmpty: +class AnyToList: """ - Creates a new empty list. + Converts any input datatype into a LIST. - This node creates and returns an empty list. - """ - @classmethod - def INPUT_TYPES(cls): - return {"required": {}} - - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "create_empty" - OUTPUT_IS_LIST = (True,) - - def create_empty(self) -> tuple[[]]: - return ([],) - - -class ListZip: - """ - Combines multiple lists element-wise. - - This node takes multiple data lists as input and returns a new data list - where each item is a list containing the corresponding elements from the input lists. - The length of the output list will be equal to the length of the shortest input list. + This node takes any input value and wraps it in a LIST object, + allowing it to be processed by nodes that expect LIST inputs. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "list1": ("*",), - "list2": ("*",), - }, - "optional": { - "list3": ("*",), - "list4": ("*",), + "input": ("*", {}), } } - RETURN_TYPES = ("*",) - RETURN_NAMES = ("list",) - CATEGORY = "Basic/list" + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "zip_lists" - INPUT_IS_LIST = True - OUTPUT_IS_LIST = (True,) + FUNCTION = "convert" - @classmethod - def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool: - return True - - def zip_lists(self, **kwargs: dict[str, list]) -> tuple[list[Any]]: - lists = [kwargs.get('list1', []), kwargs.get('list2', [])] - - if 'list3' in kwargs: - lists.append(kwargs['list3']) - - if 'list4' in kwargs: - lists.append(kwargs['list4']) - - # Zip the lists together and convert each tuple to a list - result = [list(item) for item in zip(*lists)] - return (result,) + def convert(self, input: Any) -> tuple[list[Any]]: + # Create a new list containing the input value + return ([input],) NODE_CLASS_MAPPINGS = { + "ListConvertToDataList": ListConvertToDataList, "ListAppend": ListAppend, "ListExtend": ListExtend, "ListInsert": ListInsert, @@ -659,17 +503,16 @@ NODE_CLASS_MAPPINGS = { "ListCount": ListCount, "ListSort": ListSort, "ListReverse": ListReverse, - "ListCopy": ListCopy, "ListLength": ListLength, "ListSlice": ListSlice, "ListGetItem": ListGetItem, "ListSetItem": ListSetItem, "ListContains": ListContains, - "ListCreateEmpty": ListCreateEmpty, - "ListZip": ListZip, + "AnyToList": AnyToList, } NODE_DISPLAY_NAME_MAPPINGS = { + "ListConvertToDataList": "convert to data list", "ListAppend": "append", "ListExtend": "extend", "ListInsert": "insert", @@ -680,12 +523,10 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ListCount": "count", "ListSort": "sort", "ListReverse": "reverse", - "ListCopy": "copy", "ListLength": "length", "ListSlice": "slice", - "ListGetItem": "get_item", - "ListSetItem": "set_item", + "ListGetItem": "get item", + "ListSetItem": "set item", "ListContains": "contains", - "ListCreateEmpty": "create_empty", - "ListZip": "zip", + "AnyToList": "any to LIST", }