Version 0.0.6

Add data list filtering, control flow and comparison.
This commit is contained in:
StableLlama
2025-05-07 23:54:32 +02:00
parent eeeedab74f
commit d31a8b4f3d
6 changed files with 595 additions and 6 deletions
+11 -1
View File
@@ -41,7 +41,7 @@ These nodes are very lightweight and require no additional dependencies.
- **data list**: ComfyUI list manipulation nodes (for processing individual items)
- Conversion: convert to LIST
- Creation: create_empty
- Modification: append, extend, insert, remove, pop, clear, set_item
- Modification: append, filter, extend, insert, remove, pop, clear, set_item
- Access: get_item, slice, index, contains
- Information: length, count
- Operations: sort, reverse, copy, zip
@@ -66,6 +66,16 @@ These nodes are very lightweight and require no additional dependencies.
- **BOOLEAN**: Boolean logic nodes
- Logic operations: and, or, not, xor, nand, nor
- **Comparison**: Value comparison nodes
- Basic comparisons: equal (==), not equal (!=), greater than (>), less than (<), etc.
- String comparison: case-sensitive/insensitive string comparison with various operators
- Special comparisons: number in range, is null, compare length
- Container operations: length comparison for strings, lists, and other containers
- **Flow Control**: Workflow control nodes
- Conditional: if/else for branching logic based on conditions
- Selection: switch/case for selecting from multiple options based on an index
## Understanding LIST vs. data list vs. SET
ComfyUI has different data types that serve different purposes:
+3 -2
View File
@@ -4,10 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "basic_data_handling"
version = "0.0.5"
version = "0.0.6"
description = """Basic Python functions for manipulating data that every programmer is used to.
Currently supported ComfyUI data types: BOOLEAN, FLOAT, INT, STRING and data lists.
Additionally supported Python data types as custom data types: DICT, LIST."""
Additionally supported Python data types as custom data types: DICT, LIST.
Data handling is supported with comparisons and control flow operations."""
authors = [
{name = "Basic data handling"}
]
+6 -2
View File
@@ -1,8 +1,10 @@
from . import (boolean_nodes, data_list_nodes, dict_nodes, float_nodes, int_nodes,
list_nodes, set_nodes, string_nodes)
from . import (boolean_nodes, comparison_nodes, control_flow, data_list_nodes, dict_nodes,
float_nodes, int_nodes, list_nodes, set_nodes, string_nodes)
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(boolean_nodes.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(comparison_nodes.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(control_flow.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(data_list_nodes.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(dict_nodes.NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(float_nodes.NODE_CLASS_MAPPINGS)
@@ -13,6 +15,8 @@ NODE_CLASS_MAPPINGS.update(string_nodes.NODE_CLASS_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS.update(boolean_nodes.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(comparison_nodes.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(control_flow.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(data_list_nodes.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(dict_nodes.NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(float_nodes.NODE_DISPLAY_NAME_MAPPINGS)
+404
View File
@@ -0,0 +1,404 @@
from typing import Any
from inspect import cleandoc
class Equal:
"""
Checks if two values are equal.
This node takes two inputs of any type and returns True if they are equal,
and False otherwise. For complex objects, structural equality is tested.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value1": ("*", {}),
"value2": ("*", {}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool:
return True
def compare(self, value1: Any, value2: Any) -> tuple[bool]:
return (value1 == value2,)
class NotEqual:
"""
Checks if two values are not equal.
This node takes two inputs of any type and returns True if they are not equal,
and False otherwise. For complex objects, structural inequality is tested.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value1": ("*", {}),
"value2": ("*", {}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool:
return True
def compare(self, value1: Any, value2: Any) -> tuple[bool]:
return (value1 != value2,)
class GreaterThan:
"""
Checks if the first value is greater than the second.
This node takes two numerical inputs and returns True if the first value
is greater than the second value, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value1": (["FLOAT", "INT"], {}),
"value2": (["FLOAT", "INT"], {}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["value1"] not in ("FLOAT", "INT"):
return "value1 must be a FLOAT or INT type"
if input_types["value2"] not in ("FLOAT", "INT"):
return "value2 must be a FLOAT or INT type"
return True
def compare(self, value1: float, value2: float) -> tuple[bool]:
return (value1 > value2,)
class LessThan:
"""
Checks if the first value is less than the second.
This node takes two numerical inputs and returns True if the first value
is less than the second value, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value1": (["FLOAT", "INT"], {}),
"value2": (["FLOAT", "INT"], {}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["value1"] not in ("FLOAT", "INT"):
return "value1 must be a FLOAT or INT type"
if input_types["value2"] not in ("FLOAT", "INT"):
return "value2 must be a FLOAT or INT type"
return True
def compare(self, value1: float, value2: float) -> tuple[bool]:
return (value1 < value2,)
class GreaterThanOrEqual:
"""
Checks if the first value is greater than or equal to the second.
This node takes two numerical inputs and returns True if the first value
is greater than or equal to the second value, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value1": (["FLOAT", "INT"], {}),
"value2": (["FLOAT", "INT"], {}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["value1"] not in ("FLOAT", "INT"):
return "value1 must be a FLOAT or INT type"
if input_types["value2"] not in ("FLOAT", "INT"):
return "value2 must be a FLOAT or INT type"
return True
def compare(self, value1: float, value2: float) -> tuple[bool]:
return (value1 >= value2,)
class LessThanOrEqual:
"""
Checks if the first value is less than or equal to the second.
This node takes two numerical inputs and returns True if the first value
is less than or equal to the second value, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value1": (["FLOAT", "INT"], {}),
"value2": (["FLOAT", "INT"], {}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["value1"] not in ("FLOAT", "INT"):
return "value1 must be a FLOAT or INT type"
if input_types["value2"] not in ("FLOAT", "INT"):
return "value2 must be a FLOAT or INT type"
return True
def compare(self, value1: float, value2: float) -> tuple[bool]:
return (value1 <= value2,)
class StringComparison:
"""
Compares two strings using a selected comparison operator.
This node takes two string inputs and a comparison operator, and returns
a boolean result based on the selected comparison.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"string1": ("STRING", {"default": ""}),
"string2": ("STRING", {"default": ""}),
"operator": (["==", "!=", ">", "<", ">=", "<="], {"default": "=="}),
"case_sensitive": (["True", "False"], {"default": "True"}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["string1"] != "STRING":
return "string1 must be a STRING type"
if input_types["string2"] != "STRING":
return "string2 must be a STRING type"
return True
def compare(self, string1: str, string2: str, operator: str, case_sensitive: str) -> tuple[bool]:
if case_sensitive == "False":
string1 = string1.lower()
string2 = string2.lower()
if operator == "==":
return (string1 == string2,)
elif operator == "!=":
return (string1 != string2,)
elif operator == ">":
return (string1 > string2,)
elif operator == "<":
return (string1 < string2,)
elif operator == ">=":
return (string1 >= string2,)
elif operator == "<=":
return (string1 <= string2,)
else:
raise ValueError(f"Unknown operator: {operator}")
class NumberInRange:
"""
Checks if a number is within a specified range.
This node takes a number and range bounds, and returns True if the number
is within the specified range, and False otherwise. The user can specify
whether the bounds are inclusive or exclusive.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (["FLOAT", "INT"], {}),
"min_value": (["FLOAT", "INT"], {"default": 0}),
"max_value": (["FLOAT", "INT"], {"default": 100}),
},
"optional": {
"include_min": (["True", "False"], {"default": "True"}),
"include_max": (["True", "False"], {"default": "True"}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("in_range",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "check_range"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["value"] not in ("FLOAT", "INT"):
return "value must be a FLOAT or INT type"
if input_types["min_value"] not in ("FLOAT", "INT"):
return "min_value must be a FLOAT or INT type"
if input_types["max_value"] not in ("FLOAT", "INT"):
return "max_value must be a FLOAT or INT type"
return True
def check_range(self, value: float, min_value: float, max_value: float,
include_min: str = "True", include_max: str = "True") -> tuple[bool]:
min_check = value >= min_value if include_min == "True" else value > min_value
max_check = value <= max_value if include_max == "True" else value < max_value
return (min_check and max_check,)
class IsNull:
"""
Checks if a value is None/null.
This node takes any input value and returns True if the value is None,
and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("*", {}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("is_null",)
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "check_null"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool:
return True
def check_null(self, value: Any) -> tuple[bool]:
return (value is None,)
class CompareLength:
"""
Compares the length of a container (string, list, etc) with a value.
This node takes a container and a comparison value, and returns a boolean
result based on the comparison of the container's length with the value.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"container": ("*", {}),
"operator": (["==", "!=", ">", "<", ">=", "<="], {"default": "=="}),
"length": ("INT", {"default": 0, "min": 0}),
}
}
RETURN_TYPES = ("BOOLEAN", "INT")
RETURN_NAMES = ("result", "actual_length")
CATEGORY = "Basic/comparison"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare_length"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["length"] != "INT":
return "length must be an INT type"
return True
def compare_length(self, container: Any, operator: str, length: int) -> tuple[bool, int]:
try:
actual_length = len(container)
except (TypeError, AttributeError):
# If the object doesn't have a length, return False and -1
return False, -1
if operator == "==":
return (actual_length == length, actual_length)
elif operator == "!=":
return (actual_length != length, actual_length)
elif operator == ">":
return (actual_length > length, actual_length)
elif operator == "<":
return (actual_length < length, actual_length)
elif operator == ">=":
return (actual_length >= length, actual_length)
elif operator == "<=":
return (actual_length <= length, actual_length)
else:
raise ValueError(f"Unknown operator: {operator}")
NODE_CLASS_MAPPINGS = {
"Equal": Equal,
"NotEqual": NotEqual,
"GreaterThan": GreaterThan,
"LessThan": LessThan,
"GreaterThanOrEqual": GreaterThanOrEqual,
"LessThanOrEqual": LessThanOrEqual,
"StringComparison": StringComparison,
"NumberInRange": NumberInRange,
"IsNull": IsNull,
"CompareLength": CompareLength,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Equal": "==",
"NotEqual": "!=",
"GreaterThan": ">",
"LessThan": "<",
"GreaterThanOrEqual": ">=",
"LessThanOrEqual": "<=",
"StringComparison": "string compare",
"NumberInRange": "in range",
"IsNull": "is null",
"CompareLength": "compare length",
}
+124
View File
@@ -0,0 +1,124 @@
from typing import Any, Dict, List, Union
from inspect import cleandoc
class IfElse:
"""
Implements a conditional branch (if/else) in the workflow.
This node takes a condition input and two value inputs. If the condition
evaluates to True, the first value is returned; otherwise, the second value
is returned. This allows conditional data flow in ComfyUI workflows.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"condition": ("BOOLEAN", {}),
"if_true": ("*", {"lazy": True}),
"if_false": ("*", {"lazy": True}),
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/ ⎇ Flow Control"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "execute"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["condition"] not in ("BOOLEAN"):
return "condition must be a BOOLEAN type"
return True
def check_lazy_status(self, condition: bool, if_true: Any, if_false: Any) -> list[str]:
needed = []
if if_true is None and condition is True:
needed.append("if_true")
if if_false is None and condition is False:
needed.append("if_false")
return needed
def execute(self, condition: bool, if_true: Any, if_false: Any) -> tuple[Any]:
"""Execute the if/else statement and return the appropriate value."""
return (if_true if condition else if_false,)
class SwitchCase:
"""
Implements a switch/case selection in the workflow.
This node takes a selector input (an integer) and multiple case value inputs.
It returns the value corresponding to the provided index. If the index is out
of range, the default value is returned. This allows for selection from multiple
options based on a computed index.
NOTE: This version of the node will most likely be deprecated in the future.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"selector": ("INT", {"default": 0, "min": 0}),
"case_0": ("*", {"lazy": True}),
"case_1": ("*", {"lazy": True}),
},
"optional": {
"case_2": ("*", {"lazy": True}),
"case_3": ("*", {"lazy": True}),
"case_4": ("*", {"lazy": True}),
"default": ("*", {"lazy": True}),
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/ ⎇ Flow Control"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "execute"
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["selector"] not in ("INT"):
return "selector must be a INT type"
return True
def check_lazy_status(self, condition: int, case_0: Any, case_1: Any,
case_2: Any, case_3: Any, case_4: Any, default: Any) -> list[str]:
needed = []
if case_0 is None and condition == 0:
needed.append("case_0")
if case_1 is None and condition == 1:
needed.append("case_1")
if case_2 is None and condition == 2:
needed.append("case_2")
if case_3 is None and condition == 3:
needed.append("case_3")
if case_4 is None and condition == 4:
needed.append("case_4")
if default is None and not 0 <= condition <= 4:
needed.append("default")
return needed
def execute(self, selector: int, case_0: Any, case_1: Any,
case_2: Any = None, case_3: Any = None,
case_4: Any = None, default: Any = None) -> tuple[Any]:
"""Execute the switch/case statement and return the appropriate value."""
cases = [case_0, case_1, case_2, case_3, case_4]
if 0 <= selector < len(cases) and cases[selector] is not None:
return (cases[selector],)
# If selector is out of range or the selected case is None, return default
return (default,)
NODE_CLASS_MAPPINGS = {
"IfElse": IfElse,
"SwitchCase": SwitchCase,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"IfElse": "if/else",
"SwitchCase": "switch/case",
}
+47 -1
View File
@@ -173,7 +173,7 @@ class DataListPop:
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types["index"] not in ("INT"):
if input_types[0].get("index", "INT") not in ("INT"):
return "index must be an INT type"
return True
@@ -648,6 +648,50 @@ class DataListZip:
return (result,)
class DataListFilter:
"""
Filters a data list using boolean values.
This node takes a value data list and a filter data list (containing only boolean values).
It returns a new data list containing only the elements from the value list where the
corresponding element in the filter list is False.
If the lists have different lengths, the last element of the shorter list is repeated
till the lengths are matching.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("*", {}),
"filter": ("BOOLEAN", {}),
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("filtered_list",)
CATEGORY = "Basic/data list"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "filter_data"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
@classmethod
def VALIDATE_INPUTS(cls, input_types: dict[str, str]) -> bool|str:
if input_types[0]["filter"] not in ("BOOLEAN"):
return "filter must be a BOOLEAN type"
return True
def filter_data(self, **kwargs: dict[str, list]) -> tuple[list[Any]]:
values = kwargs.get('value', [])
filters = kwargs.get('filter', [])
# Create a new list with only items where the filter is False
result = [_val for _val, _filter in zip(values, filters) if not _filter]
return (result,)
class DataListToList:
"""
Converts a ComfyUI data list into a LIST object.
@@ -696,6 +740,7 @@ NODE_CLASS_MAPPINGS = {
"DataListContains": DataListContains,
"DataListCreateEmpty": DataListCreateEmpty,
"DataListZip": DataListZip,
"DataListFilter": DataListFilter,
"DataListToList": DataListToList,
}
@@ -718,5 +763,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"DataListContains": "contains",
"DataListCreateEmpty": "create_empty",
"DataListZip": "zip",
"DataListFilter": "filter",
"DataListToList": "convert to LIST",
}