From 443e75be09a4cd618033b83d8b189ec44bf15985 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Sat, 31 Jan 2026 19:42:33 +0100 Subject: [PATCH] Add shuffle to Data List and LIST Bump version to 1.3.0 --- README.md | 4 +-- pyproject.toml | 2 +- src/basic_data_handling/data_list_nodes.py | 35 +++++++++++++++++++++ src/basic_data_handling/list_nodes.py | 31 +++++++++++++++++++ tests/test_data_list_nodes.py | 36 ++++++++++++++++++++++ tests/test_list_nodes.py | 36 ++++++++++++++++++++++ 6 files changed, 141 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f524b7..6231c6e 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Mechanisms to direct workflow execution: ComfyUI list manipulation nodes (for processing individual items): - **Creation**: create Data List (generic and type-specific versions) -- **Modification**: append, extend, insert, set item, remove, pop, pop random +- **Modification**: append, extend, insert, set item, shuffle, remove, pop, pop random - **Filtering**: filter, filter select - **Access**: get item, first, last, slice, index, contains - **Information**: length, count @@ -103,7 +103,7 @@ Integer operation nodes: Python list manipulation nodes (as a single variable): - **Creation**: create LIST (generic and type-specific versions) -- **Modification**: append, extend, insert, remove, pop, pop random, set_item +- **Modification**: append, extend, insert, remove, pop, pop random, set_item, shuffle - **Access**: get_item, first, last, slice, index, contains - **Information**: length, count - **Operations**: sort, reverse, min, max diff --git a/pyproject.toml b/pyproject.toml index dbb2e4a..8c30f81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "1.2.0" +version = "1.3.0" description = """Basic Python functions for manipulating data that every programmer is used to, lightweight with no additional dependencies. Supported data types: diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 7ec7f01..833efd9 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -920,6 +920,39 @@ class DataListSetItem(ComfyNodeABC): raise IndexError(f"Index {index} out of range for list of length {len(input_list)}") +class DataListShuffle(ComfyNodeABC): + """ + Shuffles the items in a list using a seed for reproducibility. + + This node takes a list and a seed as input and returns a new shuffled list. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": (IO.ANY, {}), + "seed": (IO.INT, {"default": 0}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("list",) + CATEGORY = "Basic/Data List" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "shuffle_list" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + def shuffle_list(self, **kwargs: list[Any]) -> tuple[list[Any]]: + import random + input_list = kwargs.get('list', []) + seed = kwargs.get('seed', [0])[0] + random.seed(seed) + result = input_list.copy() + random.shuffle(result) + return (result,) + + class DataListSlice(ComfyNodeABC): """ Creates a slice of a list. @@ -1145,6 +1178,7 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: DataListRemove": DataListRemove, "Basic data handling: DataListReverse": DataListReverse, "Basic data handling: DataListSetItem": DataListSetItem, + "Basic data handling: DataListShuffle": DataListShuffle, "Basic data handling: DataListSlice": DataListSlice, "Basic data handling: DataListSort": DataListSort, "Basic data handling: DataListSum": DataListSum, @@ -1183,6 +1217,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: DataListRemove": "remove", "Basic data handling: DataListReverse": "reverse", "Basic data handling: DataListSetItem": "set item", + "Basic data handling: DataListShuffle": "shuffle", "Basic data handling: DataListSlice": "slice", "Basic data handling: DataListSort": "sort", "Basic data handling: DataListSum": "sum", diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 966cd23..c2344ea 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -741,6 +741,35 @@ class ListSetItem(ComfyNodeABC): except IndexError: raise IndexError(f"Index {index} out of range for LIST of length {len(list)}") + +class ListShuffle(ComfyNodeABC): + """ + Shuffles the items in a list using a seed for reproducibility. + + This node takes a LIST and a seed as input and returns a new shuffled LIST. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "list": ("LIST", {}), + "seed": ("INT", {"default": 0}), + } + } + + RETURN_TYPES = ("LIST",) + CATEGORY = "Basic/LIST" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "shuffle_list" + + def shuffle_list(self, list: list[Any], seed: int) -> tuple[list[Any]]: + import random + random.seed(seed) + result = list.copy() + random.shuffle(result) + return (result,) + + class ListSlice(ComfyNodeABC): """ Creates a slice of a LIST. @@ -910,6 +939,7 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: ListRemove": ListRemove, "Basic data handling: ListReverse": ListReverse, "Basic data handling: ListSetItem": ListSetItem, + "Basic data handling: ListShuffle": ListShuffle, "Basic data handling: ListSlice": ListSlice, "Basic data handling: ListSort": ListSort, "Basic data handling: ListSum": ListSum, @@ -944,6 +974,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: ListRemove": "remove", "Basic data handling: ListReverse": "reverse", "Basic data handling: ListSetItem": "set item", + "Basic data handling: ListShuffle": "shuffle", "Basic data handling: ListSlice": "slice", "Basic data handling: ListSort": "sort", "Basic data handling: ListSum": "sum", diff --git a/tests/test_data_list_nodes.py b/tests/test_data_list_nodes.py index 45e1894..09a3eb0 100644 --- a/tests/test_data_list_nodes.py +++ b/tests/test_data_list_nodes.py @@ -29,6 +29,7 @@ from src.basic_data_handling.data_list_nodes import ( DataListRemove, DataListReverse, DataListSetItem, + DataListShuffle, DataListSlice, DataListSort, DataListSum, @@ -128,6 +129,41 @@ def test_set_item(): node.set_item(list=[], index=[0], value=["test"]) # Out of range +def test_shuffle(): + node = DataListShuffle() + + # Test determinism: Same seed should produce the same shuffle + original_list = [1, 2, 3, 4, 5] + result1 = node.shuffle_list(list=original_list, seed=[42]) + result2 = node.shuffle_list(list=original_list, seed=[42]) + assert result1 == result2 # Same seed, same output + + # Verify the output is a permutation of the input + assert sorted(result1[0]) == sorted(original_list) + assert len(result1[0]) == len(original_list) + + # Test different seeds produce different shuffles (not guaranteed, but likely for this list) + result3 = node.shuffle_list(list=original_list, seed=[123]) + assert result1 != result3 # Different seed, likely different output + + # Test empty list + assert node.shuffle_list(list=[], seed=[0]) == ([],) + + # Test single-item list + assert node.shuffle_list(list=[42], seed=[99]) == ([42],) + + # Test mixed data types + mixed_list = ["apple", 3.14, True, 42] + result4 = node.shuffle_list(list=mixed_list, seed=[7]) + assert sorted(result4[0], key=str) == sorted(mixed_list, key=str) # Sort for comparison since types may not be directly comparable + assert len(result4[0]) == len(mixed_list) + + # Test that the original list is not modified (node should return a copy) + original_copy = original_list.copy() + node.shuffle_list(list=original_list, seed=[1]) + assert original_list == original_copy # Original should remain unchanged + + def test_contains(): node = DataListContains() assert node.contains(list=[1, 2, 3], value=[2]) == (True,) diff --git a/tests/test_list_nodes.py b/tests/test_list_nodes.py index 1da2fbb..018ed71 100644 --- a/tests/test_list_nodes.py +++ b/tests/test_list_nodes.py @@ -26,6 +26,7 @@ from src.basic_data_handling.list_nodes import ( ListRemove, ListReverse, ListSetItem, + ListShuffle, ListSlice, ListSort, ListSum, @@ -158,6 +159,41 @@ def test_list_set_item(): node.set_item([1, 2, 3], 3, 42) == ([1, 2, 3],) # Out of range +def test_shuffle(): + node = ListShuffle() + + # Test determinism: Same seed should produce the same shuffle + original_list = [1, 2, 3, 4, 5] + result1 = node.shuffle_list(list=original_list, seed=42) + result2 = node.shuffle_list(list=original_list, seed=42) + assert result1 == result2 # Same seed, same output + + # Verify the output is a permutation of the input + assert sorted(result1[0]) == sorted(original_list) + assert len(result1[0]) == len(original_list) + + # Test different seeds produce different shuffles (not guaranteed, but likely for this list) + result3 = node.shuffle_list(list=original_list, seed=123) + assert result1 != result3 # Different seed, likely different output + + # Test empty list + assert node.shuffle_list(list=[], seed=0) == ([],) + + # Test single-item list + assert node.shuffle_list(list=[42], seed=99) == ([42],) + + # Test mixed data types + mixed_list = ["apple", 3.14, True, 42] + result4 = node.shuffle_list(list=mixed_list, seed=7) + assert sorted(result4[0], key=str) == sorted(mixed_list, key=str) # Sort for comparison since types may not be directly comparable + assert len(result4[0]) == len(mixed_list) + + # Test that the original list is not modified (node should return a copy) + original_copy = original_list.copy() + node.shuffle_list(list=original_list, seed=1) + assert original_list == original_copy # Original should remain unchanged + + def test_list_contains(): node = ListContains() assert node.contains([1, 2, 3], 2) == (True,)