From 1deac10f157e2f5c5d70041fcf638809312c6d19 Mon Sep 17 00:00:00 2001 From: StableLlama Date: Fri, 11 Sep 2026 11:02:43 +0200 Subject: [PATCH] bump version to 2.0.0 - Critical change that ensures reliablity and **should** have no bad side effects, but when you see any, please report: DICT, LIST, SET and Data List nodes (plus the DICT/LIST-taking STRING nodes) now degrade gracefully when their data source is not connected (`None`) instead of raising: read/query nodes return their natural "no data" result (the `get` default, an empty list, `False`, `0`, `None`), and transform nodes behave as if given an empty container. - Add a "continue if not empty" flow-control node: it passes a value through unchanged when it is non-empty, and blocks execution (via an execution blocker) when the value is empty. It is list-aware (`INPUT_IS_LIST` / `OUTPUT_IS_LIST`), so it is immune to ComfyUI's empty-input batching crash and passes data lists through unchanged. By default the block is silent; an optional `message` can be set to show a dialog. Use it before nodes that cannot handle an empty input (e.g. a node receiving an empty Data List) to stop execution instead of failing. --- .github/workflows/publish_node.yml | 12 +- CHANGELOG.md | 16 ++- pyproject.toml | 2 +- src/basic_data_handling/control_flow_nodes.py | 61 ++++++++++ src/basic_data_handling/data_list_nodes.py | 104 +++++++++--------- src/basic_data_handling/dict_nodes.py | 33 +++++- src/basic_data_handling/list_nodes.py | 23 ++++ src/basic_data_handling/set_nodes.py | 32 ++++++ src/basic_data_handling/string_nodes.py | 3 + tests/test_control_flow_nodes.py | 22 +++- tests/test_data_list_nodes.py | 32 ++++++ tests/test_dict_nodes.py | 41 +++++++ tests/test_list_nodes.py | 29 +++++ tests/test_set_nodes.py | 24 ++++ tests/test_string_nodes.py | 17 +++ 15 files changed, 391 insertions(+), 60 deletions(-) diff --git a/.github/workflows/publish_node.yml b/.github/workflows/publish_node.yml index 0e386be..e7ebef9 100644 --- a/.github/workflows/publish_node.yml +++ b/.github/workflows/publish_node.yml @@ -39,14 +39,18 @@ jobs: changelog = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8") # Match headings like "## [1.7.0] - 2026-08-27", "## 1.7.0" or "## v1.7.0". - heading = re.compile(rf"^##\s+\[?v?{re.escape(version)}\]?\s*(?:-\s*\d{{4}}-\d{{2}}-\d{{2}})?\s*$", re.M) - match = heading.search(changelog) + version_heading = re.compile(rf"^##\s+\[?v?{re.escape(version)}\]?\s*(?:-\s*\d{{4}}-\d{{2}}-\d{{2}})?\s*$", re.M) + # Any second-level changelog heading. This locates the *next* (older) + # release so we capture only this version's section, not the whole log. + next_heading = re.compile(r"^##\s+", re.M) + + match = version_heading.search(changelog) if match is None: print(f"::notice::No CHANGELOG.md section found for v{version}; publishing without a changelog") pathlib.Path("CHANGELOG_CURRENT.md").write_text("", encoding="utf-8") else: - end = heading.search(changelog, match.end()) - section = changelog[match.end():end.start() if end else None].strip() + end = next_heading.search(changelog, match.end()) + section = changelog[match.end(): end.start() if end else None].strip() pathlib.Path("CHANGELOG_CURRENT.md").write_text(section + "\n", encoding="utf-8") EOF diff --git a/CHANGELOG.md b/CHANGELOG.md index 9199ea7..b402737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [2.0.0] - 2026-09-11 +- Critical change that ensures reliablity and **should** have no bad side + effects, but when you see any, please report: + DICT, LIST, SET and Data List nodes (plus the DICT/LIST-taking STRING nodes) + now degrade gracefully when their data source is not connected (`None`) instead + of raising: read/query nodes return their natural "no data" result (the `get` + default, an empty list, `False`, `0`, `None`), and transform nodes behave as if + given an empty container. +- Add a "continue if not empty" flow-control node: it passes a value through + unchanged when it is non-empty, and blocks execution (via an execution blocker) + when the value is empty. It is list-aware (`INPUT_IS_LIST` / `OUTPUT_IS_LIST`), + so it is immune to ComfyUI's empty-input batching crash and passes data lists + through unchanged. By default the block is silent; an optional `message` can be + set to show a dialog. Use it before nodes that cannot handle an empty input + (e.g. a node receiving an empty Data List) to stop execution instead of failing. - Add "create DICT from JSON string" and "create LIST from JSON string" nodes to parse a multiline JSON object/array into a DICT/LIST - "save IMAGE to file" and "save IMAGE+MASK to file": `format` is now a dropdown diff --git a/pyproject.toml b/pyproject.toml index 68552ad..2a9334a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "1.8.1" +version = "2.0.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/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index f550a75..96d64c1 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -213,6 +213,65 @@ class SwitchCase(ComfyNodeABC): return (kwargs.get("default"),) +class ContinueIfNotEmpty(ComfyNodeABC): + """ + Passes a value through only when it is not empty; otherwise blocks execution. + + This node guards against feeding an empty collection (None, empty data list, + empty LIST/DICT/SET, etc.) into downstream nodes that cannot handle an empty + input — e.g. ComfyUI's own list-mapping fails with an empty input list. + + When the value is non-empty it is passed through unchanged; when it is empty an + execution blocker is returned so downstream nodes do not run. It is especially + useful placed before a node such as "get" / "filter" / "list" that would + otherwise crash on an empty input (for example an empty Data List). + + A value counts as empty when it is falsy: ``None``, ``[]``, ``{}``, ``set()``, + ``""``, ``0`` or ``False``. + + By default the block is silent; set ``message`` to show a dialog (like + "continue flow") when the execution is blocked. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": (IO.ANY, {"tooltip": "The value to guard; execution is blocked when the value is empty."}), + }, + "optional": { + "message": (IO.STRING, {"default": "", "tooltip": "Optional message shown when the flow is blocked. Leave empty for silent operation."}), + } + } + + RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("value",) + OUTPUT_TOOLTIPS = ("The input value, or an execution blocker when the value is empty.",) + CATEGORY = "Basic/flow control" + DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "execute" + INPUT_IS_LIST = True + OUTPUT_IS_LIST = (True,) + + def execute(self, value, message="") -> tuple[Any]: + # INPUT_IS_LIST hands the raw inputs over (a data list or a single value), + # so ComfyUI never slices them — this node is immune to the empty-list + # batching crash it guards against. `message` arrives as a list under + # ComfyUI, but may be a plain string when called directly. + if value is None: + value = [] + msg = message[0] if isinstance(message, (list, tuple)) and message else (message or "") + if isinstance(value, list): + empty = (len(value) == 0) + else: + empty = (not value) + if empty: + return (ExecutionBlocker(msg if msg else None),) + # OUTPUT_IS_LIST expects a list, so wrap a single non-list value. + if not isinstance(value, list): + value = [value] + return (value,) + + class ContinueFlow(ComfyNodeABC): """ Conditionally enable or disable a flow. @@ -380,6 +439,7 @@ NODE_CLASS_MAPPINGS = { "Basic data handling: IfElse": IfElse, "Basic data handling: IfElifElse": IfElifElse, "Basic data handling: SwitchCase": SwitchCase, + "Basic data handling: ContinueIfNotEmpty": ContinueIfNotEmpty, "Basic data handling: ContinueFlow": ContinueFlow, "Basic data handling: FlowSelect": FlowSelect, "Basic data handling: ForceCalculation": ForceCalculation, @@ -391,6 +451,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Basic data handling: IfElse": "if/else", "Basic data handling: IfElifElse": "if/elif/.../else", "Basic data handling: SwitchCase": "switch/case", + "Basic data handling: ContinueIfNotEmpty": "continue if not empty", "Basic data handling: ContinueFlow": "continue flow", "Basic data handling: FlowSelect": "flow select", "Basic data handling: ForceCalculation": "force calculation", diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index e045317..803d3a1 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -212,7 +212,7 @@ class DataListAll(ComfyNodeABC): INPUT_IS_LIST = True def check_all(self, **kwargs: list[Any]) -> tuple[bool]: - return (all(kwargs.get('list', [])),) + return (all(kwargs.get('list') or []),) class DataListAny(ComfyNodeABC): @@ -238,7 +238,7 @@ class DataListAny(ComfyNodeABC): INPUT_IS_LIST = True def check_any(self, **kwargs: list[Any]) -> tuple[bool]: - return (any(kwargs.get('list', [])),) + return (any(kwargs.get('list') or []),) class DataListAppend(ComfyNodeABC): @@ -267,8 +267,8 @@ class DataListAppend(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def append(self, **kwargs: list[Any]) -> tuple[list[Any]]: - result = kwargs.get('list', []).copy() - item = kwargs.get('item', []) + result = (kwargs.get('list') or []).copy() + item = kwargs.get('item') or [] if len(item) > 0: result.append(item[0]) return (result,) @@ -299,10 +299,10 @@ class DataListContains(ComfyNodeABC): INPUT_IS_LIST = True def contains(self, **kwargs: list[Any]) -> tuple[bool]: - value = kwargs.get('value', []) + value = kwargs.get('value') or [] if len(value) == 0: return (False,) - return (value[0] in kwargs.get('list', []),) + return (value[0] in (kwargs.get('list') or []),) class DataListCount(ComfyNodeABC): @@ -330,8 +330,8 @@ class DataListCount(ComfyNodeABC): INPUT_IS_LIST = True def count(self, **kwargs: list[Any]) -> tuple[int]: - value = kwargs.get('value', [None])[0] - return (kwargs.get('list', []).count(value),) + value = (kwargs.get('value') or [None])[0] + return ((kwargs.get('list') or []).count(value),) class DataListEnumerate(ComfyNodeABC): @@ -361,8 +361,8 @@ class DataListEnumerate(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def enumerate_list(self, **kwargs: list[Any]) -> tuple[list]: - input_list = kwargs.get('list', []) - start = kwargs.get('start', [0])[0] + input_list = kwargs.get('list') or [] + start = (kwargs.get('start') or [0])[0] return ([list(item) for item in enumerate(input_list, start=start)],) @@ -392,7 +392,7 @@ class DataListExtend(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def extend(self, **kwargs: list[Any]) -> tuple[list[Any]]: - return (kwargs.get('list_a', []) + kwargs.get('list_b', []),) + return ((kwargs.get('list_a') or []) + (kwargs.get('list_b') or []),) class DataListFilter(ComfyNodeABC): @@ -425,8 +425,8 @@ class DataListFilter(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def filter_data(self, **kwargs: list[Any]) -> tuple[list[Any]]: - values = kwargs.get('value', []) - filters = kwargs.get('filter', []) + values = kwargs.get('value') or [] + filters = kwargs.get('filter') or [] # Create a new list with only items where the filter is False result = [_val for _val, _filter in zip(values, filters) if not _filter] @@ -464,8 +464,8 @@ class DataListFilterSelect(ComfyNodeABC): OUTPUT_IS_LIST = (True, True,) def select(self, **kwargs: list[Any]) -> tuple[list[Any]]: - values = kwargs.get('value', []) - selects = kwargs.get('select', []) + values = kwargs.get('value') or [] + selects = kwargs.get('select') or [] # Create a new list with only items where the filter is False result_true, result_false = [], [] @@ -499,7 +499,7 @@ class DataListFirst(ComfyNodeABC): INPUT_IS_LIST = True def get_first_element(self, **kwargs: list[Any]) -> tuple[Any]: - input_list = kwargs.get('list', []) + input_list = kwargs.get('list') or [] return (input_list[0] if input_list else None,) @@ -529,9 +529,9 @@ class DataListGetItem(ComfyNodeABC): INPUT_IS_LIST = True def get_item(self, **kwargs: list[Any]) -> tuple[Any]: - index = kwargs.get('index', [0])[0] + index = (kwargs.get('index') or [0])[0] try: - return (kwargs.get('list', [])[index],) + return ((kwargs.get('list') or [])[index],) except IndexError: return (None,) @@ -566,10 +566,10 @@ class DataListIndex(ComfyNodeABC): INPUT_IS_LIST = True def list_index(self, **kwargs: list[Any]) -> tuple[int]: - input_list = kwargs.get('list', []) - value = kwargs.get('value', [None])[0] - start = kwargs.get('start', [0])[0] - end = kwargs.get('end', [-1])[0] + input_list = kwargs.get('list') or [] + value = (kwargs.get('value') or [None])[0] + start = (kwargs.get('start') or [0])[0] + end = (kwargs.get('end') or [-1])[0] if end == -1: end = len(input_list) @@ -606,8 +606,8 @@ class DataListInsert(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def insert(self, **kwargs: list[Any]) -> tuple[list[Any]]: - result = kwargs.get('list', []).copy() - result.insert(kwargs.get('index', [0])[0], kwargs.get('item', [None])[0]) + result = (kwargs.get('list') or []).copy() + result.insert((kwargs.get('index') or [0])[0], (kwargs.get('item') or [None])[0]) return (result,) @@ -635,7 +635,7 @@ class DataListLast(ComfyNodeABC): INPUT_IS_LIST = True def get_last_element(self, **kwargs: list[Any]) -> tuple[Any]: - input_list = kwargs.get('list', []) + input_list = kwargs.get('list') or [] return (input_list[-1] if input_list else None,) @@ -662,7 +662,7 @@ class DataListLength(ComfyNodeABC): INPUT_IS_LIST = True def length(self, **kwargs: list[Any]) -> tuple[int]: - return (len(kwargs.get('list', [])),) + return (len(kwargs.get('list') or []),) class DataListMax(ComfyNodeABC): @@ -690,7 +690,7 @@ class DataListMax(ComfyNodeABC): INPUT_IS_LIST = True def find_max(self, **kwargs: list[Any]) -> tuple[Any]: - values = kwargs.get('list', []) + values = kwargs.get('list') or [] if not values: return (None,) @@ -727,7 +727,7 @@ class DataListMin(ComfyNodeABC): INPUT_IS_LIST = True def find_min(self, **kwargs: list[Any]) -> tuple[Any]: - values = kwargs.get('list', []) + values = kwargs.get('list') or [] if not values: return (None,) @@ -769,8 +769,8 @@ class DataListPop(ComfyNodeABC): OUTPUT_IS_LIST = (True, False) def pop(self, **kwargs: list[Any]) -> tuple[list[Any], Any]: - result = kwargs.get('list', []).copy() - index = kwargs.get('index', [-1])[0] + result = (kwargs.get('list') or []).copy() + index = (kwargs.get('index') or [-1])[0] try: item = result.pop(index) return result, item @@ -820,7 +820,7 @@ class DataListPopRandom(ComfyNodeABC): def pop_random_element(self, **kwargs: list[Any]) -> tuple[list[Any], Any]: import random - input_list = kwargs.get('list', []).copy() + input_list = (kwargs.get('list') or []).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 @@ -889,8 +889,8 @@ class DataListRemove(ComfyNodeABC): OUTPUT_IS_LIST = (True, False,) def remove(self, **kwargs: list[Any]) -> tuple[list[Any], bool]: - result = kwargs.get('list', []).copy() - value = kwargs.get('value', []) + result = (kwargs.get('list') or []).copy() + value = kwargs.get('value') or [] try: result.remove(value[0]) return result, True @@ -922,7 +922,7 @@ class DataListReverse(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def reverse(self, **kwargs: list[Any]) -> tuple[list[Any]]: - result = kwargs.get('list', []).copy() + result = (kwargs.get('list') or []).copy() result.reverse() return (result,) @@ -954,9 +954,9 @@ class DataListSetItem(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def set_item(self, **kwargs: list[Any]) -> tuple[Any]: - input_list = kwargs.get('list', []) - index = kwargs.get('index', [0])[0] - value = kwargs.get('value', [None])[0] + input_list = kwargs.get('list') or [] + index = (kwargs.get('index') or [0])[0] + value = (kwargs.get('value') or [None])[0] try: result = input_list.copy() result[index] = value @@ -991,8 +991,8 @@ class DataListShuffle(ComfyNodeABC): def shuffle_list(self, **kwargs: list[Any]) -> tuple[list[Any]]: import random - input_list = kwargs.get('list', []) - seed = kwargs.get('seed', [0])[0] + input_list = kwargs.get('list') or [] + seed = (kwargs.get('seed') or [0])[0] random.seed(seed) result = input_list.copy() random.shuffle(result) @@ -1029,10 +1029,10 @@ class DataListSlice(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def slice(self, **kwargs: list[Any]) -> tuple[list[Any]]: - input_list = kwargs.get('list', []) - start = kwargs.get('start', [0])[0] - stop = kwargs.get('stop', [INT_MAX])[0] - step = kwargs.get('step', [1])[0] + input_list = kwargs.get('list') or [] + start = (kwargs.get('start') or [0])[0] + stop = (kwargs.get('stop') or [INT_MAX])[0] + step = (kwargs.get('step') or [1])[0] return (input_list[start:stop:step],) @@ -1066,9 +1066,9 @@ class DataListSort(ComfyNodeABC): def sort(self, **kwargs: list[Any]) -> tuple[list[Any]]: # Convert string to boolean - reverse = kwargs.get('reverse', ["False"])[0] == "True" + reverse = (kwargs.get('reverse') or ["False"])[0] == "True" - result = sorted(kwargs.get('list', []), reverse=reverse) + result = sorted(kwargs.get('list') or [], reverse=reverse) return (result,) @@ -1098,8 +1098,8 @@ class DataListSum(ComfyNodeABC): INPUT_IS_LIST = True def sum_list(self, **kwargs: list[Any]) -> tuple[int, float]: - input_list = kwargs.get('list', []) - start = kwargs.get('start', [0])[0] + input_list = kwargs.get('list') or [] + start = (kwargs.get('start') or [0])[0] result = sum(input_list, start) return int(result), float(result) @@ -1135,13 +1135,13 @@ class DataListZip(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def zip_lists(self, **kwargs: list[Any]) -> tuple[list[Any]]: - lists = [kwargs.get('list1', []), kwargs.get('list2', [])] + lists = [(kwargs.get('list1') or []), (kwargs.get('list2') or [])] if 'list3' in kwargs: - lists.append(kwargs['list3']) + lists.append(kwargs['list3'] or []) if 'list4' in kwargs: - lists.append(kwargs['list4']) + lists.append(kwargs['list4'] or []) # Zip the lists together and convert each tuple to a list result = [list(item) for item in zip(*lists)] @@ -1172,7 +1172,7 @@ class DataListToList(ComfyNodeABC): INPUT_IS_LIST = True def convert(self, **kwargs: list[Any]) -> tuple[list[Any]]: - return (list(kwargs.get('list', [])).copy(),) + return (list(kwargs.get('list') or []).copy(),) class DataListToSet(ComfyNodeABC): @@ -1199,7 +1199,7 @@ class DataListToSet(ComfyNodeABC): INPUT_IS_LIST = True def convert(self, **kwargs: list[Any]) -> tuple[set[Any]]: - return (set(kwargs.get('list', [])),) + return (set(kwargs.get('list') or []),) NODE_CLASS_MAPPINGS = { diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index e404d70..1c48746 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -246,7 +246,7 @@ class DictCreateFromItemsDataList(ComfyNodeABC): def create_from_items(self, **kwargs: list[Any]) -> tuple[dict]: try: # Check if items are valid (key-value pairs) - items = kwargs.get('item', []) + items = kwargs.get('item') or [] for item in items: if not isinstance(item, tuple) and len(item) != 2: raise ValueError("Each item must be a (key, value) pair") @@ -280,6 +280,7 @@ class DictCreateFromItemsList(ComfyNodeABC): def create_from_items(self, items: list) -> tuple[dict]: try: # Check if items are valid (key-value pairs) + items = items or [] for item in items: if not isinstance(item, tuple) and len(item) != 2: raise ValueError("Each item must be a (key, value) pair") @@ -315,6 +316,8 @@ class DictCreateFromLists(ComfyNodeABC): def create_from_lists(self, keys: List[KT], values: List[VT]) -> tuple[Dict[KT, VT]]: # Pair keys with values up to the length of the shorter list + keys = keys or [] + values = values or [] result = dict(zip(keys, values)) return (result,) @@ -344,6 +347,9 @@ class DictCompare(ComfyNodeABC): FUNCTION = "compare" def compare(self, dict1: dict, dict2: dict) -> tuple[bool, list, list, list]: + dict1 = dict1 or {} + dict2 = dict2 or {} + are_equal = (dict1 == dict2) keys1 = set(dict1.keys()) @@ -384,6 +390,7 @@ class DictContainsKey(ComfyNodeABC): FUNCTION = "contains_key" def contains_key(self, input_dict: dict, key: str) -> tuple[bool]: + input_dict = input_dict or {} return (key in input_dict,) @@ -411,6 +418,9 @@ class DictExcludeKeys(ComfyNodeABC): FUNCTION = "exclude_keys" def exclude_keys(self, input_dict: Mapping[KT, VT], keys_to_exclude: list) -> tuple[Mapping[KT, VT]]: + input_dict = input_dict if input_dict is not None else {} + keys_to_exclude = keys_to_exclude or [] + if not(input_dict and keys_to_exclude): return (input_dict,) @@ -462,6 +472,9 @@ class DictFilterByKeys(ComfyNodeABC): FUNCTION = "filter_by_keys" def filter_by_keys(self, input_dict: Mapping[KT, VT], keys: list) -> tuple[Mapping[KT, VT]]: + input_dict = input_dict if input_dict is not None else {} + keys = keys or [] + if not input_dict: return (input_dict,) if not keys: @@ -518,6 +531,7 @@ class DictFromKeys(ComfyNodeABC): FUNCTION = "from_keys" def from_keys(self, keys: List[KT], value: VT = None) -> tuple[Dict[KT, VT]]: + keys = keys or [] return (dict.fromkeys(keys, value),) @@ -549,6 +563,7 @@ class DictGet(ComfyNodeABC): FUNCTION = "get" def get(self, input_dict: Mapping[Any, VT], key: Any, default: T = None) -> tuple[_U[VT, T]]: + input_dict = input_dict if input_dict is not None else {} return (input_dict.get(key, default),) @@ -575,6 +590,7 @@ class DictGetKeysValues(ComfyNodeABC): FUNCTION = "get_keys_values" def get_keys_values(self, input_dict: Mapping[KT, VT]) -> tuple[List[KT], List[VT]]: + input_dict = input_dict if input_dict is not None else {} keys = list(input_dict.keys()) values = list(input_dict.values()) return keys, values @@ -611,6 +627,8 @@ class DictGetMultiple(ComfyNodeABC): self, input_dict: Mapping[Any, VT], keys: list, default: T = None ) -> tuple[List[_U[VT, T]]]: + input_dict = input_dict if input_dict is not None else {} + keys = keys or [] values = [input_dict.get(key, default) for key in keys] return (values,) @@ -639,6 +657,7 @@ class DictInvert(ComfyNodeABC): FUNCTION = "invert" def invert(self, input_dict: Mapping[KT, VT]) -> tuple[Mapping[VT, KT], bool]: + input_dict = input_dict if input_dict is not None else {} try: inverted = {v: k for k, v in input_dict.items()} except Exception: @@ -672,6 +691,7 @@ class DictItems(ComfyNodeABC): FUNCTION = "items" def items(self, input_dict: Mapping[KT, VT]) -> tuple[List[Tuple[KT, VT]]]: + input_dict = input_dict if input_dict is not None else {} return (list(input_dict.items()),) @@ -697,6 +717,7 @@ class DictKeys(ComfyNodeABC): FUNCTION = "keys" def keys(self, input_dict: Mapping[KT, Any]) -> tuple[List[KT]]: + input_dict = input_dict if input_dict is not None else {} return (list(input_dict.keys()),) @@ -722,6 +743,7 @@ class DictLength(ComfyNodeABC): FUNCTION = "length" def length(self, input_dict: Mapping) -> tuple[int]: + input_dict = input_dict if input_dict is not None else {} return (len(input_dict),) @@ -759,6 +781,7 @@ class DictMerge(ComfyNodeABC): dict3: Mapping[KT3, VT3] = None, dict4: Mapping[KT4, VT4] = None, ) -> tuple[Mapping[_U[KT, KT2, KT3, KT4], _U[VT, VT2, VT3, VT4]]]: + dict1 = dict1 if dict1 is not None else {} extra_dicts = [x for x in (dict2, dict3, dict4) if x is not None] if not extra_dicts: return (dict1,) @@ -803,6 +826,7 @@ class DictPop(ComfyNodeABC): FUNCTION = "pop" def pop(self, input_dict: Mapping[KT, VT], key: Any, default_value: T = None) -> tuple[Mapping[KT, VT], _U[VT, T]]: + input_dict = input_dict if input_dict is not None else {} if key not in input_dict: return input_dict, default_value @@ -844,6 +868,7 @@ class DictPopItem(ComfyNodeABC): FUNCTION = "popitem" def popitem(self, input_dict: Mapping[KT, VT]) -> tuple[Mapping[KT, VT], _U[KT, str], _O[VT], bool]: + input_dict = input_dict if input_dict is not None else {} if not input_dict: return input_dict, "", None, False @@ -893,6 +918,7 @@ class DictPopRandom(ComfyNodeABC): return seed def pop_random(self, input_dict: Mapping[KT, VT], seed: _O[int] = None) -> tuple[Mapping[KT, VT], _U[KT, str], _O[VT], bool]: + input_dict = input_dict if input_dict is not None else {} if not input_dict: return input_dict, "", None, False @@ -933,6 +959,7 @@ class DictRemove(ComfyNodeABC): FUNCTION = "remove" def remove(self, input_dict: Mapping[KT, VT], key: Any) -> tuple[Mapping[KT, VT], bool]: + input_dict = input_dict if input_dict is not None else {} if key not in input_dict: return input_dict, False @@ -968,6 +995,7 @@ class DictSet(ComfyNodeABC): FUNCTION = "set" def set(self, input_dict: Mapping[KT, VT], key: KT2, value: VT2) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]]]: + input_dict = input_dict if input_dict is not None else {} result = dict(input_dict) result[key] = value @@ -1001,6 +1029,7 @@ class DictSetDefault(ComfyNodeABC): FUNCTION = "setdefault" def setdefault(self, input_dict: Mapping[KT, VT], key: KT2, default_value: VT2 = None) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]], _U[VT, VT2]]: + input_dict = input_dict if input_dict is not None else {} result = dict(input_dict) value = result.setdefault(key, default_value) @@ -1033,6 +1062,7 @@ class DictUpdate(ComfyNodeABC): FUNCTION = "update" def update(self, dict1: Mapping[KT, VT], dict2: Mapping[KT2, VT2]) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]]]: + dict1 = dict1 if dict1 is not None else {} if not dict2: return (dict1,) @@ -1065,6 +1095,7 @@ class DictValues(ComfyNodeABC): FUNCTION = "values" def values(self, input_dict: Mapping[Any, VT]) -> tuple[List[VT]]: + input_dict = input_dict if input_dict is not None else {} return (list(input_dict.values()),) diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 8d6ff02..1550f26 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -176,6 +176,7 @@ class ListAll: FUNCTION = "check_all" def check_all(self, list: list[Any]) -> tuple[bool]: + list = list if list is not None else [] return (all(list),) @@ -201,6 +202,7 @@ class ListAny: FUNCTION = "check_any" def check_any(self, list: list[Any]) -> tuple[bool]: + list = list if list is not None else [] return (any(list),) @@ -228,6 +230,7 @@ class ListAppend(ComfyNodeABC): FUNCTION = "append" def append(self, list: list[Any], item: Any) -> tuple[list[Any]]: + list = list if list is not None else [] result = list.copy() result.append(item) return (result,) @@ -257,6 +260,7 @@ class ListContains(ComfyNodeABC): FUNCTION = "contains" def contains(self, list: list[Any], value: Any) -> tuple[bool]: + list = list if list is not None else [] return (value in list,) @@ -284,6 +288,7 @@ class ListCount(ComfyNodeABC): FUNCTION = "count" def count(self, list: list[Any], value: Any) -> tuple[int]: + list = list if list is not None else [] return (list.count(value),) @@ -312,6 +317,7 @@ class ListEnumerate: FUNCTION = "enumerate_list" def enumerate_list(self, list: list[Any], start: int = 0) -> tuple[list]: + list = list if list is not None else [] return ([__builtins__['list'](enumerate(list, start=start))],) @@ -339,6 +345,8 @@ class ListExtend(ComfyNodeABC): FUNCTION = "extend" def extend(self, list1: list[Any], list2: list[Any]) -> tuple[list[Any]]: + list1 = list1 if list1 is not None else [] + list2 = list2 if list2 is not None else [] result = list1.copy() result.extend(list2) return (result,) @@ -395,6 +403,7 @@ class ListGetItem(ComfyNodeABC): FUNCTION = "get_item" def get_item(self, list: list[Any], index: int) -> tuple[Any]: + list = list if list is not None else [] try: return (list[index],) except IndexError: @@ -430,6 +439,7 @@ class ListIndex(ComfyNodeABC): FUNCTION = "index" def index(self, list: list[Any], value: Any, start: int = 0, end: int = -1) -> tuple[int]: + list = list if list is not None else [] if end == -1: end = len(list) @@ -464,6 +474,7 @@ class ListInsert(ComfyNodeABC): FUNCTION = "insert" def insert(self, list: list[Any], index: int, item: Any) -> tuple[list[Any]]: + list = list if list is not None else [] result = list.copy() result.insert(index, item) return (result,) @@ -517,6 +528,7 @@ class ListLength(ComfyNodeABC): FUNCTION = "length" def length(self, list: list[Any]) -> tuple[int]: + list = list if list is not None else [] return (len(list),) @@ -614,6 +626,7 @@ class ListPop(ComfyNodeABC): FUNCTION = "pop" def pop(self, list: list[Any], index: int = -1) -> tuple[list[Any], Any]: + list = list if list is not None else [] result = list.copy() try: item = result.pop(index) @@ -657,6 +670,7 @@ class ListPopRandom(ComfyNodeABC): def pop_random_element(self, list: list[Any], seed=None) -> tuple[list[Any], Any]: import random + list = list if list is not None else [] rng = random.Random(seed) if seed is not None else random result = list.copy() if result: @@ -723,6 +737,7 @@ class ListRemove(ComfyNodeABC): FUNCTION = "remove" def remove(self, list: list[Any], value: Any) -> tuple[list[Any], bool]: + list = list if list is not None else [] result = list.copy() try: result.remove(value) @@ -753,6 +768,7 @@ class ListReverse(ComfyNodeABC): FUNCTION = "reverse" def reverse(self, list: list[Any]) -> tuple[list[Any]]: + list = list if list is not None else [] result = list.copy() result.reverse() return (result,) @@ -783,6 +799,7 @@ class ListSetItem(ComfyNodeABC): FUNCTION = "set_item" def set_item(self, list: list[Any], index: int, value: Any) -> tuple[list[Any]]: + list = list if list is not None else [] result = list.copy() try: result[index] = value @@ -815,6 +832,7 @@ class ListShuffle(ComfyNodeABC): def shuffle_list(self, list: list[Any], seed: int) -> tuple[list[Any]]: import random + list = list if list is not None else [] random.seed(seed) result = list.copy() random.shuffle(result) @@ -850,6 +868,7 @@ class ListSlice(ComfyNodeABC): def slice(self, list: list[Any], start: int = 0, stop: int = INT_MAX, step: int = 1) -> tuple[list[Any]]: + list = list if list is not None else [] return (list[start:stop:step],) class ListSort(ComfyNodeABC): @@ -880,6 +899,7 @@ class ListSort(ComfyNodeABC): def sort(self, list: list[Any], reverse: str = "False") -> tuple[list[Any]]: # Convert string to boolean reverse_bool = (reverse == "True") + list = list if list is not None else [] # Use sorted to create a new sorted list try: @@ -915,6 +935,7 @@ class ListSum: FUNCTION = "sum_list" def sum_list(self, list: list[Any], start: int = 0) -> tuple[int, float]: + list = list if list is not None else [] result = sum(list, start) return result, float(result) @@ -944,6 +965,7 @@ class ListToDataList(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def convert(self, list) -> tuple[list[Any]]: + list = list if list is not None else [] return (list,) @@ -970,6 +992,7 @@ class ListToSet(ComfyNodeABC): FUNCTION = "convert" def convert(self, list: list[Any]) -> tuple[set[Any]]: + list = list if list is not None else [] return (set(list),) diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index 3d317c8..9894ba1 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -16,6 +16,11 @@ except: from ._dynamic_input import ContainsDynamicDict +def _empty_set() -> set: + """Return an empty set, independent of a node parameter named ``set``.""" + return set() + + class SetCreate(ComfyNodeABC): """ Creates a new SET from items. @@ -175,6 +180,7 @@ class SetAdd(ComfyNodeABC): FUNCTION = "add" def add(self, set: set[Any], item: Any) -> tuple[set[Any]]: + set = set if set is not None else _empty_set() result = set.copy() result.add(item) return (result,) @@ -203,6 +209,7 @@ class SetAll(ComfyNodeABC): FUNCTION = "check_all" def check_all(self, set: set[Any]) -> tuple[bool]: + set = set if set is not None else _empty_set() return (all(set),) @@ -229,6 +236,7 @@ class SetAny(ComfyNodeABC): FUNCTION = "check_any" def check_any(self, set: set[Any]) -> tuple[bool]: + set = set if set is not None else _empty_set() return (any(set),) @@ -256,6 +264,7 @@ class SetContains(ComfyNodeABC): FUNCTION = "contains" def contains(self, set: set[Any], value: Any) -> tuple[bool]: + set = set if set is not None else _empty_set() return (value in set,) @@ -283,6 +292,8 @@ class SetDifference(ComfyNodeABC): FUNCTION = "difference" def difference(self, set1: set[Any], set2: set[Any]) -> tuple[set[Any]]: + set1 = set1 if set1 is not None else set() + set2 = set2 if set2 is not None else set() result = set1.copy() result.difference_update(set2) return (result,) @@ -312,6 +323,7 @@ class SetDiscard(ComfyNodeABC): FUNCTION = "discard" def discard(self, set: set[Any], item: Any) -> tuple[set[Any]]: + set = set if set is not None else _empty_set() result = set.copy() result.discard(item) return (result,) @@ -347,6 +359,7 @@ class SetEnumerate(ComfyNodeABC): FUNCTION = "enumerate_set" def enumerate_set(self, set: set[Any], start: int = 0) -> tuple[list]: + set = set if set is not None else _empty_set() return (list(enumerate(set, start=start)),) @@ -378,6 +391,8 @@ class SetIntersection(ComfyNodeABC): FUNCTION = "intersection" def intersection(self, set1: set[Any], set2: set[Any], set3=None, set4=None) -> tuple[set[Any]]: + set1 = set1 if set1 is not None else set() + set2 = set2 if set2 is not None else set() result = set1.copy() result.intersection_update(set2) @@ -413,6 +428,8 @@ class SetIsDisjoint(ComfyNodeABC): FUNCTION = "is_disjoint" def is_disjoint(self, set1: set[Any], set2: set[Any]) -> tuple[bool]: + set1 = set1 if set1 is not None else set() + set2 = set2 if set2 is not None else set() return (set1.isdisjoint(set2),) @@ -440,6 +457,8 @@ class SetIsSubset(ComfyNodeABC): FUNCTION = "is_subset" def is_subset(self, set1: set[Any], set2: set[Any]) -> tuple[bool]: + set1 = set1 if set1 is not None else set() + set2 = set2 if set2 is not None else set() return (set1.issubset(set2),) @@ -467,6 +486,8 @@ class SetIsSuperset(ComfyNodeABC): FUNCTION = "is_superset" def is_superset(self, set1: set[Any], set2: set[Any]) -> tuple[bool]: + set1 = set1 if set1 is not None else set() + set2 = set2 if set2 is not None else set() return (set1.issuperset(set2),) @@ -492,6 +513,7 @@ class SetLength(ComfyNodeABC): FUNCTION = "length" def length(self, set: set[Any]) -> tuple[int]: + set = set if set is not None else _empty_set() return (len(set),) @@ -519,6 +541,7 @@ class SetPop(ComfyNodeABC): FUNCTION = "pop" def pop(self, set: set[Any]) -> tuple[set[Any], Any]: + set = set if set is not None else _empty_set() result = set.copy() try: item = result.pop() @@ -562,6 +585,7 @@ class SetPopRandom(ComfyNodeABC): def pop_random_element(self, set: set[Any], seed=None) -> tuple[set[Any], Any]: import random + set = set if set is not None else _empty_set() rng = random.Random(seed) if seed is not None else random result = set.copy() if result: @@ -596,6 +620,7 @@ class SetRemove(ComfyNodeABC): FUNCTION = "remove" def remove(self, set: set[Any], item: Any) -> tuple[set[Any], bool]: + set = set if set is not None else _empty_set() result = set.copy() try: result.remove(item) @@ -634,6 +659,7 @@ class SetSum(ComfyNodeABC): FUNCTION = "sum_set" def sum_set(self, set: set[Any], start: int = 0) -> tuple[int, float]: + set = set if set is not None else _empty_set() result = sum(set, start) return result, float(result) @@ -662,6 +688,8 @@ class SetSymmetricDifference(ComfyNodeABC): FUNCTION = "symmetric_difference" def symmetric_difference(self, set1: set[Any], set2: set[Any]) -> tuple[set[Any]]: + set1 = set1 if set1 is not None else set() + set2 = set2 if set2 is not None else set() result = set1.copy() result.symmetric_difference_update(set2) return (result,) @@ -695,6 +723,8 @@ class SetUnion(ComfyNodeABC): FUNCTION = "union" def union(self, set1: set[Any], set2: set[Any], set3=None, set4=None) -> tuple[set[Any]]: + set1 = set1 if set1 is not None else set() + set2 = set2 if set2 is not None else set() result = set1.copy() result.update(set2) @@ -732,6 +762,7 @@ class SetToDataList(ComfyNodeABC): OUTPUT_IS_LIST = (True,) def convert(self, set) -> tuple[list[Any]]: + set = set if set is not None else _empty_set() return (list(set),) @@ -759,6 +790,7 @@ class SetToList(ComfyNodeABC): FUNCTION = "convert" def convert(self, set: set[Any]) -> tuple[list[Any]]: + set = set if set is not None else _empty_set() return (list(set),) diff --git a/src/basic_data_handling/string_nodes.py b/src/basic_data_handling/string_nodes.py index c147c54..1ac9e81 100644 --- a/src/basic_data_handling/string_nodes.py +++ b/src/basic_data_handling/string_nodes.py @@ -371,6 +371,7 @@ class StringFormatMap(ComfyNodeABC): FUNCTION = "format_map" def format_map(self, template, mapping): + mapping = mapping if mapping is not None else {} try: result = template.format_map(mapping) return (result,) @@ -778,6 +779,7 @@ class StringDataListJoin(ComfyNodeABC): def join(self, sep, strings): separator = sep[0] # everything comes as a list, so sep is list[str] + strings = strings if strings is not None else [] return (separator.join(strings),) @@ -806,6 +808,7 @@ class StringListJoin(ComfyNodeABC): FUNCTION = "join" def join(self, sep, strings): + strings = strings if strings is not None else [] return (sep.join(strings),) diff --git a/tests/test_control_flow_nodes.py b/tests/test_control_flow_nodes.py index 8a8d306..8c199a9 100644 --- a/tests/test_control_flow_nodes.py +++ b/tests/test_control_flow_nodes.py @@ -1,6 +1,6 @@ #import pytest from src.basic_data_handling.control_flow_nodes import ( - IfElse, SwitchCase, IfElifElse, ContinueFlow, FlowSelect, ForceCalculation, ExecutionOrder, IsConnected + IfElse, SwitchCase, IfElifElse, ContinueFlow, ContinueIfNotEmpty, FlowSelect, ForceCalculation, ExecutionOrder, IsConnected ) @@ -166,6 +166,26 @@ def test_continue_flow(): assert res == (None,) or "ExecutionBlocker" in str(res[0]) +def test_continue_if_not_empty(): + node = ContinueIfNotEmpty() + # Non-empty values pass through (single values wrapped into a list for OUTPUT_IS_LIST). + assert node.execute("some value") == (["some value"],) + assert node.execute([1, 2]) == ([1, 2],) + assert node.execute({"key": "value"}) == ([{"key": "value"}],) + assert node.execute({1, 2}) == ([{1, 2}],) + # Empty values block execution (ExecutionBlocker under ComfyUI). Silent by default. + res = node.execute(None) + assert res == (None,) or "ExecutionBlocker" in str(res[0]) + for empty in ([], {}, set(), "", 0, False): + res = node.execute(empty) + assert res == (None,) or "ExecutionBlocker" in str(res[0]) + # A provided message is passed to the blocker (as a list under INPUT_IS_LIST). + res = node.execute(None, message=["Interrupted"]) + assert res == (None,) or "ExecutionBlocker" in str(res[0]) + res = node.execute(None, message="Interrupted") + assert res == (None,) or "ExecutionBlocker" in str(res[0]) + + def test_flow_select(): node = FlowSelect() # select is True diff --git a/tests/test_data_list_nodes.py b/tests/test_data_list_nodes.py index e1e8131..8306c2c 100644 --- a/tests/test_data_list_nodes.py +++ b/tests/test_data_list_nodes.py @@ -426,3 +426,35 @@ def test_range(): # Test with ValueError (step cannot be zero) with pytest.raises(ValueError, match="Step cannot be zero"): node.create_range(start=0, stop=5, step=0) + + +def test_data_list_none_source(): + """Graceful behaviour when the Data List source is None.""" + assert DataListAll().check_all(list=None) == (True,) + assert DataListAny().check_any(list=None) == (False,) + assert DataListAppend().append(list=None, item=[1]) == ([1],) + assert DataListContains().contains(list=None, value=[1]) == (False,) + assert DataListCount().count(list=None, value=[1]) == (0,) + assert DataListEnumerate().enumerate_list(list=None) == ([],) + assert DataListExtend().extend(list_a=None, list_b=None) == ([],) + assert DataListFilter().filter_data(value=None, filter=None) == ([],) + assert DataListFilterSelect().select(value=None, select=None) == ([], []) + assert DataListFirst().get_first_element(list=None) == (None,) + assert DataListGetItem().get_item(list=None, index=[0]) == (None,) + assert DataListIndex().list_index(list=None, value=[1]) == (-1,) + assert DataListInsert().insert(list=None, index=[0], item=[1]) == ([1],) + assert DataListLast().get_last_element(list=None) == (None,) + assert DataListLength().length(list=None) == (0,) + assert DataListMax().find_max(list=None) == (None,) + assert DataListMin().find_min(list=None) == (None,) + assert DataListPop().pop(list=None) == ([], None) + assert DataListPopRandom().pop_random_element(list=None) == ([], None) + assert DataListRemove().remove(list=None, value=[1]) == ([], False) + assert DataListReverse().reverse(list=None) == ([],) + assert DataListShuffle().shuffle_list(list=None, seed=[0]) == ([],) + assert DataListSlice().slice(list=None) == ([],) + assert DataListSort().sort(list=None) == ([],) + assert DataListSum().sum_list(list=None) == (0, 0.0) + assert DataListZip().zip_lists(list1=None, list2=None) == ([],) + assert DataListToList().convert(list=None) == ([],) + assert DataListToSet().convert(list=None) == (set(),) diff --git a/tests/test_dict_nodes.py b/tests/test_dict_nodes.py index 0e021fe..96a155f 100644 --- a/tests/test_dict_nodes.py +++ b/tests/test_dict_nodes.py @@ -464,3 +464,44 @@ def test_dict_compare(a_type, b_type): # Test empty dictionaries assert node.compare(a_type({}), b_type({})) == (True, [], [], []) + + +def test_dict_none_source(): + """Graceful behaviour when the input dict/source is None.""" + # Read/query nodes return the natural "no data" result. + assert DictGet().get(None, "key") == (None,) + assert DictGet().get(None, "key", default="default") == ("default",) + assert DictGetMultiple().get_multiple(None, ["a", "b"], default="d") == (["d", "d"],) + assert DictContainsKey().contains_key(None, "key") == (False,) + assert DictLength().length(None) == (0,) + assert DictKeys().keys(None) == ([],) + assert DictValues().values(None) == ([],) + assert DictItems().items(None) == ([],) + assert DictGetKeysValues().get_keys_values(None) == ([], []) + + # Comparison treats missing sources as empty dicts. + assert DictCompare().compare(None, None) == (True, [], [], []) + assert DictCompare().compare(None, {"a": 1}) == (False, [], ["a"], []) + assert DictCompare().compare({"a": 1}, None) == (False, ["a"], [], []) + + # Transform/mutate nodes treat a missing source as an empty dict. + assert DictSet().set(None, "k", "v") == ({"k": "v"},) + assert DictRemove().remove(None, "k") == ({}, False) + assert DictSetDefault().setdefault(None, "k", "d") == ({"k": "d"}, "d") + assert DictUpdate().update(None, {"a": 1}) == ({"a": 1},) + assert DictUpdate().update(None, None) == ({},) + assert DictMerge().merge(None, {"a": 1}) == ({"a": 1},) + assert DictMerge().merge(None) == ({},) + assert DictPop().pop(None, "k", default_value="d") == ({}, "d") + assert DictPop().pop(None, "k") == ({}, None) + assert DictPopItem().popitem(None) == ({}, "", None, False) + assert DictPopRandom().pop_random(None) == ({}, "", None, False) + assert DictInvert().invert(None) == ({}, True) + + # Constructors / filters treat missing sources as empty. + assert DictFromKeys().from_keys(None, value="v") == ({},) + assert DictExcludeKeys().exclude_keys(None, ["a"]) == ({},) + assert DictFilterByKeys().filter_by_keys(None, ["a"]) == ({},) + assert DictCreateFromItemsList().create_from_items(None) == ({},) + assert DictCreateFromItemsDataList().create_from_items(item=None) == ({},) + assert DictCreateFromLists().create_from_lists(None, None) == ({},) diff --git a/tests/test_list_nodes.py b/tests/test_list_nodes.py index a6a50de..2f5b21b 100644 --- a/tests/test_list_nodes.py +++ b/tests/test_list_nodes.py @@ -404,3 +404,32 @@ def test_list_sum(): int_result, float_result = node.sum_list([1, 2.5, 3]) assert int_result == 6.5 assert float_result == 6.5 + + +def test_list_none_source(): + """Graceful behaviour when the input LIST source is None.""" + assert ListAll().check_all(None) == (True,) + assert ListAny().check_any(None) == (False,) + assert ListAppend().append(None, "item") == (["item"],) + assert ListContains().contains(None, "value") == (False,) + assert ListCount().count(None, "value") == (0,) + assert ListEnumerate().enumerate_list(None) == ([[]],) + assert ListExtend().extend(None, None) == ([],) + assert ListFirst().get_first_element(None) == (None,) + assert ListGetItem().get_item(None, 0) == (None,) + assert ListIndex().index(None, "value") == (-1,) + assert ListInsert().insert(None, 0, "item") == (["item"],) + assert ListLast().get_last_element(None) == (None,) + assert ListLength().length(None) == (0,) + assert ListMax().find_max(None) == (None,) + assert ListMin().find_min(None) == (None,) + assert ListPop().pop(None) == ([], None) + assert ListPopRandom().pop_random_element(None) == ([], None) + assert ListRemove().remove(None, "value") == ([], False) + assert ListReverse().reverse(None) == ([],) + assert ListShuffle().shuffle_list(None, seed=0) == ([],) + assert ListSlice().slice(None) == ([],) + assert ListSort().sort(None) == ([],) + assert ListSum().sum_list(None) == (0, 0.0) + assert ListToDataList().convert(None) == ([],) + assert ListToSet().convert(None) == (set(),) diff --git a/tests/test_set_nodes.py b/tests/test_set_nodes.py index e6d47e0..181e41a 100644 --- a/tests/test_set_nodes.py +++ b/tests/test_set_nodes.py @@ -349,3 +349,27 @@ def test_set_sum(): int_result, float_result = node.sum_set(set(), start=5) assert int_result == 5 assert float_result == 5.0 + + +def test_set_none_source(): + """Graceful behaviour when the input SET source is None.""" + assert SetAdd().add(None, 1) == ({1},) + assert SetAll().check_all(None) == (True,) + assert SetAny().check_any(None) == (False,) + assert SetContains().contains(None, 1) == (False,) + assert SetDifference().difference(None, None) == (set(),) + assert SetDiscard().discard(None, 1) == (set(),) + assert SetEnumerate().enumerate_set(None) == ([],) + assert SetIntersection().intersection(None, None) == (set(),) + assert SetIsDisjoint().is_disjoint(None, None) == (True,) + assert SetIsSubset().is_subset(None, None) == (True,) + assert SetIsSuperset().is_superset(None, None) == (True,) + assert SetLength().length(None) == (0,) + assert SetPop().pop(None) == (set(), None) + assert SetPopRandom().pop_random_element(None) == (set(), None) + assert SetRemove().remove(None, 1) == (set(), False) + assert SetSum().sum_set(None) == (0, 0.0) + assert SetSymmetricDifference().symmetric_difference(None, None) == (set(),) + assert SetUnion().union(None, None) == (set(),) + assert SetToDataList().convert(None) == ([],) + assert SetToList().convert(None) == ([],) diff --git a/tests/test_string_nodes.py b/tests/test_string_nodes.py index ff17247..f172ab8 100644 --- a/tests/test_string_nodes.py +++ b/tests/test_string_nodes.py @@ -403,3 +403,20 @@ def test_length(): assert node.length("hello") == (5,) assert node.length("") == (0,) # Empty string assert node.length("hello world") == (11,) # String with spaces + + +def test_join_none_source(): + # A missing (None) strings list behaves like an empty list. + node = StringDataListJoin() + assert node.join([", "], None) == ("",) + node_list = StringListJoin() + assert node_list.join(", ", None) == ("",) + + +def test_format_map_none_source(): + node = StringFormatMap() + # A missing (None) mapping behaves like an empty dict. + assert node.format_map("plain text", None) == ("plain text",) + # With placeholders a missing mapping produces the key-error message. + result = node.format_map("Hello, {name}!", None)[0] + assert result.startswith("Key error")