diff --git a/README.md b/README.md index ae797e5..764d9b3 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,14 @@ These nodes are very lightweight and require no additional dependencies. - Information: length, count - Operations: sort, reverse +- **SET**: Python set manipulation nodes (as a single variable) + - Conversion: convert to data list, to LIST + - Creation: any to SET, LIST to SET + - Modification: add, remove, discard, pop, clear + - Information: length, contains + - Set operations: union, intersection, difference, symmetric_difference + - Comparison: is_subset, is_superset, is_disjoint + - **data list**: ComfyUI list manipulation nodes (for processing individual items) - Conversion: convert to LIST - Creation: create_empty @@ -58,9 +66,9 @@ 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 +## Understanding LIST vs. data list vs. SET -ComfyUI has two different list data types that serve different purposes: +ComfyUI has different data types that serve different purposes: ### 1. LIST datatype - A Python list represented as a **single variable** in the workflow @@ -72,7 +80,17 @@ ComfyUI has two different list data types that serve different purposes: - Passing collections between different parts of your workflow - Complex data storage that shouldn't be split apart -### 2. data list +### 2. SET datatype +- A Python set represented as a **single variable** in the workflow +- Stores unique values with no duplicates +- Supports mathematical set operations (union, intersection, etc.) +- Best for: + - Eliminating duplicate values + - Testing membership efficiently + - Set operations (union, difference, etc.) + - When element order doesn't matter + +### 3. 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 @@ -84,15 +102,23 @@ ComfyUI has two different list data types that serve different purposes: - 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 data list` node to transform a LIST or SET into a ComfyUI data list - Use `convert to LIST` node to transform a ComfyUI data list into a LIST object +- Use `LIST to SET` to convert a LIST to a SET (removing duplicates) +- Use `SET to LIST` to convert a SET to a LIST ### 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 **LIST** when you need: + - Ordered collection with potential duplicates + - To preserve insertion order + - To access elements by position/index + - To use methods like append, extend, etc. + +- Use **SET** when you need: + - Collection of unique values (no duplicates) + - Fast membership testing (x in set) + - Set theory operations (union, intersection) + - To eliminate duplicates from data - Use **data list** when you need to: - Process each item individually through ComfyUI nodes diff --git a/src/basic_data_handling/__init__.py b/src/basic_data_handling/__init__.py index 5bc6e53..0197e6b 100644 --- a/src/basic_data_handling/__init__.py +++ b/src/basic_data_handling/__init__.py @@ -1,4 +1,5 @@ -from . import boolean_nodes, data_list_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, set_nodes, string_nodes) NODE_CLASS_MAPPINGS = {} NODE_CLASS_MAPPINGS.update(boolean_nodes.NODE_CLASS_MAPPINGS) @@ -7,6 +8,7 @@ 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) NODE_CLASS_MAPPINGS.update(list_nodes.NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(set_nodes.NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(string_nodes.NODE_CLASS_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS = {} @@ -16,4 +18,5 @@ 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) NODE_DISPLAY_NAME_MAPPINGS.update(list_nodes.NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(set_nodes.NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(string_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 index d5f3cc6..fc49457 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -2,35 +2,6 @@ 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. @@ -677,8 +648,36 @@ class DataListZip: return (result,) +class DataListToList: + """ + 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(),) + + NODE_CLASS_MAPPINGS = { - "DataListConvertToList": DataListConvertToList, "DataListAppend": DataListAppend, "DataListExtend": DataListExtend, "DataListInsert": DataListInsert, @@ -697,10 +696,10 @@ NODE_CLASS_MAPPINGS = { "DataListContains": DataListContains, "DataListCreateEmpty": DataListCreateEmpty, "DataListZip": DataListZip, + "DataListToList": DataListToList, } NODE_DISPLAY_NAME_MAPPINGS = { - "DataListConvertToList": "convert to LIST", "DataListAppend": "append", "DataListExtend": "extend", "DataListInsert": "insert", @@ -719,4 +718,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { "DataListContains": "contains", "DataListCreateEmpty": "create_empty", "DataListZip": "zip", + "DataListToList": "convert to LIST", } diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index 63fc7f9..a41b3a1 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -537,59 +537,6 @@ class DictRemove: 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. @@ -772,6 +719,59 @@ class DictCompare: return are_equal, only_in_dict1, only_in_dict2, different_values +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 + + NODE_CLASS_MAPPINGS = { "DictCreate": DictCreate, "DictCreateFromItems": DictCreateFromItems, @@ -792,13 +792,13 @@ NODE_CLASS_MAPPINGS = { "DictMerge": DictMerge, "DictGetKeysValues": DictGetKeysValues, "DictRemove": DictRemove, - "AnyToDict": AnyToDict, "DictFilterByKeys": DictFilterByKeys, "DictExcludeKeys": DictExcludeKeys, "DictGetMultiple": DictGetMultiple, "DictInvert": DictInvert, "DictCreateFromLists": DictCreateFromLists, "DictCompare": DictCompare, + "AnyToDict": AnyToDict, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -821,11 +821,11 @@ NODE_DISPLAY_NAME_MAPPINGS = { "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", + "AnyToDict": "any_to_DICT", } diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 9d21e6c..47ef9cb 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -1,32 +1,6 @@ from typing import Any from inspect import cleandoc -class ListConvertToDataList: - """ - Converts a LIST object into a ComfyUI data list. - - 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 { - "required": { - "list": ("LIST", {}), - } - } - - RETURN_TYPES = ("*",) - CATEGORY = "Basic/LIST" - DESCRIPTION = cleandoc(__doc__ or "") - FUNCTION = "convert" - OUTPUT_IS_LIST = (True,) - - def convert(self, list) -> tuple[list[Any]]: - return (list,) - - class ListAppend: """ Adds an item to the end of a LIST. @@ -466,6 +440,56 @@ class ListContains: return (value in list,) +class ListToDataList: + """ + Converts a LIST object into a ComfyUI data list. + + 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 { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("*",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "convert" + OUTPUT_IS_LIST = (True,) + + def convert(self, list) -> tuple[list[Any]]: + return (list,) + + +class ListToSet: + """ + Converts a LIST into a SET. + + This node takes a LIST input and creates a new SET containing all unique elements + from the LIST, removing any duplicates. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "convert" + + def convert(self, list: list[Any]) -> tuple[set[Any]]: + return (set(list),) + + class AnyToList: """ Converts any input datatype into a LIST. @@ -492,7 +516,6 @@ class AnyToList: NODE_CLASS_MAPPINGS = { - "ListConvertToDataList": ListConvertToDataList, "ListAppend": ListAppend, "ListExtend": ListExtend, "ListInsert": ListInsert, @@ -508,11 +531,12 @@ NODE_CLASS_MAPPINGS = { "ListGetItem": ListGetItem, "ListSetItem": ListSetItem, "ListContains": ListContains, + "ListToDataList": ListToDataList, + "ListToSet": ListToSet, "AnyToList": AnyToList, } NODE_DISPLAY_NAME_MAPPINGS = { - "ListConvertToDataList": "convert to data list", "ListAppend": "append", "ListExtend": "extend", "ListInsert": "insert", @@ -528,5 +552,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ListGetItem": "get item", "ListSetItem": "set item", "ListContains": "contains", + "ListToDataList": "convert to data list", + "ListToSet": "convert to SET", "AnyToList": "any to LIST", } diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py new file mode 100644 index 0000000..a0f37bc --- /dev/null +++ b/src/basic_data_handling/set_nodes.py @@ -0,0 +1,512 @@ +from typing import Any +from inspect import cleandoc + +class SetAdd: + """ + Adds an item to a SET. + + This node takes a SET and any item as inputs, then returns a new SET + with the item added. If the item is already present, the SET remains unchanged. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set": ("SET", {}), + "item": ("*", {}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "add" + + def add(self, set: set[Any], item: Any) -> tuple[set[Any]]: + result = set.copy() + result.add(item) + return (result,) + + +class SetRemove: + """ + 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": ("*", {}), + } + } + + RETURN_TYPES = ("SET", "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 SetDiscard: + """ + Removes an item from a SET if it is present. + + This node takes a SET and any item as inputs, then returns a new SET + with the item removed. Unlike remove, no error is raised if the item is not present. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set": ("SET", {}), + "item": ("*", {}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "discard" + + def discard(self, set: set[Any], item: Any) -> tuple[set[Any]]: + result = set.copy() + result.discard(item) + return (result,) + + +class SetPop: + """ + 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", "*") + 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 SetClear: + """ + Removes all items from a SET. + + This node takes a SET as input and returns a new empty SET. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set": ("SET", {}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "clear" + + def clear(self, set: set[Any]) -> tuple[set[Any]]: + return (set.__class__(),) + + +class SetUnion: + """ + 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: + """ + Returns the intersection of two or more SETs. + + This node takes multiple SETs as input and returns a new SET containing + only elements present in all 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 = "intersection" + + def intersection(self, set1: set[Any], set2: set[Any], set3=None, set4=None) -> tuple[set[Any]]: + result = set1.copy() + result.intersection_update(set2) + + if set3 is not None: + result.intersection_update(set3) + + if set4 is not None: + result.intersection_update(set4) + + return (result,) + + +class SetDifference: + """ + 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 SetSymmetricDifference: + """ + 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,) + + +class SetIsSubset: + """ + Checks if set1 is a subset of set2. + + This node takes two SETs as input and returns True if set1 is a subset of set2 + (all elements in set1 are also in set2). + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set1": ("SET", {}), + "set2": ("SET", {}), + } + } + + RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("is_subset",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "is_subset" + + def is_subset(self, set1: set[Any], set2: set[Any]) -> tuple[bool]: + return (set1.issubset(set2),) + + +class SetIsSuperset: + """ + Checks if set1 is a superset of set2. + + This node takes two SETs as input and returns True if set1 is a superset of set2 + (set1 contains all elements in set2). + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set1": ("SET", {}), + "set2": ("SET", {}), + } + } + + RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("is_superset",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "is_superset" + + def is_superset(self, set1: set[Any], set2: set[Any]) -> tuple[bool]: + return (set1.issuperset(set2),) + + +class SetIsDisjoint: + """ + 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 = ("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: + """ + 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": ("*", {}), + } + } + + RETURN_TYPES = ("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: + """ + Returns the number of items in a SET. + + This node takes a SET as input and returns its length (number of elements) as an integer. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set": ("SET", {}), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("length",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "length" + + def length(self, set: set[Any]) -> tuple[int]: + return (len(set),) + + +class ListToSet: + """ + Converts a LIST into a SET. + + This node takes a LIST input and creates a new SET containing all unique elements + from the LIST, removing any duplicates. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("SET",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "convert" + + def convert(self, list: list[Any]) -> tuple[set[Any]]: + return (set(list),) + + +class SetToDataList: + """ + Converts a SET object into a ComfyUI data list. + + This node takes a SET object (Python set 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 { + "required": { + "set": ("SET", {}), + } + } + + RETURN_TYPES = ("*",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "convert" + OUTPUT_IS_LIST = (True,) + + def convert(self, set) -> tuple[list[Any]]: + return (list(set),) + + +class SetToList: + """ + Converts a SET into a LIST. + + This node takes a SET input and creates a new LIST containing all elements + from the SET. Note that the order of elements in the resulting LIST is arbitrary + since SETs are unordered collections. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set": ("SET", {}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "convert" + + def convert(self, set: set[Any]) -> tuple[list[Any]]: + return (list(set),) + + +NODE_CLASS_MAPPINGS = { + "SetAdd": SetAdd, + "SetRemove": SetRemove, + "SetDiscard": SetDiscard, + "SetPop": SetPop, + "SetClear": SetClear, + "SetUnion": SetUnion, + "SetIntersection": SetIntersection, + "SetDifference": SetDifference, + "SetSymmetricDifference": SetSymmetricDifference, + "SetIsSubset": SetIsSubset, + "SetIsSuperset": SetIsSuperset, + "SetIsDisjoint": SetIsDisjoint, + "SetContains": SetContains, + "SetLength": SetLength, + "SetToDataList": SetToDataList, + "SetToList": SetToList, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SetAdd": "add", + "SetRemove": "remove", + "SetDiscard": "discard", + "SetPop": "pop", + "SetClear": "clear", + "SetUnion": "union", + "SetIntersection": "intersection", + "SetDifference": "difference", + "SetSymmetricDifference": "symmetric_difference", + "SetIsSubset": "is_subset", + "SetIsSuperset": "is_superset", + "SetIsDisjoint": "is_disjoint", + "SetContains": "contains", + "SetLength": "length", + "SetToDataList": "convert to data list", + "SetToList": "convert to LIST", +}