Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
59 changes: 47 additions & 12 deletions src/basic_data_handling/data_list_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def INPUT_TYPES(cls):
OUTPUT_IS_LIST = (True,)

def create_list(self, **kwargs: list[Any]) -> tuple[list]:
values = list(kwargs.values())
return (values[:-1],)
values = list(kwargs.values())[:-1]
return (values,)


class DataListListCreate(ComfyNodeABC):
Expand Down Expand Up @@ -71,8 +71,8 @@ def INPUT_TYPES(cls):
INPUT_IS_LIST = True

def create_list(self, **kwargs: list[Any]) -> tuple[list]:
values = list(kwargs.values())
return (values[:-1],)
values = list(kwargs.values())[:-1]
return (values,)


class DataListCreateFromBoolean(ComfyNodeABC):
Expand All @@ -98,8 +98,8 @@ def INPUT_TYPES(cls):
OUTPUT_IS_LIST = (True,)

def create_list(self, **kwargs: list[Any]) -> tuple[list]:
values = [bool(value) for value in kwargs.values()]
return (values[:-1],)
values = [bool(value) for value in list(kwargs.values())[:-1]]
return (values,)


class DataListCreateFromFloat(ComfyNodeABC):
Expand All @@ -125,8 +125,8 @@ def INPUT_TYPES(cls):
OUTPUT_IS_LIST = (True,)

def create_list(self, **kwargs: list[Any]) -> tuple[list]:
values = [float(value) for value in kwargs.values()]
return (values[:-1],)
values = [float(value) for value in list(kwargs.values())[:-1]]
return (values,)


class DataListCreateFromInt(ComfyNodeABC):
Expand All @@ -152,8 +152,8 @@ def INPUT_TYPES(cls):
OUTPUT_IS_LIST = (True,)

def create_list(self, **kwargs: list[Any]) -> tuple[list]:
values = [int(value) for value in kwargs.values()]
return (values[:-1],)
values = [int(value) for value in list(kwargs.values())[:-1]]
return (values,)


class DataListCreateFromString(ComfyNodeABC):
Expand All @@ -179,8 +179,8 @@ def INPUT_TYPES(cls):
OUTPUT_IS_LIST = (True,)

def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
values = [str(value) for value in kwargs.values()]
return (values[:-1],)
values = [str(value) for value in list(kwargs.values())[:-1]]
return (values,)


class DataListAll(ComfyNodeABC):
Expand Down Expand Up @@ -920,6 +920,39 @@ def set_item(self, **kwargs: list[Any]) -> tuple[Any]:
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.
Expand Down Expand Up @@ -1145,6 +1178,7 @@ def convert(self, **kwargs: list[Any]) -> tuple[set[Any]]:
"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,
Expand Down Expand Up @@ -1183,6 +1217,7 @@ def convert(self, **kwargs: list[Any]) -> tuple[set[Any]]:
"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",
Expand Down
39 changes: 35 additions & 4 deletions src/basic_data_handling/list_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ def INPUT_TYPES(cls):
FUNCTION = "create_list"

def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
values = list(kwargs.values())
return (values[:-1],)
values = list(kwargs.values())[:-1]
return (values,)


class ListCreateFromBoolean(ComfyNodeABC):
Expand All @@ -64,8 +64,8 @@ def INPUT_TYPES(cls):
FUNCTION = "create_list"

def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
values = [bool(value) for value in kwargs.values()]
return (values[:-1],)
values = [bool(value) for value in list(kwargs.values())[:-1]]
return (values,)


class ListCreateFromFloat(ComfyNodeABC):
Expand Down Expand Up @@ -741,6 +741,35 @@ def set_item(self, list: list[Any], index: int, value: Any) -> tuple[list[Any]]:
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.
Expand Down Expand Up @@ -910,6 +939,7 @@ def convert(self, list: list[Any]) -> tuple[set[Any]]:
"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,
Expand Down Expand Up @@ -944,6 +974,7 @@ def convert(self, list: list[Any]) -> tuple[set[Any]]:
"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",
Expand Down
72 changes: 54 additions & 18 deletions tests/test_data_list_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
DataListRemove,
DataListReverse,
DataListSetItem,
DataListShuffle,
DataListSlice,
DataListSort,
DataListSum,
Expand Down Expand Up @@ -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,)
Expand Down Expand Up @@ -183,75 +219,75 @@ def test_filter_select():
def test_create():
node = DataListCreate()
# Testing with one item
assert node.create_list(item_0="test", _dynamic_number=1) == (["test"],)
assert node.create_list(item_0="test", item_1="") == (["test"],)

