From 2808f41972dfb79ac4248f75788f2d2c29e5fc2d Mon Sep 17 00:00:00 2001 From: StableLlama Date: Fri, 30 May 2025 00:38:56 +0200 Subject: [PATCH] Version 0.3.4 Add nodes to LIST, data list, DICT and SET to get first and last element as well as popping a random element --- pyproject.toml | 2 +- src/basic_data_handling/data_list_nodes.py | 92 ++++++++++++++++++++++ src/basic_data_handling/dict_nodes.py | 40 +++++++++- src/basic_data_handling/list_nodes.py | 87 ++++++++++++++++++++ src/basic_data_handling/set_nodes.py | 33 ++++++++ 5 files changed, 252 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 997058e..f74a889 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "0.3.3" +version = "0.3.4" description = """NOTE: Still in development! Expect breaking changes! Basic Python functions for manipulating data that every programmer is used to. Currently supported ComfyUI data types: BOOLEAN, FLOAT, INT, STRING and data lists. diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 4c157bd..583f76d 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -348,6 +348,33 @@ class DataListFilterSelect(ComfyNodeABC): return result_true, result_false +class DataListFirst(ComfyNodeABC): + """ + Returns the first element in a list. + + This node takes a list as input and returns the first element of the list. + If the list is empty, it returns None. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY, {}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("first_element",) + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_first_element" + INPUT_IS_LIST = True + + def get_first_element(self, **kwargs: list[Any]) -> tuple[Any]: + input_list = kwargs.get('list', []) + return (input_list[0] if input_list else None,) + + class DataListGetItem(ComfyNodeABC): """ Retrieves an item at a specified position in a list. @@ -453,6 +480,33 @@ class DataListInsert(ComfyNodeABC): return (result,) +class DataListLast(ComfyNodeABC): + """ + Returns the last element in a list. + + This node takes a list as input and returns the last element of the list. + If the list is empty, it returns None. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY, {}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("last_element",) + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_last_element" + INPUT_IS_LIST = True + + def get_last_element(self, **kwargs: list[Any]) -> tuple[Any]: + input_list = kwargs.get('list', []) + return (input_list[-1] if input_list else None,) + + class DataListLength(ComfyNodeABC): """ Counts the number of items in a list. @@ -588,6 +642,38 @@ class DataListPop(ComfyNodeABC): return result, None +class DataListPopRandom(ComfyNodeABC): + """ + Removes and returns a random element from a list. + + This node takes a list as input and returns the list with the random element removed + and the removed element itself. If the list is empty, it returns None for the element. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY, {}), + } + } + + RETURN_TYPES = (IO.ANY, IO.ANY) + RETURN_NAMES = ("list", "item") + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop_random_element" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True, False) + + def pop_random_element(self, **kwargs: list[Any]) -> tuple[list[Any], Any]: + from random import randrange + input_list = kwargs.get('list', []).copy() + if input_list: + random_element = input_list.pop(randrange(len(input_list))) + return input_list, random_element + return input_list, None + + class DataListRemove(ComfyNodeABC): """ Removes the first occurrence of a specified value from a list. @@ -864,13 +950,16 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: DataListExtend": DataListExtend, "Basic data handling: DataListFilter": DataListFilter, "Basic data handling: DataListFilterSelect": DataListFilterSelect, + "Basic data handling: DataListFirst": DataListFirst, "Basic data handling: DataListGetItem": DataListGetItem, "Basic data handling: DataListIndex": DataListIndex, "Basic data handling: DataListInsert": DataListInsert, + "Basic data handling: DataListLast": DataListLast, "Basic data handling: DataListLength": DataListLength, "Basic data handling: DataListMax": DataListMax, "Basic data handling: DataListMin": DataListMin, "Basic data handling: DataListPop": DataListPop, + "Basic data handling: DataListPopRandom": DataListPopRandom, "Basic data handling: DataListRemove": DataListRemove, "Basic data handling: DataListReverse": DataListReverse, "Basic data handling: DataListSetItem": DataListSetItem, @@ -893,13 +982,16 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: DataListExtend": "extend", "Basic data handling: DataListFilter": "filter", "Basic data handling: DataListFilterSelect": "filter select", + "Basic data handling: DataListFirst": "first", "Basic data handling: DataListGetItem": "get item", "Basic data handling: DataListIndex": "index", "Basic data handling: DataListInsert": "insert", + "Basic data handling: DataListLast": "last", "Basic data handling: DataListLength": "length", "Basic data handling: DataListMax": "max", "Basic data handling: DataListMin": "min", "Basic data handling: DataListPop": "pop", + "Basic data handling: DataListPopRandom": "pop random", "Basic data handling: DataListRemove": "remove", "Basic data handling: DataListReverse": "reverse", "Basic data handling: DataListSetItem": "set item", diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index d243aa2..c2f0871 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -719,6 +719,42 @@ class DictPopItem(ComfyNodeABC): return result, "", None, False +class DictPopRandom(ComfyNodeABC): + """ + Removes and returns a random key-value pair from a dictionary. + + This node takes a dictionary as input, removes a random key-value pair, + and returns the modified dictionary along with the removed key and value. + If the dictionary is empty, it returns empty values. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_dict": ("DICT", {}), + } + } + + RETURN_TYPES = ("DICT", IO.STRING, IO.ANY, IO.BOOLEAN) + RETURN_NAMES = ("dict", "key", "value", "success") + CATEGORY = "Basic/DICT" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop_random" + + def pop_random(self, input_dict: dict) -> tuple[dict, str, Any, bool]: + import random + result = input_dict.copy() + try: + if result: + random_key = random.choice(list(result.keys())) + random_value = result.pop(random_key) + return result, random_key, random_value, True + else: + return result, "", None, False + except: + return result, "", None, False + + class DictRemove(ComfyNodeABC): """ Removes a key-value pair from a dictionary. @@ -883,6 +919,7 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: DictMerge": DictMerge, "Basic data handling: DictPop": DictPop, "Basic data handling: DictPopItem": DictPopItem, + "Basic data handling: DictPopRandom": DictPopRandom, "Basic data handling: DictRemove": DictRemove, "Basic data handling: DictSet": DictSet, "Basic data handling: DictSetDefault": DictSetDefault, @@ -913,7 +950,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: DictLength": "length", "Basic data handling: DictMerge": "merge", "Basic data handling: DictPop": "pop", - "Basic data handling: DictPopItem": "popitem", + "Basic data handling: DictPopItem": "pop item", + "Basic data handling: DictPopRandom": "pop random", "Basic data handling: DictRemove": "remove", "Basic data handling: DictSet": "set", "Basic data handling: DictSetDefault": "setdefault", diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 11d8300..b8de2d6 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -248,6 +248,31 @@ class ListExtend(ComfyNodeABC): return (result,) +class ListFirst(ComfyNodeABC): + """ + Returns the first element in a LIST. + + This node takes a LIST as input and returns the first element of the list. + If the LIST is empty, it returns None. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("first_element",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_first_element" + + def get_first_element(self, list: list[Any]) -> tuple[Any]: + return (list[0] if list else None,) + + class ListGetItem(ComfyNodeABC): """ Retrieves an item at a specified position in a LIST. @@ -343,6 +368,31 @@ class ListInsert(ComfyNodeABC): return (result,) +class ListLast(ComfyNodeABC): + """ + Returns the last element in a LIST. + + This node takes a LIST as input and returns the last element of the list. + If the LIST is empty, it returns None. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("last_element",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "get_last_element" + + def get_last_element(self, list: list[Any]) -> tuple[Any]: + return (list[-1] if list else None,) + + class ListLength(ComfyNodeABC): """ Returns the number of items in a LIST. @@ -466,6 +516,37 @@ class ListPop(ComfyNodeABC): return result, None +class ListPopRandom(ComfyNodeABC): + """ + Removes and returns a random element from a LIST. + + This node takes a LIST as input and returns the LIST with the random element removed + and the removed element itself. If the LIST is empty, it returns None for the element. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + } + } + + RETURN_TYPES = ("LIST", IO.ANY) + RETURN_NAMES = ("list", "item") + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop_random_element" + + def pop_random_element(self, list: list[Any]) -> tuple[list[Any], Any]: + import random + result = list.copy() + if result: + random_index = random.randrange(len(result)) + random_element = result.pop(random_index) + return result, random_element + return result, None + + class ListRemove(ComfyNodeABC): """ Removes the first occurrence of a specified value from a LIST. @@ -680,13 +761,16 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: ListContains": ListContains, "Basic data handling: ListCount": ListCount, "Basic data handling: ListExtend": ListExtend, + "Basic data handling: ListFirst": ListFirst, "Basic data handling: ListGetItem": ListGetItem, "Basic data handling: ListIndex": ListIndex, "Basic data handling: ListInsert": ListInsert, + "Basic data handling: ListLast": ListLast, "Basic data handling: ListLength": ListLength, "Basic data handling: ListMax": ListMax, "Basic data handling: ListMin": ListMin, "Basic data handling: ListPop": ListPop, + "Basic data handling: ListPopRandom": ListPopRandom, "Basic data handling: ListRemove": ListRemove, "Basic data handling: ListReverse": ListReverse, "Basic data handling: ListSetItem": ListSetItem, @@ -706,13 +790,16 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: ListContains": "contains", "Basic data handling: ListCount": "count", "Basic data handling: ListExtend": "extend", + "Basic data handling: ListFirst": "first element", "Basic data handling: ListGetItem": "get item", "Basic data handling: ListIndex": "index", "Basic data handling: ListInsert": "insert", + "Basic data handling: ListLast": "last element", "Basic data handling: ListLength": "length", "Basic data handling: ListMax": "max", "Basic data handling: ListMin": "min", "Basic data handling: ListPop": "pop", + "Basic data handling: ListPopRandom": "pop random", "Basic data handling: ListRemove": "remove", "Basic data handling: ListReverse": "reverse", "Basic data handling: ListSetItem": "set item", diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index ea9014e..7bebdd9 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -417,6 +417,37 @@ class SetPop(ComfyNodeABC): return result, None +class SetPopRandom(ComfyNodeABC): + """ + Removes and returns a random element from a SET. + + This node takes a SET as input and returns the SET with a random element removed + and the removed element itself. If the SET is empty, it returns None for the element. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "set": ("SET", {}), + } + } + + RETURN_TYPES = ("SET", IO.ANY) + RETURN_NAMES = ("set", "item") + CATEGORY = "Basic/SET" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "pop_random_element" + + def pop_random_element(self, set: set[Any]) -> tuple[set[Any], Any]: + import random + result = set.copy() + if result: + random_element = random.choice(list(result)) + result.remove(random_element) + return result, random_element + return result, None + + class SetRemove(ComfyNodeABC): """ Removes an item from a SET. @@ -581,6 +612,7 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: SetIsSuperset": SetIsSuperset, "Basic data handling: SetLength": SetLength, "Basic data handling: SetPop": SetPop, + "Basic data handling: SetPopRandom": SetPopRandom, "Basic data handling: SetRemove": SetRemove, "Basic data handling: SetSymmetricDifference": SetSymmetricDifference, "Basic data handling: SetUnion": SetUnion, @@ -604,6 +636,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: SetIsSuperset": "is superset", "Basic data handling: SetLength": "length", "Basic data handling: SetPop": "pop", + "Basic data handling: SetPopRandom": "pop random", "Basic data handling: SetRemove": "remove", "Basic data handling: SetSymmetricDifference": "symmetric difference", "Basic data handling: SetUnion": "union",