From b1f9824fa2a98d0b56629cc32a8cb86da13e4e48 Mon Sep 17 00:00:00 2001 From: Comahar Date: Tue, 14 Jul 2026 23:06:54 +0300 Subject: [PATCH] Add optional seed inputs to pop-random nodes. --- src/basic_data_handling/data_list_nodes.py | 23 ++++++++++++++++++---- src/basic_data_handling/dict_nodes.py | 16 +++++++++++---- src/basic_data_handling/list_nodes.py | 16 +++++++++++---- src/basic_data_handling/set_nodes.py | 16 +++++++++++---- tests/test_data_list_nodes.py | 7 +++++++ tests/test_dict_nodes.py | 5 +++++ tests/test_list_nodes.py | 7 +++++++ tests/test_set_nodes.py | 7 +++++++ 8 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 833efd9..04e855e 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -760,13 +760,17 @@ class DataListPopRandom(ComfyNodeABC): 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. + An optional seed can be provided for reproducible selection. """ @classmethod def INPUT_TYPES(cls): return { "required": { "list": (IO.ANY, {}), - } + }, + "optional": { + "seed": (IO.INT, {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}), + }, } RETURN_TYPES = (IO.ANY, IO.ANY) @@ -779,13 +783,24 @@ class DataListPopRandom(ComfyNodeABC): @classmethod def IS_CHANGED(cls, **kwargs): - return float("NaN") # Not equal to anything -> trigger recalculation + seed = kwargs.get("seed") + if seed is None: + return float("NaN") # Not equal to anything -> trigger recalculation + # INPUT_IS_LIST wraps scalars in a list + if isinstance(seed, list): + seed = seed[0] if seed else None + if seed is None: + return float("NaN") + return seed def pop_random_element(self, **kwargs: list[Any]) -> tuple[list[Any], Any]: - from random import randrange + import random input_list = kwargs.get('list', []).copy() + seed_values = kwargs.get('seed') + seed = seed_values[0] if seed_values is not None else None + rng = random.Random(seed) if seed is not None else random if input_list: - random_element = input_list.pop(randrange(len(input_list))) + random_element = input_list.pop(rng.randrange(len(input_list))) return input_list, random_element return input_list, None diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index 0f01ebd..b0084d9 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -826,13 +826,17 @@ class DictPopRandom(ComfyNodeABC): 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. + An optional seed can be provided for reproducible selection. """ @classmethod def INPUT_TYPES(cls): return { "required": { "input_dict": ("DICT", {}), - } + }, + "optional": { + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}), + }, } RETURN_TYPES = ("DICT", IO.STRING, IO.ANY, IO.BOOLEAN) @@ -843,15 +847,19 @@ class DictPopRandom(ComfyNodeABC): @classmethod def IS_CHANGED(cls, **kwargs): - return float("NaN") # Not equal to anything -> trigger recalculation + seed = kwargs.get("seed") + if seed is None: + return float("NaN") # Not equal to anything -> trigger recalculation + return seed - def pop_random(self, input_dict: Mapping[KT, VT]) -> tuple[Mapping[KT, VT], _U[KT, str], _O[VT], bool]: + def pop_random(self, input_dict: Mapping[KT, VT], seed: _O[int] = None) -> tuple[Mapping[KT, VT], _U[KT, str], _O[VT], bool]: if not input_dict: return input_dict, "", None, False + rng = random.Random(seed) if seed is not None else random result = dict(input_dict) try: - random_key = random.choice(list(result.keys())) + random_key = rng.choice(list(result.keys())) random_value = result.pop(random_key) except Exception: return input_dict, "", None, False diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index c2344ea..ef96439 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -595,13 +595,17 @@ class ListPopRandom(ComfyNodeABC): 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. + An optional seed can be provided for reproducible selection. """ @classmethod def INPUT_TYPES(cls): return { "required": { "list": ("LIST", {}), - } + }, + "optional": { + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}), + }, } RETURN_TYPES = ("LIST", IO.ANY) @@ -612,13 +616,17 @@ class ListPopRandom(ComfyNodeABC): @classmethod def IS_CHANGED(cls, **kwargs): - return float("NaN") # Not equal to anything -> trigger recalculation + seed = kwargs.get("seed") + if seed is None: + return float("NaN") # Not equal to anything -> trigger recalculation + return seed - def pop_random_element(self, list: list[Any]) -> tuple[list[Any], Any]: + def pop_random_element(self, list: list[Any], seed=None) -> tuple[list[Any], Any]: import random + rng = random.Random(seed) if seed is not None else random result = list.copy() if result: - random_index = random.randrange(len(result)) + random_index = rng.randrange(len(result)) random_element = result.pop(random_index) return result, random_element return result, None diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index 774c0ce..f704cea 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -505,13 +505,17 @@ class SetPopRandom(ComfyNodeABC): 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. + An optional seed can be provided for reproducible selection. """ @classmethod def INPUT_TYPES(cls): return { "required": { "set": ("SET", {}), - } + }, + "optional": { + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}), + }, } RETURN_TYPES = ("SET", IO.ANY) @@ -522,13 +526,17 @@ class SetPopRandom(ComfyNodeABC): @classmethod def IS_CHANGED(cls, **kwargs): - return float("NaN") # Not equal to anything -> trigger recalculation + seed = kwargs.get("seed") + if seed is None: + return float("NaN") # Not equal to anything -> trigger recalculation + return seed - def pop_random_element(self, set: set[Any]) -> tuple[set[Any], Any]: + def pop_random_element(self, set: set[Any], seed=None) -> tuple[set[Any], Any]: import random + rng = random.Random(seed) if seed is not None else random result = set.copy() if result: - random_element = random.choice(list(result)) + random_element = rng.choice(list(result)) result.remove(random_element) return result, random_element return result, None diff --git a/tests/test_data_list_nodes.py b/tests/test_data_list_nodes.py index 09a3eb0..e1e8131 100644 --- a/tests/test_data_list_nodes.py +++ b/tests/test_data_list_nodes.py @@ -303,6 +303,13 @@ def test_pop_random(): # Empty list case assert node.pop_random_element(list=[]) == ([], None) + # Same seed should produce the same popped item + original_list = [1, 2, 3, 4] + result1, item1 = node.pop_random_element(list=original_list, seed=[42]) + result2, item2 = node.pop_random_element(list=original_list, seed=[42]) + assert item1 == item2 + assert result1 == result2 + def test_first(): node = DataListFirst() diff --git a/tests/test_dict_nodes.py b/tests/test_dict_nodes.py index a47b7a2..36c0a46 100644 --- a/tests/test_dict_nodes.py +++ b/tests/test_dict_nodes.py @@ -151,6 +151,11 @@ def test_dict_pop_random(dict_type): assert empty_value is None assert empty_success is False + # Same seed should produce the same popped key/value + result1 = node.pop_random(my_dict, seed=42) + result2 = node.pop_random(my_dict, seed=42) + assert result1 == result2 + @pytest.mark.parametrize("dict_type", _tested_dict_types) def test_dict_keys(dict_type): diff --git a/tests/test_list_nodes.py b/tests/test_list_nodes.py index 018ed71..8c70a49 100644 --- a/tests/test_list_nodes.py +++ b/tests/test_list_nodes.py @@ -87,6 +87,13 @@ def test_list_pop_random(): # Test with empty list assert node.pop_random_element([]) == ([], None) + # Same seed should produce the same popped item + result1, item1 = node.pop_random_element(original_list, seed=42) + result2, item2 = node.pop_random_element(original_list, seed=42) + assert item1 == item2 + assert result1 == result2 + assert original_list == [1, 2, 3, 4] # Input not mutated + def test_list_first(): node = ListFirst() diff --git a/tests/test_set_nodes.py b/tests/test_set_nodes.py index dfb2276..e6d47e0 100644 --- a/tests/test_set_nodes.py +++ b/tests/test_set_nodes.py @@ -132,6 +132,13 @@ def test_set_pop_random(): empty_set = set() assert node.pop_random_element(empty_set) == (set(), None) + # Same seed should produce the same popped item for the same set contents + seeded_set = {1, 2, 3, 4} + result1, item1 = node.pop_random_element(seeded_set, seed=42) + result2, item2 = node.pop_random_element(seeded_set, seed=42) + assert item1 == item2 + assert result1 == result2 + def test_set_union(): node = SetUnion()