# Testing with multiple items of different types
assert node.create_list(item_0=1, item_1="two", item_2=3.0, _dynamic_number=3) == ([1, "two", 3.0],)
assert node.create_list(item_0=1, item_1="two", item_2=3.0, item_3="") == ([1, "two", 3.0],)

# Testing with empty list (no items)
assert node.create_list(_dynamic_number=0) == ([],)
assert node.create_list(item_0="") == ([],)


def test_create_from_boolean():
node = DataListCreateFromBoolean()
# Testing with boolean values
assert node.create_list(item_0=True, item_1=False, _dynamic_number=2) == ([True, False],)
assert node.create_list(item_0=True, item_1=False, item_2=False) == ([True, False],)

# Testing with boolean-convertible values
assert node.create_list(item_0=1, item_1=0, _dynamic_number=2) == ([True, False],)
assert node.create_list(item_0=1, item_1=0, item_2="") == ([True, False],)

# Testing with empty list
assert node.create_list(_dynamic_number=0) == ([],)
assert node.create_list(item_0=0) == ([],)


def test_create_from_float():
node = DataListCreateFromFloat()
# Testing with float values
assert node.create_list(item_0=1.5, item_1=2.5, _dynamic_number=2) == ([1.5, 2.5],)
assert node.create_list(item_0=1.5, item_1=2.5, item_2=2) == ([1.5, 2.5],)

# Testing with float-convertible values
assert node.create_list(item_0=1, item_1="2.5", _dynamic_number=2) == ([1.0, 2.5],)
assert node.create_list(item_0=1, item_1="2.5", item_2="") == ([1.0, 2.5],)

# Testing with empty list
assert node.create_list(_dynamic_number=0) == ([],)
assert node.create_list(item_0="") == ([],)


def test_create_from_int():
node = DataListCreateFromInt()
# Testing with integer values
assert node.create_list(item_0=1, item_1=2, _dynamic_number=2) == ([1, 2],)
assert node.create_list(item_0=1, item_1=2, item_2="") == ([1, 2],)

# Testing with int-convertible values
assert node.create_list(item_0="1", item_1=2.0, _dynamic_number=2) == ([1, 2],)
assert node.create_list(item_0="1", item_1=2.0, item_2="") == ([1, 2],)

# Testing with empty list
assert node.create_list(_dynamic_number=0) == ([],)
assert node.create_list(item_0="") == ([],)


def test_create_from_string():
node = DataListCreateFromString()
# Testing with string values
assert node.create_list(item_0="hello", item_1="world", _dynamic_number=2) == (["hello", "world"],)
assert node.create_list(item_0="hello", item_1="world", item_2="") == (["hello", "world"],)

# Testing with string-convertible values
assert node.create_list(item_0=123, item_1=True, _dynamic_number=2) == (["123", "True"],)
assert node.create_list(item_0=123, item_1=True, item_2="") == (["123", "True"],)

# Testing with empty list
assert node.create_list(_dynamic_number=0) == ([],)
assert node.create_list(item_0="") == ([],)


def test_list_create():
node = DataListListCreate()
# Testing with string values
assert (node.create_list(item_0=["hello", "world"], item_1=["bye", "bye!"], _dynamic_number=2) ==
assert (node.create_list(item_0=["hello", "world"], item_1=["bye", "bye!"], item_2="") ==
([["hello", "world"], ["bye", "bye!"]],))

# Testing with mixed values
assert (node.create_list(item_0=[123, 456], item_1=[True, False], _dynamic_number=2) ==
assert (node.create_list(item_0=[123, 456], item_1=[True, False], item_2="") ==
([[123, 456], [True, False]],))

# Testing with empty list
assert node.create_list(_dynamic_number=0) == ([],)
assert node.create_list(item_0="") == ([],)


def test_pop_random():
Expand Down
Loading