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.
This commit is contained in:
StableLlama
2026-09-11 11:02:43 +02:00
parent 8ff49ec895
commit 1deac10f15
15 changed files with 391 additions and 60 deletions
+8 -4
View File
@@ -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
+15 -1
View File
@@ -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
+1 -1
View File
@@ -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:
@@ -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",
+52 -52
View File
@@ -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 = {
+32 -1
View File
@@ -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()),)
+23
View File
@@ -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),)
+32
View File
@@ -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),)
+3
View File
@@ -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),)
+21 -1
View File
@@ -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
+32
View File
@@ -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(),)
+41
View File
@@ -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) == ({},)
+29
View File
@@ -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(),)
+24
View File
@@ -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) == ([],)
+17
View File
@@ -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")