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:
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()),)
|
||||
|
||||
|
||||
|
||||
@@ -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),)
|
||||
|
||||
|
||||
|
||||
@@ -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),)
|
||||
|
||||
|
||||
|
||||
@@ -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),)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),)
|
||||
|
||||
@@ -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) == ({},)
|
||||
|
||||
@@ -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(),)
|
||||
|
||||
@@ -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) == ([],)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user