Merge pull request #8 from StableLlama/dynamic_nodes

Dynamic nodes
This commit is contained in:
StableLlama
2025-05-18 01:53:21 +02:00
committed by GitHub
25 changed files with 1966 additions and 272 deletions
+3 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "basic_data_handling"
version = "0.2.2"
version = "0.3.0"
description = """NOTE: Still in development! Expect breaking changes!
Basic Python functions for manipulating data that every programmer is used to.
Currently supported ComfyUI data types: BOOLEAN, FLOAT, INT, STRING and data lists.
@@ -47,11 +47,12 @@ minversion = "8.0"
pythonpath = [
"src",
"../..", # Path to parent directory containing comfy module
"."
]
testpaths = [
"tests",
]
# python_files = ["test_*.py"]
#python_files = ["test_*.py"]
python_files = ["test_boolean_nodes.py"]
[tool.mypy]
+12 -12
View File
@@ -145,19 +145,19 @@ class BooleanXor(ComfyNodeABC):
NODE_CLASS_MAPPINGS = {
"Basic data handling: BooleanAnd": BooleanAnd,
"Basic data handling: BooleanNand": BooleanNand,
"Basic data handling: BooleanNor": BooleanNor,
"Basic data handling: BooleanNot": BooleanNot,
"Basic data handling: BooleanOr": BooleanOr,
"Basic data handling: BooleanXor": BooleanXor,
"Basic data handling: Boolean And": BooleanAnd,
"Basic data handling: Boolean Nand": BooleanNand,
"Basic data handling: Boolean Nor": BooleanNor,
"Basic data handling: Boolean Not": BooleanNot,
"Basic data handling: Boolean Or": BooleanOr,
"Basic data handling: Boolean Xor": BooleanXor,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: BooleanAnd": "and",
"Basic data handling: BooleanNand": "nand",
"Basic data handling: BooleanNor": "nor",
"Basic data handling: BooleanNot": "not",
"Basic data handling: BooleanOr": "or",
"Basic data handling: BooleanXor": "xor",
"Basic data handling: Boolean And": "and",
"Basic data handling: Boolean Nand": "nand",
"Basic data handling: Boolean Nor": "nor",
"Basic data handling: Boolean Not": "not",
"Basic data handling: Boolean Or": "or",
"Basic data handling: Boolean Xor": "xor",
}
+8 -8
View File
@@ -58,7 +58,7 @@ class NotEqual(ComfyNodeABC):
return (value1 != value2,)
class LessThan:
class LessThan(ComfyNodeABC):
"""
Checks if the first value is less than the second.
@@ -84,7 +84,7 @@ class LessThan:
return (value1 < value2,)
class LessThanOrEqual:
class LessThanOrEqual(ComfyNodeABC):
"""
Checks if the first value is less than or equal to the second.
@@ -110,7 +110,7 @@ class LessThanOrEqual:
return (value1 <= value2,)
class GreaterThan:
class GreaterThan(ComfyNodeABC):
"""
Checks if the first value is greater than the second.
@@ -136,7 +136,7 @@ class GreaterThan:
return (value1 > value2,)
class GreaterThanOrEqual:
class GreaterThanOrEqual(ComfyNodeABC):
"""
Checks if the first value is greater than or equal to the second.
@@ -162,7 +162,7 @@ class GreaterThanOrEqual:
return (value1 >= value2,)
class IsNull:
class IsNull(ComfyNodeABC):
"""
Checks if a value is None/null.
@@ -187,7 +187,7 @@ class IsNull:
return (value is None,)
class NumberInRange:
class NumberInRange(ComfyNodeABC):
"""
Checks if a number is within a specified range.
@@ -223,7 +223,7 @@ class NumberInRange:
return (min_check and max_check,)
class CompareLength:
class CompareLength(ComfyNodeABC):
"""
Compares the length of a container (string, list, etc) with a value.
@@ -269,7 +269,7 @@ class CompareLength:
raise ValueError(f"Unknown operator: {operator}")
class StringComparison:
class StringComparison(ComfyNodeABC):
"""
Compares two strings using a selected comparison operator.
+117 -26
View File
@@ -22,7 +22,7 @@ class IfElse(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/Flow Control"
CATEGORY = "Basic/flow control"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "execute"
@@ -38,6 +38,90 @@ class IfElse(ComfyNodeABC):
return (if_true if condition else if_false,)
class IfElifElse(ComfyNodeABC):
"""
Implements a conditional branch (if/elif/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 elif (else if)
is evaluated and returned when true. This continues for all additional elifs.
When none is true, the value of the else is returned.
This allows conditional data flow in ComfyUI workflows.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"if": (IO.BOOLEAN, {}),
"then": (IO.ANY, {"lazy": True}),
},
"optional": {
"elif_0": (IO.BOOLEAN, {"lazy": True, "_dynamic": "number", "_dynamicGroup": 0}),
"then_0": (IO.ANY, {"lazy": True, "_dynamic": "number", "_dynamicGroup": 0}),
"else": (IO.ANY, {"lazy": True}),
}
}
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/flow control"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "execute"
def check_lazy_status(self, **kwargs) -> list[str]:
needed = []
# Check if condition
if kwargs.get("if", False) and kwargs.get("then") is None:
needed.append("then")
return needed # If main condition is true, we only need "then"
# Check each elif condition
elif_index = 0
while True:
elif_key = f"elif_{elif_index}"
then_key = f"then_{elif_index}"
if elif_key not in kwargs:
break
# If this elif condition is true and its value is None, add it to needed
if kwargs.get(elif_key, False) and kwargs.get(then_key) is None:
needed.append(then_key)
return needed # If any elif is true, we need only its then value
elif_index += 1
# If no conditions were true, check if we need the else
if "else" in kwargs and kwargs["else"] is None:
needed.append("else")
return needed
def execute(self, **kwargs) -> tuple[Any]:
# Check if the main condition is true
if kwargs.get("if", False):
return (kwargs.get("then"),)
# Check each elif condition
elif_index = 0
while True:
elif_key = f"elif_{elif_index}"
then_key = f"then_{elif_index}"
if elif_key not in kwargs:
break
if kwargs.get(elif_key, False):
return (kwargs.get(then_key),)
elif_index += 1
# If no conditions were true, return the else value
return (kwargs.get("else"),)
class SwitchCase(ComfyNodeABC):
"""
Implements a switch/case selection in the workflow.
@@ -56,59 +140,66 @@ class SwitchCase(ComfyNodeABC):
return {
"required": {
"selector": (IO.INT, {"default": 0, "min": 0}),
"case_0": (IO.ANY, {"lazy": True}),
"case_1": (IO.ANY, {"lazy": True}),
"case_0": (IO.ANY, {"lazy": True, "_dynamic": "number"}),
},
"optional": {
"case_2": (IO.ANY, {"lazy": True}),
"case_3": (IO.ANY, {"lazy": True}),
"case_4": (IO.ANY, {"lazy": True}),
"default": (IO.ANY, {"lazy": True}),
}
}
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("result",)
CATEGORY = "Basic/Flow Control"
CATEGORY = "Basic/flow control"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "execute"
def check_lazy_status(self, selector: int, case_0: Any, case_1: Any,
case_2: Any = None, case_3: Any = None,
case_4: Any = None, default: Any = None) -> list[str]:
def check_lazy_status(self, selector: int, **kwargs) -> list[str]:
needed = []
if case_0 is None and selector == 0:
needed.append("case_0")
if case_1 is None and selector == 1:
needed.append("case_1")
if case_2 is None and selector == 2:
needed.append("case_2")
if case_3 is None and selector == 3:
needed.append("case_3")
if case_4 is None and selector == 4:
needed.append("case_4")
if default is None and not 0 <= selector <= 4:
# Check for needed case inputs based on selector
case_count = 0
for key, value in kwargs.items():
if key.startswith("case_"):
try:
case_index = int(key.split("_")[1])
case_count = max(case_count, case_index + 1)
if value is None and selector == case_index:
needed.append(key)
except ValueError:
pass # Not a numeric case key
# Check if default is needed when selector is out of range
if "default" in kwargs and kwargs["default"] is None and not 0 <= selector < case_count:
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]:
cases = [case_0, case_1, case_2, case_3, case_4]
def execute(self, selector: int, **kwargs) -> tuple[Any]:
# Build cases array from all case_X inputs
cases = []
for i in range(len(kwargs)):
case_key = f"case_{i}"
if case_key in kwargs:
cases.append(kwargs[case_key])
else:
break
# Return the selected case if valid
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,)
return (kwargs.get("default"),)
NODE_CLASS_MAPPINGS = {
"Basic data handling: IfElse": IfElse,
"Basic data handling: IfElifElse": IfElifElse,
"Basic data handling: SwitchCase": SwitchCase,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: IfElse": "if/else",
"Basic data handling: IfElifElse": "if/elif/.../else",
"Basic data handling: SwitchCase": "switch/case",
}
+173 -51
View File
@@ -4,6 +4,141 @@ from comfy.comfy_types.node_typing import IO, ComfyNodeABC
INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes
class DataListCreate(ComfyNodeABC):
"""
Creates a new Data List from items.
This node creates and returns a Data List. The list of items is dynamically
extended based on the number of inputs provided.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.ANY, {"_dynamic": "number"}),
}
}
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list]:
return (list(kwargs.values()),)
class DataListCreateFromBoolean(ComfyNodeABC):
"""
Creates a new Data List from BOOLEAN items.
This node creates and returns a Data List. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.BOOLEAN, {"_dynamic": "number"}),
}
}
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list]:
return ([bool(value) for value in kwargs.values()],)
class DataListCreateFromFloat(ComfyNodeABC):
"""
Creates a new Data List from FLOAT items.
This node creates and returns a Data List. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.FLOAT, {"_dynamic": "number"}),
}
}
RETURN_TYPES = (IO.FLOAT,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list]:
return ([float(value) for value in kwargs.values()],)
class DataListCreateFromInt(ComfyNodeABC):
"""
Creates a new Data List from INT items.
This node creates and returns a Data List. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.INT, {"_dynamic": "number"}),
}
}
RETURN_TYPES = (IO.INT,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list]:
return ([int(value) for value in kwargs.values()],)
class DataListCreateFromString(ComfyNodeABC):
"""
Creates a new Data List from STRING items.
This node creates and returns a Data List. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.STRING, {"_dynamic": "number"}),
}
}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
OUTPUT_IS_LIST = (True,)
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([str(value) for value in kwargs.values()],)
class DataListAppend(ComfyNodeABC):
"""
Adds an item to the end of a list.
@@ -22,7 +157,7 @@ class DataListAppend(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "append"
INPUT_IS_LIST = True
@@ -54,7 +189,7 @@ class DataListContains(ComfyNodeABC):
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("contains",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "contains"
INPUT_IS_LIST = True
@@ -84,7 +219,7 @@ class DataListCount(ComfyNodeABC):
RETURN_TYPES = (IO.INT,)
RETURN_NAMES = ("count",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "count"
INPUT_IS_LIST = True
@@ -94,27 +229,6 @@ class DataListCount(ComfyNodeABC):
return (kwargs.get('list', []).count(value),)
class DataListCreateEmpty(ComfyNodeABC):
"""
Creates a new empty list.
This node creates and returns an empty list.
"""
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_empty"
OUTPUT_IS_LIST = (True,)
def create_empty(self) -> tuple[list]:
return ([],)
class DataListExtend(ComfyNodeABC):
"""
Extends a list by appending elements from another list.
@@ -133,7 +247,7 @@ class DataListExtend(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "extend"
INPUT_IS_LIST = True
@@ -145,10 +259,10 @@ class DataListExtend(ComfyNodeABC):
class DataListFilter(ComfyNodeABC):
"""
Filters a data list using boolean values.
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
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
@@ -165,7 +279,7 @@ class DataListFilter(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("filtered_list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "filter_data"
INPUT_IS_LIST = True
@@ -200,7 +314,7 @@ class DataListGetItem(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("item",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_item"
INPUT_IS_LIST = True
@@ -236,7 +350,7 @@ class DataListIndex(ComfyNodeABC):
RETURN_TYPES = (IO.INT,)
RETURN_NAMES = ("index",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "list_index"
INPUT_IS_LIST = True
@@ -274,7 +388,7 @@ class DataListInsert(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "insert"
INPUT_IS_LIST = True
@@ -302,7 +416,7 @@ class DataListLength(ComfyNodeABC):
RETURN_TYPES = (IO.INT,)
RETURN_NAMES = ("length",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "length"
INPUT_IS_LIST = True
@@ -329,7 +443,7 @@ class DataListMax(ComfyNodeABC):
RETURN_TYPES = (IO.NUMBER,)
RETURN_NAMES = ("max",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "find_max"
INPUT_IS_LIST = True
@@ -365,7 +479,7 @@ class DataListMin(ComfyNodeABC):
RETURN_TYPES = (IO.NUMBER,)
RETURN_NAMES = ("min",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "find_min"
INPUT_IS_LIST = True
@@ -405,7 +519,7 @@ class DataListPop(ComfyNodeABC):
RETURN_TYPES = (IO.ANY, IO.ANY)
RETURN_NAMES = ("list", "item")
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "pop"
INPUT_IS_LIST = True
@@ -439,7 +553,7 @@ class DataListRemove(ComfyNodeABC):
RETURN_TYPES = (IO.ANY, IO.BOOLEAN,)
RETURN_NAMES = ("list", "success",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "remove"
INPUT_IS_LIST = True
@@ -471,7 +585,7 @@ class DataListReverse(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "reverse"
INPUT_IS_LIST = True
@@ -502,7 +616,7 @@ class DataListSetItem(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "set_item"
INPUT_IS_LIST = True
@@ -542,7 +656,7 @@ class DataListSlice(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "slice"
INPUT_IS_LIST = True
@@ -578,7 +692,7 @@ class DataListSort(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "sort"
INPUT_IS_LIST = True
@@ -596,7 +710,7 @@ class DataListZip(ComfyNodeABC):
"""
Combines multiple lists element-wise.
This node takes multiple data lists as input and returns a new data list
This node takes multiple Data Lists as input and returns a new Data List
where each item is a list containing the corresponding elements from the input lists.
The length of the output list will be equal to the length of the shortest input list.
"""
@@ -615,7 +729,7 @@ class DataListZip(ComfyNodeABC):
RETURN_TYPES = (IO.ANY,)
RETURN_NAMES = ("list",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "zip_lists"
INPUT_IS_LIST = True
@@ -637,9 +751,9 @@ class DataListZip(ComfyNodeABC):
class DataListToList(ComfyNodeABC):
"""
Converts a ComfyUI data list into a LIST object.
Converts a ComfyUI Data List into a LIST object.
This node takes a data list input (which is typically a list of items with the same type)
This node takes a Data List input (which is typically a list of items with the same type)
and converts it to a LIST object (a Python list as a single variable).
"""
@classmethod
@@ -651,7 +765,7 @@ class DataListToList(ComfyNodeABC):
}
RETURN_TYPES = ("LIST",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "convert"
INPUT_IS_LIST = True
@@ -662,9 +776,9 @@ class DataListToList(ComfyNodeABC):
class DataListToSet(ComfyNodeABC):
"""
Converts a ComfyUI data list into a LIST object.
Converts a ComfyUI Data List into a LIST object.
This node takes a data list input (which is typically a list of items with the same type)
This node takes a Data List input (which is typically a list of items with the same type)
and converts it to a LIST object (a Python list as a single variable).
"""
@classmethod
@@ -676,7 +790,7 @@ class DataListToSet(ComfyNodeABC):
}
RETURN_TYPES = ("SET",)
CATEGORY = "Basic/data list"
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "convert"
INPUT_IS_LIST = True
@@ -686,10 +800,14 @@ class DataListToSet(ComfyNodeABC):
NODE_CLASS_MAPPINGS = {
"Basic data handling: DataListCreate": DataListCreate,
"Basic data handling: DataListCreateFromBoolean": DataListCreateFromBoolean,
"Basic data handling: DataListCreateFromFloat": DataListCreateFromFloat,
"Basic data handling: DataListCreateFromInt": DataListCreateFromInt,
"Basic data handling: DataListCreateFromString": DataListCreateFromString,
"Basic data handling: DataListAppend": DataListAppend,
"Basic data handling: DataListContains": DataListContains,
"Basic data handling: DataListCount": DataListCount,
"Basic data handling: DataListCreateEmpty": DataListCreateEmpty,
"Basic data handling: DataListExtend": DataListExtend,
"Basic data handling: DataListFilter": DataListFilter,
"Basic data handling: DataListGetItem": DataListGetItem,
@@ -710,10 +828,14 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: DataListCreate": "create Data List",
"Basic data handling: DataListCreateFromBoolean": "create Data List from BOOLEANs",
"Basic data handling: DataListCreateFromFloat": "create Data List from FLOATs",
"Basic data handling: DataListCreateFromInt": "create Data List from INTs",
"Basic data handling: DataListCreateFromString": "create Data List from STRINGs",
"Basic data handling: DataListAppend": "append",
"Basic data handling: DataListContains": "contains",
"Basic data handling: DataListCount": "count",
"Basic data handling: DataListCreateEmpty": "create empty",
"Basic data handling: DataListExtend": "extend",
"Basic data handling: DataListFilter": "filter",
"Basic data handling: DataListGetItem": "get item",
+155 -5
View File
@@ -2,23 +2,165 @@ from typing import Any
from inspect import cleandoc
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
class DictCreate(ComfyNodeABC):
"""
Creates a new empty dictionary.
This node creates and returns a new empty dictionary object.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
return {
"optional": {
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.ANY, {"_dynamic": "number", "_dynamicGroup": 0}),
}
}
RETURN_TYPES = ("DICT",)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
def create(self) -> tuple[dict]:
return ({},)
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
result[kwargs[key_name]] = kwargs[value_name]
return (result,)
class DictCreateFromBoolean(ComfyNodeABC):
"""
Creates a new empty dictionary.
This node creates and returns a new empty dictionary object.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0}),
}
}
RETURN_TYPES = ("DICT",)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
result[kwargs[key_name]] = bool(kwargs[value_name])
return (result,)
class DictCreateFromFloat(ComfyNodeABC):
"""
Creates a new empty dictionary.
This node creates and returns a new empty dictionary object.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.FLOAT, {"_dynamic": "number", "_dynamicGroup": 0}),
}
}
RETURN_TYPES = ("DICT",)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
result[kwargs[key_name]] = float(kwargs[value_name])
return (result,)
class DictCreateFromInt(ComfyNodeABC):
"""
Creates a new empty dictionary.
This node creates and returns a new empty dictionary object.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.INT, {"_dynamic": "number", "_dynamicGroup": 0}),
}
}
RETURN_TYPES = ("DICT",)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
result[kwargs[key_name]] = int(kwargs[value_name])
return (result,)
class DictCreateFromString(ComfyNodeABC):
"""
Creates a new empty dictionary.
This node creates and returns a new empty dictionary object.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
"value_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0}),
}
}
RETURN_TYPES = ("DICT",)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
def create(self, **kwargs: list[Any]) -> tuple[dict]:
result = {}
# Process all key_X/value_X pairs from dynamic inputs
for i in range(len(kwargs) // 2): # Divide by 2 since we have key/value pairs
key_name = f"key_{i}"
value_name = f"value_{i}"
if key_name in kwargs and value_name in kwargs:
result[kwargs[key_name]] = str(kwargs[value_name])
return (result,)
class DictCreateFromItemsDataList(ComfyNodeABC):
@@ -706,6 +848,10 @@ class DictValues(ComfyNodeABC):
NODE_CLASS_MAPPINGS = {
"Basic data handling: DictCreate": DictCreate,
"Basic data handling: DictCreateFromBoolean": DictCreateFromBoolean,
"Basic data handling: DictCreateFromFloat": DictCreateFromFloat,
"Basic data handling: DictCreateFromInt": DictCreateFromInt,
"Basic data handling: DictCreateFromString": DictCreateFromString,
"Basic data handling: DictCreateFromItemsDataList": DictCreateFromItemsDataList,
"Basic data handling: DictCreateFromItemsList": DictCreateFromItemsList,
"Basic data handling: DictCreateFromLists": DictCreateFromLists,
@@ -732,7 +878,11 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: DictCreate": "create empty",
"Basic data handling: DictCreate": "create DICT",
"Basic data handling: DictCreateFromBoolean": "create DICT from BOOLEANs",
"Basic data handling: DictCreateFromFloat": "create DICT from FLOATs",
"Basic data handling: DictCreateFromInt": "create DICT from INTs",
"Basic data handling: DictCreateFromString": "create DICT from STRINGs",
"Basic data handling: DictCreateFromItemsDataList": "create from items (data list)",
"Basic data handling: DictCreateFromItemsList": "create from items (LIST)",
"Basic data handling: DictCreateFromLists": "create from LISTs",
@@ -740,7 +890,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: DictContainsKey": "contains key",
"Basic data handling: DictExcludeKeys": "exclude keys",
"Basic data handling: DictFilterByKeys": "filter by keys",
"Basic data handling: DictFromKeys": "fromkeys",
"Basic data handling: DictFromKeys": "from keys",
"Basic data handling: DictGet": "get",
"Basic data handling: DictGetKeysValues": "get keys values",
"Basic data handling: DictGetMultiple": "get multiple",
+30 -1
View File
@@ -1,6 +1,31 @@
from inspect import cleandoc
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
class FloatCreate(ComfyNodeABC):
"""
Create a FLOAT from a STRING widget.
The input string must be a valid floating-point number and will be
directly converted to a FLOAT without any further processing.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.ANY, {"default": "0.0", "widgetType": "STRING"}),
}
}
RETURN_TYPES = (IO.FLOAT,)
CATEGORY = "Basic/FLOAT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
def create(self, value: str) -> tuple[float]:
return (float(value),)
class FloatAdd(ComfyNodeABC):
"""
Adds two floating-point numbers.
@@ -124,6 +149,8 @@ class FloatDivideSafe(ComfyNodeABC):
def divide(self, float1: float, float2: float) -> tuple[float]:
if float2 == 0.0:
if float1 == 0.0:
return (float('nan'),)
return (float('inf') if float1 > 0 else float('-inf'),)
return (float1 / float2,)
@@ -276,6 +303,7 @@ class FloatRound(ComfyNodeABC):
NODE_CLASS_MAPPINGS = {
"Basic data handling: FloatCreate": FloatCreate,
"Basic data handling: FloatAdd": FloatAdd,
"Basic data handling: FloatSubtract": FloatSubtract,
"Basic data handling: FloatMultiply": FloatMultiply,
@@ -290,11 +318,12 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: FloatCreate": "create FLOAT",
"Basic data handling: FloatAdd": "add",
"Basic data handling: FloatSubtract": "subtract",
"Basic data handling: FloatMultiply": "multiply",
"Basic data handling: FloatDivide": "divide",
"Basic data handling: FloatDivideSafe": "divide (division by zero safe)",
"Basic data handling: FloatDivideSafe": "divide (zero safe)",
"Basic data handling: FloatAsIntegerRatio": "integer ratio",
"Basic data handling: FloatFromHex": "from hex",
"Basic data handling: FloatHex": "to hex",
+60 -1
View File
@@ -2,6 +2,61 @@ from inspect import cleandoc
from typing import Literal
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
class IntCreate(ComfyNodeABC):
"""
Create an INT from a STRING widget.
The input string must be a valid integer number and will be
directly converted to an INT without any further processing.
Strings starting with "0b" are interpreted as binary numbers,
"0o" as octal numbers, and "0x" as hexadecimal numbers.
Note: This doesn't handle ones' complement as the data size is unknown.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.ANY, {"default": "0", "widgetType": "STRING"}),
}
}
RETURN_TYPES = (IO.INT,)
CATEGORY = "Basic/INT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
def create(self, value: str) -> tuple[int]:
return (int(value, 0),) # Automatically detects base using prefixes
class IntCreateWithBase(ComfyNodeABC):
"""
Create an INT from a STRING with a given base.
The input string must be a valid integer number and will be
directly converted to an INT without any further processing.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.STRING, {"default": "0"}),
"base": (IO.INT, {"default": "10", "min": 2}),
}
}
RETURN_TYPES = (IO.INT,)
CATEGORY = "Basic/INT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
def create(self, value: str, base: int) -> tuple[int]:
return (int(value, base),)
class IntAdd(ComfyNodeABC):
"""
Adds two integers.
@@ -286,6 +341,8 @@ class IntToBytes(ComfyNodeABC):
NODE_CLASS_MAPPINGS = {
"Basic data handling: IntCreate": IntCreate,
"Basic data handling: IntCreateWithBase": IntCreateWithBase,
"Basic data handling: IntAdd": IntAdd,
"Basic data handling: IntSubtract": IntSubtract,
"Basic data handling: IntMultiply": IntMultiply,
@@ -300,11 +357,13 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: IntCreate": "create INT",
"Basic data handling: IntCreateWithBase": "create INT with base",
"Basic data handling: IntAdd": "add",
"Basic data handling: IntSubtract": "subtract",
"Basic data handling: IntMultiply": "multiply",
"Basic data handling: IntDivide": "divide",
"Basic data handling: IntDivideSafe": "divide (division by zero safe)",
"Basic data handling: IntDivideSafe": "divide (zero safe)",
"Basic data handling: IntBitCount": "bit count",
"Basic data handling: IntBitLength": "bit length",
"Basic data handling: IntFromBytes": "from bytes",
+123 -9
View File
@@ -4,23 +4,129 @@ from comfy.comfy_types.node_typing import IO, ComfyNodeABC
INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes
class ListEmpty(ComfyNodeABC):
class ListCreate(ComfyNodeABC):
"""
Create an empty LIST.
Creates a new LIST from items.
This node creates and returns a LIST. The list of items is dynamically
extended based on the number of inputs provided.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {}
"optional": {
"item_0": (IO.ANY, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("LIST",)
CATEGORY = "Basic/LIST"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create"
FUNCTION = "create_list"
def create(self) -> tuple[list[Any]]:
return ([],)
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return (list(kwargs.values()),)
class ListCreateFromBoolean(ComfyNodeABC):
"""
Creates a new LIST from BOOLEAN items.
This node creates and returns a LIST. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.BOOLEAN, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("LIST",)
CATEGORY = "Basic/LIST"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([bool(value) for value in kwargs.values()],)
class ListCreateFromFloat(ComfyNodeABC):
"""
Creates a new LIST from FLOAT items.
This node creates and returns a LIST. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.FLOAT, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("LIST",)
CATEGORY = "Basic/LIST"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([float(value) for value in kwargs.values()],)
class ListCreateFromInt(ComfyNodeABC):
"""
Creates a new LIST from INT items.
This node creates and returns a LIST. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.INT, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("LIST",)
CATEGORY = "Basic/LIST"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([int(value) for value in kwargs.values()],)
class ListCreateFromString(ComfyNodeABC):
"""
Creates a new LIST from STRING items.
This node creates and returns a LIST. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.STRING, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("LIST",)
CATEGORY = "Basic/LIST"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ([str(value) for value in kwargs.values()],)
class ListAppend(ComfyNodeABC):
@@ -552,7 +658,11 @@ class ListToSet(ComfyNodeABC):
NODE_CLASS_MAPPINGS = {
"Basic data handling: ListEmpty": ListEmpty,
"Basic data handling: ListCreate": ListCreate,
"Basic data handling: ListCreateFromBoolean": ListCreateFromBoolean,
"Basic data handling: ListCreateFromFloat": ListCreateFromFloat,
"Basic data handling: ListCreateFromInt": ListCreateFromInt,
"Basic data handling: ListCreateFromString": ListCreateFromString,
"Basic data handling: ListAppend": ListAppend,
"Basic data handling: ListContains": ListContains,
"Basic data handling: ListCount": ListCount,
@@ -574,7 +684,11 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: ListEmpty": "create empty",
"Basic data handling: ListCreate": "create LIST",
"Basic data handling: ListCreateFromBoolean": "create LIST from BOOLEANs",
"Basic data handling: ListCreateFromFloat": "create LIST from FLOATs",
"Basic data handling: ListCreateFromInt": "create LIST from INTs",
"Basic data handling: ListCreateFromString": "create LIST from STRINGs",
"Basic data handling: ListAppend": "append",
"Basic data handling: ListContains": "contains",
"Basic data handling: ListCount": "count",
@@ -591,6 +705,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: ListSetItem": "set item",
"Basic data handling: ListSlice": "slice",
"Basic data handling: ListSort": "sort",
"Basic data handling: ListToDataList": "convert to data list",
"Basic data handling: ListToDataList": "convert to Data List",
"Basic data handling: ListToSet": "convert to SET",
}
+2 -7
View File
@@ -21,16 +21,11 @@ class MathFormula(ComfyNodeABC):
return {
"required": {
"formula": (IO.STRING, {"default": "a + b"}),
"a": (IO.NUMBER, {"default": 0.0}),
},
"optional": {
"b": (IO.NUMBER, {"default": 0.0}),
"c": (IO.NUMBER, {"default": 0.0}),
"d": (IO.NUMBER, {"default": 0.0}),
"a": (IO.NUMBER, {"default": 0.0, "_dynamic": "letter"}),
},
}
RETURN_TYPES = (IO.NUMBER,)
RETURN_TYPES = (IO.FLOAT,)
CATEGORY = "Basic/maths"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "evaluate"
+62 -65
View File
@@ -13,7 +13,7 @@ class MathAbs(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
}
}
@@ -22,8 +22,8 @@ class MathAbs(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: Union[float, int]) -> tuple[Union[float, int]]:
return (abs(value),)
def calculate(self, value: Union[float, int, str]) -> tuple[Union[float, int]]:
return (abs(float(value)),)
class MathAcos(ComfyNodeABC):
@@ -37,7 +37,7 @@ class MathAcos(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"unit": (["radians", "degrees"], {"default": "degrees"}),
}
}
@@ -47,8 +47,8 @@ class MathAcos(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]:
result = math.acos(value)
def calculate(self, value: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]:
result = math.acos(float(value))
if unit == "degrees":
result = math.degrees(result)
return (result,)
@@ -65,7 +65,7 @@ class MathAsin(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"unit": (["radians", "degrees"], {"default": "degrees"}),
}
}
@@ -75,8 +75,8 @@ class MathAsin(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]:
result = math.asin(value)
def calculate(self, value: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]:
result = math.asin(float(value))
if unit == "degrees":
result = math.degrees(result)
return (result,)
@@ -93,7 +93,7 @@ class MathAtan(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"unit": (["radians", "degrees"], {"default": "degrees"}),
}
}
@@ -103,8 +103,8 @@ class MathAtan(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float, unit: Literal["radians", "degrees"]) -> tuple[float]:
result = math.atan(value)
def calculate(self, value: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]:
result = math.atan(float(value))
if unit == "degrees":
result = math.degrees(result)
return (result,)
@@ -122,8 +122,8 @@ class MathAtan2(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"y": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"x": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"y": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"x": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"unit": (["radians", "degrees"], {"default": "degrees"}),
}
}
@@ -133,8 +133,8 @@ class MathAtan2(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, y: float, x: float, unit: Literal["radians", "degrees"]) -> tuple[float]:
result = math.atan2(y, x)
def calculate(self, y: Union[float, int, str], x: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]:
result = math.atan2(float(y), float(x))
if unit == "degrees":
result = math.degrees(result)
return (result,)
@@ -150,7 +150,7 @@ class MathCeil(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
}
}
@@ -159,8 +159,8 @@ class MathCeil(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float) -> tuple[int]:
return (math.ceil(value),)
def calculate(self, value: Union[float, int, str]) -> tuple[int]:
return (math.ceil(float(value)),)
class MathCos(ComfyNodeABC):
@@ -175,7 +175,7 @@ class MathCos(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"unit": (["radians", "degrees"], {"default": "degrees"}),
}
}
@@ -185,11 +185,11 @@ class MathCos(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]:
def calculate(self, angle: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]:
if unit == "degrees":
# Convert degrees to radians
angle = math.radians(angle)
return (math.cos(angle),)
angle = math.radians(float(angle))
return (math.cos(float(angle)),)
class MathDegrees(ComfyNodeABC):
@@ -202,7 +202,7 @@ class MathDegrees(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"radians": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"radians": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
}
}
@@ -211,8 +211,8 @@ class MathDegrees(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, radians: float) -> tuple[float]:
return (math.degrees(radians),)
def calculate(self, radians: Union[float, int, str]) -> tuple[float]:
return (math.degrees(float(radians)),)
class MathE(ComfyNodeABC):
@@ -244,7 +244,7 @@ class MathExp(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
}
}
@@ -253,8 +253,8 @@ class MathExp(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float) -> tuple[float]:
return (math.exp(value),)
def calculate(self, value: Union[float, int, str]) -> tuple[float]:
return (math.exp(float(value)),)
class MathFloor(ComfyNodeABC):
@@ -267,7 +267,7 @@ class MathFloor(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
}
}
@@ -276,8 +276,8 @@ class MathFloor(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float) -> tuple[int]:
return (math.floor(value),)
def calculate(self, value: Union[float, int, str]) -> tuple[int]:
return (math.floor(float(value)),)
class MathLog(ComfyNodeABC):
@@ -291,10 +291,10 @@ class MathLog(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING"}),
},
"optional": {
"base": (IO.NUMBER, {"default": math.e, "min": 0.0000001, "widgetType": "FLOAT"}),
"base": (IO.NUMBER, {"default": math.e, "min": 0.0000001, "widgetType": "STRING"}),
}
}
@@ -303,11 +303,8 @@ class MathLog(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float, base: float = math.e) -> tuple[float]:
if base == math.e:
return (math.log(value),)
else:
return (math.log(value, base),)
def calculate(self, value: Union[float, int, str], base: Union[float, int, str] = math.e) -> tuple[float]:
return (math.log(float(value), float(base)),)
class MathLog10(ComfyNodeABC):
@@ -320,7 +317,7 @@ class MathLog10(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING"}),
}
}
@@ -329,8 +326,8 @@ class MathLog10(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float) -> tuple[float]:
return (math.log10(value),)
def calculate(self, value: Union[float, int, str]) -> tuple[float]:
return (math.log10(float(value)),)
class MathMax(ComfyNodeABC):
@@ -344,8 +341,8 @@ class MathMax(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value1": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value2": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value1": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"value2": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
}
}
@@ -354,8 +351,8 @@ class MathMax(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value1: Union[float, int], value2: Union[float, int]) -> tuple[Union[float, int]]:
return (max(value1, value2),)
def calculate(self, value1: Union[float, int, str], value2: Union[float, int, str]) -> tuple[Union[float, int]]:
return (max(float(value1), float(value2)),)
class MathMin(ComfyNodeABC):
@@ -369,8 +366,8 @@ class MathMin(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value1": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value2": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"value1": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"value2": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
}
}
@@ -379,8 +376,8 @@ class MathMin(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value1: Union[float, int], value2: Union[float, int]) -> tuple[Union[float, int]]:
return (min(value1, value2),)
def calculate(self, value1: Union[float, int, str], value2: Union[float, int, str]) -> tuple[Union[float, int]]:
return (min(float(value1), float(value2)),)
class MathPi(ComfyNodeABC):
@@ -412,7 +409,7 @@ class MathRadians(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"degrees": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"degrees": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
}
}
@@ -421,8 +418,8 @@ class MathRadians(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, degrees: float) -> tuple[float]:
return (math.radians(degrees),)
def calculate(self, degrees: Union[float, int, str]) -> tuple[float]:
return (math.radians(float(degrees)),)
class MathSin(ComfyNodeABC):
@@ -437,7 +434,7 @@ class MathSin(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"unit": (["radians", "degrees"], {"default": "degrees"}),
}
}
@@ -447,11 +444,11 @@ class MathSin(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]:
def calculate(self, angle: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]:
if unit == "degrees":
# Convert degrees to radians
angle = math.radians(angle)
return (math.sin(angle),)
angle = math.radians(float(angle))
return (math.sin(float(angle)),)
class MathSqrt(ComfyNodeABC):
@@ -464,7 +461,7 @@ class MathSqrt(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "FLOAT"}),
"value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "STRING"}),
}
}
@@ -473,8 +470,8 @@ class MathSqrt(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, value: float) -> tuple[float]:
return (math.sqrt(value),)
def calculate(self, value: Union[float, int, str]) -> tuple[float]:
return (math.sqrt(float(value)),)
class MathTan(ComfyNodeABC):
@@ -489,7 +486,7 @@ class MathTan(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"angle": (IO.NUMBER, {"default": 0.0, "widgetType": "FLOAT"}),
"angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}),
"unit": (["radians", "degrees"], {"default": "degrees"}),
}
}
@@ -499,16 +496,16 @@ class MathTan(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "calculate"
def calculate(self, angle: float, unit: Literal["radians", "degrees"]) -> tuple[float]:
def calculate(self, angle: Union[float, int, str], unit: Literal["radians", "degrees"]) -> tuple[float]:
if unit == "degrees":
# Convert degrees to radians
angle = math.radians(angle)
angle = math.radians(float(angle))
# Handle specific angles that would result in division by zero
if abs(math.cos(angle)) < 1e-10:
if abs(math.cos(float(angle))) < 1e-10:
raise ValueError("Tangent is undefined at this angle (division by zero)")
return (math.tan(angle),)
return (math.tan(float(angle)),)
NODE_CLASS_MAPPINGS = {
+31 -31
View File
@@ -20,8 +20,8 @@ class PathAbspath(ComfyNodeABC):
}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("absolute_path",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("absolute path",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_abspath"
@@ -46,7 +46,7 @@ class PathBasename(ComfyNodeABC):
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("basename",)
CATEGORY = "Basic/path"
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_basename"
@@ -72,8 +72,8 @@ class PathCommonPrefix(ComfyNodeABC):
}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("common_prefix",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("common prefix",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_common_prefix"
@@ -99,7 +99,7 @@ class PathDirname(ComfyNodeABC):
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("dirname",)
CATEGORY = "Basic/path"
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_dirname"
@@ -124,7 +124,7 @@ class PathExists(ComfyNodeABC):
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("exists",)
CATEGORY = "Basic/path"
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "check_exists"
@@ -148,8 +148,8 @@ class PathExpandVars(ComfyNodeABC):
}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("expanded_path",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("expanded path",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "expand_vars"
@@ -168,8 +168,8 @@ class PathGetCwd(ComfyNodeABC):
return {"required": {}}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("current_directory",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("current directory",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_cwd"
@@ -194,7 +194,7 @@ class PathGetExtension(ComfyNodeABC):
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("extension",)
CATEGORY = "Basic/path"
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_extension"
@@ -218,8 +218,8 @@ class PathGetSize(ComfyNodeABC):
}
RETURN_TYPES = (IO.INT,)
RETURN_NAMES = ("size_bytes",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("size (bytes)",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_size"
@@ -254,8 +254,8 @@ class PathGlob(ComfyNodeABC):
}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("matching_paths",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("matching paths",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "glob_paths"
OUTPUT_IS_LIST = (True,)
@@ -280,8 +280,8 @@ class PathIsAbsolute(ComfyNodeABC):
}
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("is_absolute",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("is absolute",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "check_is_absolute"
@@ -305,8 +305,8 @@ class PathIsDir(ComfyNodeABC):
}
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("is_dir",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("is dir",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "check_is_dir"
@@ -330,8 +330,8 @@ class PathIsFile(ComfyNodeABC):
}
RETURN_TYPES = (IO.BOOLEAN,)
RETURN_NAMES = ("is_file",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("is file",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "check_is_file"
@@ -360,7 +360,7 @@ class PathJoin(ComfyNodeABC):
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("path",)
CATEGORY = "Basic/path"
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "join_paths"
@@ -392,7 +392,7 @@ class PathListDir(ComfyNodeABC):
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("entries",)
CATEGORY = "Basic/path"
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "list_directory"
OUTPUT_IS_LIST = (True,)
@@ -433,8 +433,8 @@ class PathNormalize(ComfyNodeABC):
}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("normalized_path",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("normalized path",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "normalize_path"
@@ -461,8 +461,8 @@ class PathRelative(ComfyNodeABC):
}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("relative_path",)
CATEGORY = "Basic/path"
RETURN_NAMES = ("relative path",)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_relative_path"
@@ -489,7 +489,7 @@ class PathSplit(ComfyNodeABC):
RETURN_TYPES = (IO.STRING, IO.STRING)
RETURN_NAMES = ("directory", "filename")
CATEGORY = "Basic/path"
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "split_path"
@@ -513,8 +513,8 @@ class PathSplitExt(ComfyNodeABC):
}
RETURN_TYPES = (IO.STRING, IO.STRING)
RETURN_NAMES = ("path_without_ext", "extension")
CATEGORY = "Basic/path"
RETURN_NAMES = ("path without ext", "extension")
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "split_ext"
+136 -1
View File
@@ -2,6 +2,131 @@ from typing import Any
from inspect import cleandoc
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
class SetCreate(ComfyNodeABC):
"""
Creates a new SET from items.
This node creates and returns a SET. The list of items is dynamically
extended based on the number of inputs provided.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.ANY, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("SET",)
CATEGORY = "Basic/SET"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set(kwargs.values()),)
class SetCreateFromBoolean(ComfyNodeABC):
"""
Creates a new SET from items.
This node creates and returns a SET. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.BOOLEAN, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("SET",)
CATEGORY = "Basic/SET"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set([bool(value) for value in kwargs.values()]),)
class SetCreateFromFloat(ComfyNodeABC):
"""
Creates a new SET from items.
This node creates and returns a SET. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.FLOAT, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("SET",)
CATEGORY = "Basic/SET"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set([float(value) for value in kwargs.values()]),)
class SetCreateFromInt(ComfyNodeABC):
"""
Creates a new SET from items.
This node creates and returns a SET. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.INT, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("SET",)
CATEGORY = "Basic/SET"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set([int(value) for value in kwargs.values()]),)
class SetCreateFromString(ComfyNodeABC):
"""
Creates a new SET from items.
This node creates and returns a SET. The list of items is dynamically
extended based on the number of inputs provided.
"""
EXPERIMENTAL = True
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"item_0": (IO.STRING, {"_dynamic": "number"}),
}
}
RETURN_TYPES = ("SET",)
CATEGORY = "Basic/SET"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set([str(value) for value in kwargs.values()]),)
class SetAdd(ComfyNodeABC):
"""
Adds an item to a SET.
@@ -428,6 +553,11 @@ class SetToList(ComfyNodeABC):
NODE_CLASS_MAPPINGS = {
"Basic data handling: SetCreate": SetCreate,
"Basic data handling: SetCreateFromBoolean": SetCreateFromBoolean,
"Basic data handling: SetCreateFromFloat": SetCreateFromFloat,
"Basic data handling: SetCreateFromInt": SetCreateFromInt,
"Basic data handling: SetCreateFromString": SetCreateFromString,
"Basic data handling: SetAdd": SetAdd,
"Basic data handling: SetContains": SetContains,
"Basic data handling: SetDifference": SetDifference,
@@ -446,6 +576,11 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: SetCreate": "create SET",
"Basic data handling: SetCreateFromBoolean": "create SET from BOOLEANs",
"Basic data handling: SetCreateFromFloat": "create SET from FLOATs",
"Basic data handling: SetCreateFromInt": "create SET from INTs",
"Basic data handling: SetCreateFromString": "create SET from STRINGs",
"Basic data handling: SetAdd": "add",
"Basic data handling: SetContains": "contains",
"Basic data handling: SetDifference": "difference",
@@ -459,6 +594,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: SetRemove": "remove",
"Basic data handling: SetSymmetricDifference": "symmetric difference",
"Basic data handling: SetUnion": "union",
"Basic data handling: SetToDataList": "convert to data list",
"Basic data handling: SetToDataList": "convert to Data List",
"Basic data handling: SetToList": "convert to LIST",
}
+76 -4
View File
@@ -1,4 +1,4 @@
#import pytest
import pytest
from src.basic_data_handling.comparison_nodes import (
Equal,
NotEqual,
@@ -20,6 +20,9 @@ def test_equal():
assert node.compare("hello", "hello") == (True,)
assert node.compare([1, 2], [1, 2]) == (True,)
assert node.compare(None, None) == (True,)
# Test complex objects
assert node.compare({"a": 1}, {"a": 1}) == (True,)
assert node.compare({"a": 1}, {"a": 2}) == (False,)
def test_not_equal():
@@ -29,6 +32,9 @@ def test_not_equal():
assert node.compare("hello", "world") == (True,)
assert node.compare([1, 2], [1, 3]) == (True,)
assert node.compare(None, []) == (True,)
# Test complex objects
assert node.compare({"a": 1}, {"b": 1}) == (True,)
assert node.compare({"a": 1}, {"a": 1}) == (False,)
def test_greater_than():
@@ -37,6 +43,12 @@ def test_greater_than():
assert node.compare(5, 10) == (False,)
assert node.compare(10, 10) == (False,)
assert node.compare(5.6, 2.3) == (True,)
# Test negative numbers
assert node.compare(-5, -10) == (True,)
assert node.compare(-10, -5) == (False,)
# Test zeroes
assert node.compare(0, -1) == (True,)
assert node.compare(0, 0) == (False,)
def test_less_than():
@@ -45,6 +57,12 @@ def test_less_than():
assert node.compare(10, 5) == (False,)
assert node.compare(10, 10) == (False,)
assert node.compare(2.3, 5.6) == (True,)
# Test negative numbers
assert node.compare(-10, -5) == (True,)
assert node.compare(-5, -10) == (False,)
# Test zeroes
assert node.compare(-1, 0) == (True,)
assert node.compare(0, 0) == (False,)
def test_greater_than_or_equal():
@@ -53,6 +71,12 @@ def test_greater_than_or_equal():
assert node.compare(15, 10) == (True,)
assert node.compare(5, 10) == (False,)
assert node.compare(5.5, 5.5) == (True,)
# Test negative numbers
assert node.compare(-5, -10) == (True,)
assert node.compare(-5, -5) == (True,)
# Test zeroes
assert node.compare(0, 0) == (True,)
assert node.compare(0.0, -0.0) == (True,)
def test_less_than_or_equal():
@@ -61,6 +85,12 @@ def test_less_than_or_equal():
assert node.compare(5, 15) == (True,)
assert node.compare(15, 5) == (False,)
assert node.compare(3.1, 3.1) == (True,)
# Test negative numbers
assert node.compare(-10, -5) == (True,)
assert node.compare(-5, -5) == (True,)
# Test zeroes
assert node.compare(0, 0) == (True,)
assert node.compare(-0.0, 0.0) == (True,)
def test_string_comparison():
@@ -71,11 +101,20 @@ def test_string_comparison():
assert node.compare("apple", "banana", "<", "True") == (True,)
assert node.compare("apple", "banana", ">", "True") == (False,)
assert node.compare("apple", "apple", "!=", "True") == (False,)
assert node.compare("banana", "apple", ">=", "True") == (True,)
assert node.compare("apple", "banana", "<=", "True") == (True,)
# Case-insensitive tests
assert node.compare("Hello", "hello", "==", "False") == (True,)
assert node.compare("Apple", "BANANA", "<", "False") == (True,)
assert node.compare("Grape", "GRAPE", ">=", "False") == (True,)
assert node.compare("ZEBRA", "zoo", "<", "False") == (True,)
assert node.compare("ZOO", "zebra", ">", "False") == (True,)
# Empty string tests
assert node.compare("", "hello", "<", "True") == (True,)
assert node.compare("hello", "", ">", "True") == (True,)
assert node.compare("", "", "==", "True") == (True,)
def test_number_in_range():
@@ -84,15 +123,27 @@ def test_number_in_range():
# Inclusive range tests
assert node.check_range(50, 0, 100) == (True,)
assert node.check_range(0, 0, 100) == (True,)
assert node.check_range(100, 0, 100) == (True,)
# Exclusive range tests
assert node.check_range(0, 0, 100, include_min="False") == (False,)
assert node.check_range(100, 0, 100, include_max="False") == (False,)
assert node.check_range(50, 0, 100, include_min="False", include_max="False") == (True,)
# Out of range tests
assert node.check_range(-1, 0, 100) == (False,)
assert node.check_range(101, 0, 100) == (False,)
# Edge cases
assert node.check_range(0, 0, 0) == (True,)
assert node.check_range(0, 0, 0, include_min="False") == (False,)
assert node.check_range(0, 0, 0, include_max="False") == (False,)
# Negative range tests
assert node.check_range(-50, -100, -10) == (True,)
assert node.check_range(-10, -100, -10) == (True,)
assert node.check_range(-100, -100, -10) == (True,)
def test_is_null():
node = IsNull()
@@ -100,16 +151,37 @@ def test_is_null():
assert node.check_null(0) == (False,)
assert node.check_null("") == (False,)
assert node.check_null([]) == (False,)
assert node.check_null(False) == (False,)
# Additional edge cases
assert node.check_null([None]) == (False,) # List containing None
assert node.check_null("None") == (False,) # String "None"
def test_compare_length():
node = CompareLength()
# Valid comparisons
# Valid comparisons with different operators
assert node.compare_length([1, 2, 3], "==", 3) == (True, 3)
assert node.compare_length("hello", "!=", 4) == (True, 5)
assert node.compare_length((1, 2), ">", 1) == (True, 2)
assert node.compare_length([1, 2, 3], "<", 4) == (True, 3)
assert node.compare_length("test", ">=", 4) == (True, 4)
assert node.compare_length([1], "<=", 1) == (True, 1)
# Different container types
assert node.compare_length({"a", "b", "c"}, "==", 3) == (True, 3) # Set
assert node.compare_length({"key1": 1, "key2": 2}, "==", 2) == (True, 2) # Dict
# Empty containers
assert node.compare_length([], "==", 0) == (True, 0)
assert node.compare_length("", "==", 0) == (True, 0)
assert node.compare_length({}, "==", 0) == (True, 0)
# Invalid comparisons
assert node.compare_length({"key": "value"}, "<", 1) == (False, 1)
assert node.compare_length(123, "==", 0) == (False, -1) # Non-container input edge case
assert node.compare_length({"key": "value"}, "<", 1) == (False, 1) # Dict with 1 item
assert node.compare_length(123, "==", 0) == (False, -1) # Non-container input
assert node.compare_length(None, "==", 0) == (False, -1) # None
# Error case with invalid operator
with pytest.raises(ValueError):
node.compare_length([1, 2, 3], "invalid", 3)
+10 -10
View File
@@ -21,19 +21,19 @@ def test_switch_case():
node = SwitchCase()
# Selector within range, valid cases
assert node.execute(0, "Case 0", "Case 1", "Case 2", "Case 3") == ("Case 0",)
assert node.execute(1, "Case 0", "Case 1", "Case 2", "Case 3") == ("Case 1",)
assert node.execute(3, "Case 0", "Case 1", "Case 2", "Case 3") == ("Case 3",)
assert node.execute(0, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3") == ("Case 0",)
assert node.execute(1, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3") == ("Case 1",)
assert node.execute(3, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3") == ("Case 3",)
# Selector out of range, fallback to default case
assert node.execute(4, "Case 0", "Case 1", "Case 2", "Case 3", default="Default") == ("Default",)
assert node.execute(99, "Case 0", "Case 1", "Case 2", "Case 3", default="Default") == ("Default",)
assert node.execute(4, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3", default="Default") == ("Default",)
assert node.execute(99, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3", default="Default") == ("Default",)
# Selector out of range, no default provided
assert node.execute(4, "Case 0", "Case 1", "Case 2", "Case 3") == (None,)
assert node.execute(4, case_0="Case 0", case_1="Case 1", case_2="Case 2", case_3="Case 3") == (None,)
# Edge cases
assert node.execute(0, None, "Case 1", "Case 2") == (None,) # Case with None
assert node.execute(2, "Case 0", "Case 1", "Case 2") == ("Case 2",)
assert node.execute(0, 123, False, "Case 2") == (123,) # Different data types
assert node.execute(10, "Case 0", "Case 1", "Case 2", default="Fallback") == ("Fallback",)
assert node.execute(0, case_0=None, case_1="Case 1", case_2="Case 2") == (None,) # Case with None
assert node.execute(2, case_0="Case 0", case_1="Case 1", case_2="Case 2") == ("Case 2",)
assert node.execute(0, case_0=123, case_1=False, case_2="Case 2") == (123,) # Different data types
assert node.execute(10, case_0="Case 0", case_1="Case 1", case_2="Case 2", default="Fallback") == ("Fallback",)
+2 -6
View File
@@ -14,7 +14,6 @@ from src.basic_data_handling.data_list_nodes import (
DataListGetItem,
DataListSetItem,
DataListContains,
DataListCreateEmpty,
DataListZip,
DataListFilter,
DataListMin,
@@ -119,21 +118,18 @@ def test_contains():
assert node.contains(list=[], value=["x"]) == (False,)
def test_create_empty():
node = DataListCreateEmpty()
assert node.create_empty() == ([],)
def test_zip():
node = DataListZip()
assert node.zip_lists(list1=[1, 2], list2=["a", "b"]) == ([[1, "a"], [2, "b"]],)
assert node.zip_lists(list1=[1], list2=["a", "b"]) == ([[1, "a"]],) # Shortest list length
assert node.zip_lists(list1=[1, 2, 3], list2=["a", "b"]) == ([[1, "a"], [2, "b"]],) # Different lengths
def test_filter():
node = DataListFilter()
assert node.filter_data(value=[1, 2, 3], filter=[False, True, False]) == ([1, 3],)
assert node.filter_data(value=[1, 2], filter=[True, True]) == ([],)
assert node.filter_data(value=[1, 2, 3], filter=[False, False, False]) == ([1, 2, 3],)
def test_min():
+141 -20
View File
@@ -34,30 +34,46 @@ def test_dict_get():
my_dict = {"key1": "value1", "key2": "value2"}
assert node.get(my_dict, "key1") == ("value1",)
assert node.get(my_dict, "key3", default="default_value") == ("default_value",)
# Test with no default specified
assert node.get(my_dict, "non_existent") == (None,)
def test_dict_set():
node = DictSet()
my_dict = {"key1": "value1"}
assert node.set(my_dict, "key2", "value2") == ({"key1": "value1", "key2": "value2"},)
# Test overwriting existing key
assert node.set(my_dict, "key1", "new_value") == ({"key1": "new_value"},)
# Test with empty dict
assert node.set({}, "key", "value") == ({"key": "value"},)
def test_dict_keys():
node = DictKeys()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.keys(my_dict) == (["key1", "key2"],)
# Test with empty dict
assert node.keys({}) == ([],)
def test_dict_values():
node = DictValues()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.values(my_dict) == (["value1", "value2"],)
# Test with empty dict
assert node.values({}) == ([],)
def test_dict_items():
node = DictItems()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.items(my_dict) == ([("key1", "value1"), ("key2", "value2")],)
# Note that the order might not be preserved, so we check if items are in the result
items = node.items(my_dict)[0]
assert len(items) == 2
assert ("key1", "value1") in items
assert ("key2", "value2") in items
# Test with empty dict
assert node.items({}) == ([],)
def test_dict_contains_key():
@@ -65,31 +81,52 @@ def test_dict_contains_key():
my_dict = {"key1": "value1"}
assert node.contains_key(my_dict, "key1") == (True,)
assert node.contains_key(my_dict, "key2") == (False,)
# Test with empty dict
assert node.contains_key({}, "any_key") == (False,)
def test_dict_from_keys():
node = DictFromKeys()
keys = ["key1", "key2"]
assert node.from_keys(keys, value="value") == ({"key1": "value", "key2": "value"},)
# Test without value (should use None)
assert node.from_keys(keys) == ({"key1": None, "key2": None},)
# Test with empty keys list
assert node.from_keys([]) == ({},)
def test_dict_pop():
node = DictPop()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.pop(my_dict, "key1") == ({"key2": "value2"}, "value1")
# Test with default value for non-existent key
assert node.pop(my_dict, "non_existent", default_value="default") == (my_dict, "default")
# Test with no default for non-existent key - should not modify dict
assert node.pop({"a": 1}, "b") == ({"a": 1}, None)
def test_dict_pop_item():
node = DictPopItem()
my_dict = {"key1": "value1"}
assert node.popitem(my_dict) == ({}, "key1", "value1", True)
result = node.popitem(my_dict)
# Since we only have one item, we know what should be popped
assert result[0] == {} # remaining dict is empty
assert result[1] == "key1" # popped key
assert result[2] == "value1" # popped value
assert result[3] is True # success
# Test with empty dict
assert node.popitem({}) == ({}, "", None, False)
def test_dict_set_default():
node = DictSetDefault()
my_dict = {"key1": "value1"}
# Test setting a key that doesn't exist
assert node.setdefault(my_dict, "key2", "default") == ({"key1": "value1", "key2": "default"}, "default")
# Test with a key that already exists
assert node.setdefault(my_dict, "key1", "new_default") == ({"key1": "value1"}, "value1")
# Test with empty dict
assert node.setdefault({}, "key", "value") == ({"key": "value"}, "value")
def test_dict_update():
@@ -97,69 +134,153 @@ def test_dict_update():
my_dict = {"key1": "value1"}
update_dict = {"key2": "value2"}
assert node.update(my_dict, update_dict) == ({"key1": "value1", "key2": "value2"},)
# Test updating with overlapping keys
assert node.update({"a": 1, "b": 2}, {"b": 3, "c": 4}) == ({"a": 1, "b": 3, "c": 4},)
# Test with empty update dict
assert node.update(my_dict, {}) == (my_dict,)
# Test with empty original dict
assert node.update({}, update_dict) == (update_dict,)
def test_dict_length():
node = DictLength()
my_dict = {"key1": "value1"}
assert node.length(my_dict) == (1,)
my_dict = {"key1": "value1", "key2": "value2"}
assert node.length(my_dict) == (2,)
# Test with empty dict
assert node.length({}) == (0,)
def test_dict_merge():
node = DictMerge()
dict1 = {"key1": "value1"}
dict2 = {"key2": "value2"}
# Test basic merge
assert node.merge(dict1, dict2) == ({"key1": "value1", "key2": "value2"},)
# Test with overlapping keys (later dicts override earlier ones)
assert node.merge({"a": 1}, {"a": 2}) == ({"a": 2},)
# Test with more than two dicts
result = node.merge({"a": 1}, {"b": 2}, {"c": 3})
assert result == ({"a": 1, "b": 2, "c": 3},)
# Test with empty dicts
assert node.merge(dict1, {}) == (dict1,)
assert node.merge({}, dict1) == (dict1,)
def test_dict_get_keys_values():
node = DictGetKeysValues()
my_dict = {"key1": "value1", "key2": "value2"}
assert node.get_keys_values(my_dict) == (["key1", "key2"], ["value1", "value2"])
keys, values = node.get_keys_values(my_dict)
# Check keys and values contents (order may vary)
assert set(keys) == {"key1", "key2"}
assert set(values) == {"value1", "value2"}
# Test with empty dict
assert node.get_keys_values({}) == ([], [])
def test_dict_remove():
node = DictRemove()
my_dict = {"key1": "value1"}
assert node.remove(my_dict, "key1") == ({}, True)
my_dict = {"key1": "value1", "key2": "value2"}
# Test successful removal
assert node.remove(my_dict, "key1") == ({"key2": "value2"}, True)
# Test removal of non-existent key
assert node.remove(my_dict, "non_existent") == (my_dict, False)
# Test with empty dict
assert node.remove({}, "any_key") == ({}, False)
def test_dict_filter_by_keys():
node = DictFilterByKeys()
my_dict = {"key1": "value1", "key2": "value2"}
keys = ["key1"]
assert node.filter_by_keys(my_dict, keys) == ({"key1": "value1"},)
my_dict = {"key1": "value1", "key2": "value2", "key3": "value3"}
# Test with subset of keys
assert node.filter_by_keys(my_dict, ["key1", "key3"]) == ({"key1": "value1", "key3": "value3"},)
# Test with non-existent keys
assert node.filter_by_keys(my_dict, ["key1", "non_existent"]) == ({"key1": "value1"},)
# Test with empty keys list
assert node.filter_by_keys(my_dict, []) == ({},)
# Test with empty dict
assert node.filter_by_keys({}, ["any_key"]) == ({},)
def test_dict_exclude_keys():
node = DictExcludeKeys()
my_dict = {"key1": "value1", "key2": "value2"}
keys = ["key1"]
assert node.exclude_keys(my_dict, keys) == ({"key2": "value2"},)
my_dict = {"key1": "value1", "key2": "value2", "key3": "value3"}
# Test excluding some keys
assert node.exclude_keys(my_dict, ["key1", "key3"]) == ({"key2": "value2"},)
# Test excluding non-existent keys
assert node.exclude_keys(my_dict, ["non_existent"]) == (my_dict,)
# Test excluding all keys
assert node.exclude_keys(my_dict, ["key1", "key2", "key3"]) == ({},)
# Test with empty exclude list
assert node.exclude_keys(my_dict, []) == (my_dict,)
# Test with empty dict
assert node.exclude_keys({}, ["any_key"]) == ({},)
def test_dict_get_multiple():
node = DictGetMultiple()
my_dict = {"key1": "value1", "key2": "value2"}
keys = ["key1", "key3"]
assert node.get_multiple(my_dict, keys, default="default") == (["value1", "default"],)
# Test getting existing keys
assert node.get_multiple(my_dict, ["key1", "key2"]) == (["value1", "value2"],)
# Test with mix of existing and non-existent keys
assert node.get_multiple(my_dict, ["key1", "key3"], default="default") == (["value1", "default"],)
# Test with only non-existent keys
assert node.get_multiple(my_dict, ["key3", "key4"], default="default") == (["default", "default"],)
# Test with empty keys list
assert node.get_multiple(my_dict, []) == ([],)
# Test with empty dict
assert node.get_multiple({}, ["key1"], default="default") == (["default"],)
def test_dict_invert():
node = DictInvert()
my_dict = {"key1": "value1", "key2": "value2"}
# Test basic inversion
assert node.invert(my_dict) == ({"value1": "key1", "value2": "key2"}, True)
# Test with duplicated values
result, success = node.invert({"key1": "value", "key2": "value"})
# With duplicate values, the last key wins
assert result == {"value": "key2"}
assert success is True
# Test with empty dict
assert node.invert({}) == ({}, True)
def test_dict_create_from_lists():
node = DictCreateFromLists()
keys = ["key1", "key2"]
values = ["value1", "value2"]
assert node.create_from_lists(keys, values) == ({"key1": "value1", "key2": "value2"},)
keys = ["key1", "key2", "key3"]
values = ["value1", "value2", "value3"]
# Test with matching length lists
assert node.create_from_lists(keys, values) == ({"key1": "value1", "key2": "value2", "key3": "value3"},)
# Test with more keys than values
assert node.create_from_lists(keys, ["value1", "value2"]) == ({"key1": "value1", "key2": "value2"},)
# Test with more values than keys
assert node.create_from_lists(["key1", "key2"], values) == ({"key1": "value1", "key2": "value2"},)
# Test with empty lists
assert node.create_from_lists([], []) == ({},)
def test_dict_compare():
node = DictCompare()
dict1 = {"key1": "value1"}
dict2 = {"key1": "value1"}
# Test identical dictionaries
dict1 = {"key1": "value1", "key2": "value2"}
dict2 = {"key1": "value1", "key2": "value2"}
assert node.compare(dict1, dict2) == (True, [], [], [])
# Test dictionaries with different values
dict3 = {"key1": "value1", "key2": "different"}
are_equal, only_in_1, only_in_2, diff_values = node.compare(dict1, dict3)
assert are_equal is False
assert only_in_1 == []
assert only_in_2 == []
assert "key2" in diff_values
# Test dictionaries with different keys
dict4 = {"key1": "value1", "key3": "value3"}
are_equal, only_in_1, only_in_2, diff_values = node.compare(dict1, dict4)
assert are_equal is False
assert "key2" in only_in_1
assert "key3" in only_in_2
assert diff_values == []
# Test empty dictionaries
assert node.compare({}, {}) == (True, [], [], [])
+26
View File
@@ -1,9 +1,12 @@
import math
import pytest
from src.basic_data_handling.float_nodes import (
FloatCreate,
FloatAdd,
FloatSubtract,
FloatMultiply,
FloatDivide,
FloatDivideSafe,
FloatPower,
FloatRound,
FloatIsInteger,
@@ -12,6 +15,15 @@ from src.basic_data_handling.float_nodes import (
FloatFromHex,
)
def test_float_create():
node = FloatCreate()
assert node.create("3.14") == (3.14,)
assert node.create("-2.5") == (-2.5,)
assert node.create("0.0") == (0.0,)
with pytest.raises(ValueError):
node.create("not a number")
def test_float_add():
node = FloatAdd()
assert node.add(2.2, 2.2) == (4.4,)
@@ -41,6 +53,20 @@ def test_float_divide():
node.divide(5.0, 0.0)
def test_float_divide_safe():
node = FloatDivideSafe()
assert node.divide(7.0, 2.0) == (3.5,)
assert node.divide(-6.0, 3.0) == (-2.0,)
assert node.divide(5.0, 0.0) == (float('inf'),)
assert node.divide(-5.0, 0.0) == (float('-inf'),)
# Special handling for NaN because NaN != NaN
result = node.divide(0.0, 0.0)
assert len(result) == 1
assert math.isnan(result[0])
def test_float_power():
node = FloatPower()
assert node.power(2.0, 3.0) == (8.0,)
+32 -1
View File
@@ -4,12 +4,15 @@ from src.basic_data_handling.int_nodes import (
IntSubtract,
IntMultiply,
IntDivide,
IntDivideSafe,
IntModulus,
IntPower,
IntBitLength,
IntToBytes,
IntFromBytes,
IntBitCount,
IntCreate,
IntCreateWithBase,
)
@@ -83,4 +86,32 @@ def test_int_bit_count():
node = IntBitCount()
assert node.bit_count(0) == (0,) # Zero has no 1 bits
assert node.bit_count(255) == (8,) # 255 is 11111111 (8 ones)
assert node.bit_count(-255) == (8,) # Negative integer has the same bit count
assert node.bit_count(-255) == (8,) # Negative integer has the same bit count
def test_int_create():
node = IntCreate()
assert node.create("42") == (42,)
assert node.create("-100") == (-100,)
assert node.create("0") == (0,)
assert node.create("0xFF") == (255,) # Hexadecimal
assert node.create("0b1010") == (10,) # Binary
assert node.create("0o777") == (511,) # Octal
def test_int_create_with_base():
node = IntCreateWithBase()
assert node.create("42", 10) == (42,)
assert node.create("FF", 16) == (255,)
assert node.create("1010", 2) == (10,)
assert node.create("777", 8) == (511,)
with pytest.raises(ValueError):
node.create("FF", 10) # Invalid characters for base 10
def test_int_divide_safe():
node = IntDivideSafe()
assert node.divide(10, 2, 9223372036854775807) == (5,)
assert node.divide(9, 4, 9223372036854775807) == (2,) # Integer division
assert node.divide(10, 0, 9223372036854775807) == (9223372036854775807,) # Positive infinity
assert node.divide(-10, 0, 9223372036854775807) == (-9223372036854775807,) # Negative infinity
+57 -1
View File
@@ -1,5 +1,10 @@
import pytest
from src.basic_data_handling.list_nodes import (
ListCreate,
ListCreateFromBoolean,
ListCreateFromFloat,
ListCreateFromInt,
ListCreateFromString,
ListAppend,
ListExtend,
ListInsert,
@@ -15,7 +20,9 @@ from src.basic_data_handling.list_nodes import (
ListSetItem,
ListContains,
ListMin,
ListMax
ListMax,
ListToDataList,
ListToSet
)
@@ -130,3 +137,52 @@ def test_list_max():
assert node.find_max([1, 2, 3]) == (3,)
assert node.find_max(["b", "a", "c"]) == ("c",)
assert node.find_max([]) == (None,)
def test_list_create():
node = ListCreate()
assert node.create_list(item_0=1, item_1=2, item_2=3) == ([1, 2, 3],)
assert node.create_list() == ([],) # Empty list
assert node.create_list(item_0="test") == (["test"],)
def test_list_create_from_boolean():
node = ListCreateFromBoolean()
assert node.create_list(item_0=True, item_1=False, item_2=True) == ([True, False, True],)
assert node.create_list(item_0=True) == ([True],)
assert node.create_list() == ([],)
def test_list_create_from_float():
node = ListCreateFromFloat()
assert node.create_list(item_0=1.1, item_1=2.2, item_2=3.3) == ([1.1, 2.2, 3.3],)
assert node.create_list(item_0=0.0) == ([0.0],)
assert node.create_list() == ([],)
def test_list_create_from_int():
node = ListCreateFromInt()
assert node.create_list(item_0=1, item_1=2, item_2=3) == ([1, 2, 3],)
assert node.create_list(item_0=0) == ([0],)
assert node.create_list() == ([],)
def test_list_create_from_string():
node = ListCreateFromString()
assert node.create_list(item_0="a", item_1="b", item_2="c") == (["a", "b", "c"],)
assert node.create_list(item_0="test") == (["test"],)
assert node.create_list() == ([],)
def test_list_to_data_list():
node = ListToDataList()
input_list = [1, 2, 3]
# The output should be the same list but marked as a data list by the OUTPUT_IS_LIST = (True,) flag
assert node.convert(input_list) == (input_list,)
def test_list_to_set():
node = ListToSet()
assert node.convert([1, 2, 3, 2, 1]) == ({1, 2, 3},)
assert node.convert([]) == (set(),)
assert node.convert(["a", "b", "a", "c"]) == ({"a", "b", "c"},)
+109 -3
View File
@@ -12,6 +12,10 @@ def test_math_sin():
assert node.calculate(0, "radians") == (0.0,)
assert node.calculate(math.pi / 2, "radians") == (1.0,)
assert node.calculate(30, "degrees") == (math.sin(math.radians(30)),)
# Test with string input
assert node.calculate("45", "degrees")[0] == pytest.approx(math.sin(math.radians(45)))
# Test negative angle
assert node.calculate(-90, "degrees")[0] == pytest.approx(-1.0)
def test_math_cos():
@@ -19,14 +23,25 @@ def test_math_cos():
assert node.calculate(0, "radians") == (1.0,)
assert node.calculate(math.pi, "radians") == (-1.0,)
assert node.calculate(60, "degrees") == (math.cos(math.radians(60)),)
# Test with string input
assert node.calculate("60", "degrees")[0] == pytest.approx(0.5)
# Test negative angle
assert node.calculate(-180, "degrees")[0] == pytest.approx(-1.0)
def test_math_tan():
node = MathTan()
assert node.calculate(0, "radians") == (0.0,)
assert node.calculate(45, "degrees") == (math.tan(math.radians(45)),)
# Test with string input
assert node.calculate("30", "degrees")[0] == pytest.approx(math.tan(math.radians(30)))
# Test negative angle
assert node.calculate(-45, "degrees")[0] == pytest.approx(-1.0)
# Test undefined tangent
with pytest.raises(ValueError): # Undefined tangent
node.calculate(90, "degrees")
with pytest.raises(ValueError): # Undefined tangent
node.calculate(math.pi/2, "radians")
def test_math_asin():
@@ -34,6 +49,13 @@ def test_math_asin():
assert node.calculate(0, "radians") == (0.0,)
assert node.calculate(1, "radians") == (math.pi / 2,)
assert node.calculate(0.5, "degrees") == (math.degrees(math.asin(0.5)),)
# Test with string input
assert node.calculate("0.5", "degrees")[0] == pytest.approx(30.0)
# Test boundary values
assert node.calculate(-1, "degrees")[0] == pytest.approx(-90.0)
# Test out of range values
with pytest.raises(ValueError):
node.calculate(1.1, "degrees")
def test_math_acos():
@@ -41,6 +63,13 @@ def test_math_acos():
assert node.calculate(1, "radians") == (0.0,)
assert node.calculate(0, "radians") == (math.pi / 2,)
assert node.calculate(0.5, "degrees") == (math.degrees(math.acos(0.5)),)
# Test with string input
assert node.calculate("0.5", "degrees")[0] == pytest.approx(60.0)
# Test boundary values
assert node.calculate(-1, "degrees")[0] == pytest.approx(180.0)
# Test out of range values
with pytest.raises(ValueError):
node.calculate(1.1, "degrees")
def test_math_atan():
@@ -48,6 +77,10 @@ def test_math_atan():
assert node.calculate(0, "radians") == (0.0,)
assert node.calculate(1, "degrees") == (45.0,)
assert node.calculate(-1, "radians") == (-math.pi / 4,)
# Test with string input
assert node.calculate("1.0", "degrees")[0] == pytest.approx(45.0)
# Test large values
assert node.calculate(1000, "degrees")[0] == pytest.approx(89.94, abs=0.01)
def test_math_atan2():
@@ -55,13 +88,24 @@ def test_math_atan2():
assert node.calculate(0, 1, "radians") == (0.0,)
assert node.calculate(1, 1, "degrees") == (45.0,)
assert node.calculate(-1, -1, "radians") == (-3 * math.pi / 4,)
# Test with string input
assert node.calculate("1", "1", "degrees")[0] == pytest.approx(45.0)
# Test special cases
assert node.calculate(1, 0, "degrees")[0] == pytest.approx(90.0)
assert node.calculate(-1, 0, "degrees")[0] == pytest.approx(-90.0)
assert node.calculate(0, -1, "degrees")[0] == pytest.approx(180.0)
def test_math_sqrt():
node = MathSqrt()
assert node.calculate(4) == (2.0,)
assert node.calculate(0) == (0.0,)
with pytest.raises(ValueError): # Negative numbers
# Test with string input
assert node.calculate("9")[0] == pytest.approx(3.0)
# Test with float input
assert node.calculate(2.25)[0] == pytest.approx(1.5)
# Test negative numbers (should raise ValueError)
with pytest.raises(ValueError):
node.calculate(-1)
@@ -70,6 +114,10 @@ def test_math_exp():
assert node.calculate(0) == (1.0,)
assert node.calculate(1) == (math.e,)
assert node.calculate(-1) == (1 / math.e,)
# Test with string input
assert node.calculate("2")[0] == pytest.approx(math.exp(2))
# Test with large input
assert node.calculate(10)[0] == pytest.approx(math.exp(10))
def test_math_log():
@@ -77,16 +125,30 @@ def test_math_log():
assert node.calculate(1) == (0.0,)
assert node.calculate(math.e) == (1.0,)
assert node.calculate(8, base=2) == (3.0,)
with pytest.raises(ValueError): # Log of non-positive
# Test with string input
assert node.calculate("10", base="10")[0] == pytest.approx(1.0)
# Test with default base (e)
assert node.calculate(math.exp(5))[0] == pytest.approx(5.0)
# Test with log of non-positive (should raise ValueError)
with pytest.raises(ValueError):
node.calculate(-1)
with pytest.raises(ValueError):
node.calculate(0)
def test_math_log10():
node = MathLog10()
assert node.calculate(1) == (0.0,)
assert node.calculate(10) == (1.0,)
with pytest.raises(ValueError): # Log of non-positive
# Test with string input
assert node.calculate("100")[0] == pytest.approx(2.0)
# Test with non-integer values
assert node.calculate(2)[0] == pytest.approx(math.log10(2))
# Test with log of non-positive (should raise ValueError)
with pytest.raises(ValueError):
node.calculate(0)
with pytest.raises(ValueError):
node.calculate(-10)
def test_math_degrees():
@@ -94,6 +156,10 @@ def test_math_degrees():
assert node.calculate(math.pi) == (180.0,)
assert node.calculate(math.pi / 2) == (90.0,)
assert node.calculate(0) == (0.0,)
# Test with string input
assert node.calculate("3.14159")[0] == pytest.approx(180.0, abs=0.01)
# Test with negative values
assert node.calculate(-math.pi)[0] == pytest.approx(-180.0)
def test_math_radians():
@@ -101,6 +167,12 @@ def test_math_radians():
assert node.calculate(180) == (math.pi,)
assert node.calculate(90) == (math.pi / 2,)
assert node.calculate(0) == (0.0,)
# Test with string input
assert node.calculate("180")[0] == pytest.approx(math.pi)
# Test with negative values
assert node.calculate(-180)[0] == pytest.approx(-math.pi)
# Test with decimal values
assert node.calculate(45.5)[0] == pytest.approx(math.radians(45.5))
def test_math_floor():
@@ -108,6 +180,12 @@ def test_math_floor():
assert node.calculate(1.7) == (1,)
assert node.calculate(-1.7) == (-2,)
assert node.calculate(0.0) == (0,)
# Test with string input
assert node.calculate("3.9")[0] == 3
# Test with integer input
assert node.calculate(5)[0] == 5
# Test with very small decimals
assert node.calculate(0.0001)[0] == 0
def test_math_ceil():
@@ -115,6 +193,12 @@ def test_math_ceil():
assert node.calculate(1.2) == (2,)
assert node.calculate(-1.2) == (-1,)
assert node.calculate(0.0) == (0,)
# Test with string input
assert node.calculate("3.1")[0] == 4
# Test with integer input
assert node.calculate(5)[0] == 5
# Test with very small decimals
assert node.calculate(0.0001)[0] == 1
def test_math_abs():
@@ -122,16 +206,26 @@ def test_math_abs():
assert node.calculate(-5) == (5,)
assert node.calculate(5) == (5,)
assert node.calculate(0) == (0,)
# Test with string input
assert node.calculate("-3.14")[0] == pytest.approx(3.14)
# Test with float input
assert node.calculate(-2.5)[0] == 2.5
# Test with zero
assert node.calculate(0)[0] == 0
def test_math_pi():
node = MathPi()
assert node.calculate() == (math.pi,)
# Verify precision to at least 10 decimal places
assert abs(node.calculate()[0] - 3.1415926535) < 1e-10
def test_math_e():
node = MathE()
assert node.calculate() == (math.e,)
# Verify precision to at least 10 decimal places
assert abs(node.calculate()[0] - 2.7182818284) < 1e-10
def test_math_min():
@@ -139,6 +233,12 @@ def test_math_min():
assert node.calculate(5, 10) == (5,)
assert node.calculate(-1, 0) == (-1,)
assert node.calculate(3.5, 2.5) == (2.5,)
# Test with string inputs
assert node.calculate("7", "3")[0] == 3.0
# Test with mixed types
assert node.calculate(5, "2.5")[0] == 2.5
# Test with equal values
assert node.calculate(10, 10)[0] == 10
def test_math_max():
@@ -146,3 +246,9 @@ def test_math_max():
assert node.calculate(5, 10) == (10,)
assert node.calculate(-1, 0) == (0,)
assert node.calculate(3.5, 2.5) == (3.5,)
# Test with string inputs
assert node.calculate("7", "3")[0] == 7.0
# Test with mixed types
assert node.calculate(5, "7.5")[0] == 7.5
# Test with equal values
assert node.calculate(10, 10)[0] == 10
+227 -6
View File
@@ -1,5 +1,6 @@
import os
import pytest
import platform
from src.basic_data_handling.path_nodes import (
PathJoin, PathAbspath, PathExists, PathIsFile, PathIsDir, PathGetSize,
@@ -14,21 +15,38 @@ def test_path_join():
assert node.join_paths("folder", "file.txt") == (os.path.join("folder", "file.txt"),)
assert node.join_paths("folder", "") == (os.path.join("folder"),)
assert node.join_paths(".", "") == (".",)
# Test join with empty first path
assert node.join_paths("", "file.txt") == ("file.txt",)
# Test with multiple path components
nested_path = os.path.join("folder", "subfolder", "file.txt")
assert node.join_paths("folder", os.path.join("subfolder", "file.txt")) == (nested_path,)
def test_path_abspath():
node = PathAbspath()
assert node.get_abspath(".") == (os.path.abspath("."),)
assert node.get_abspath("folder/file.txt") == (os.path.abspath("folder/file.txt"),)
# Test with empty string (should return current directory)
assert node.get_abspath("") == (os.path.abspath(os.getcwd()),)
# Test with relative path with special characters
assert node.get_abspath("./file.txt") == (os.path.abspath("./file.txt"),)
assert node.get_abspath("../file.txt") == (os.path.abspath("../file.txt"),)
def test_path_exists(tmp_path):
node = PathExists()
existing_file = tmp_path / "file.txt"
existing_file.write_text("content")
existing_dir = tmp_path / "directory"
existing_dir.mkdir()
assert node.check_exists(str(existing_file)) == (True,)
assert node.check_exists(str(existing_dir)) == (True,)
assert node.check_exists("nonexistent.file") == (False,)
# Test with special paths
assert node.check_exists(".") == (True,)
# Test with path containing non-existent parent directory
assert node.check_exists(str(tmp_path / "nonexistent" / "file.txt")) == (False,)
def test_path_is_file(tmp_path):
@@ -41,6 +59,19 @@ def test_path_is_file(tmp_path):
assert node.check_is_file(str(file)) == (True,)
assert node.check_is_file(str(directory)) == (False,)
assert node.check_is_file("nonexistent.file") == (False,)
# Test with empty path
assert node.check_is_file("") == (False,)
# Test with special paths
assert node.check_is_file(".") == (False,)
# Test with symlink to file if platform supports
if hasattr(os, 'symlink'):
try:
symlink = tmp_path / "symlink.txt"
os.symlink(str(file), str(symlink))
assert node.check_is_file(str(symlink)) == (True,)
except (OSError, NotImplementedError):
# Symlinks might not be supported on all platforms
pass
def test_path_is_dir(tmp_path):
@@ -53,14 +84,36 @@ def test_path_is_dir(tmp_path):
assert node.check_is_dir(str(directory)) == (True,)
assert node.check_is_dir(str(file)) == (False,)
assert node.check_is_dir("nonexistent.dir") == (False,)
# Test with special paths
assert node.check_is_dir(".") == (True,)
assert node.check_is_dir("") == (False,)
# Test with nested directory
nested_dir = directory / "nested"
nested_dir.mkdir()
assert node.check_is_dir(str(nested_dir)) == (True,)
# Test with symlink to directory if platform supports
if hasattr(os, 'symlink'):
try:
symlink = tmp_path / "symlink_dir"
os.symlink(str(directory), str(symlink), target_is_directory=True)
assert node.check_is_dir(str(symlink)) == (True,)
except (OSError, NotImplementedError, TypeError):
# Symlinks might not be supported on all platforms
pass
def test_path_get_size(tmp_path):
node = PathGetSize()
file = tmp_path / "file.txt"
file.write_text("content")
empty_file = tmp_path / "empty.txt"
empty_file.write_text("")
large_file = tmp_path / "large.txt"
large_file.write_text("a" * 1024) # 1KB file
assert node.get_size(str(file)) == (len("content"),)
assert node.get_size(str(empty_file)) == (0,)
assert node.get_size(str(large_file)) == (1024,)
with pytest.raises(FileNotFoundError):
node.get_size("nonexistent.file")
@@ -78,36 +131,88 @@ def test_path_split():
assert node.split_path(path) == (os.path.dirname(path), os.path.basename(path))
assert node.split_path("file.txt") == ("", "file.txt")
# Test with empty path
assert node.split_path("") == ("", "")
# Test with just directory separator
assert node.split_path("/") == ("/", "")
# Test with nested paths
nested_path = os.path.join("folder", "subfolder", "file.txt")
assert node.split_path(nested_path) == (os.path.dirname(nested_path), "file.txt")
# Test with trailing separator
dir_with_sep = "folder" + os.sep
assert node.split_path(dir_with_sep) == ("folder", "")
def test_path_splitext():
node = PathSplitExt()
assert node.split_ext("file.txt") == ("file", ".txt")
assert node.split_ext("file") == ("file", "")
# Test with multiple extensions
assert node.split_ext("file.tar.gz") == ("file.tar", ".gz")
# Test with path containing directory
path = os.path.join("folder", "file.txt")
assert node.split_ext(path) == (os.path.join("folder", "file"), ".txt")
# Test with hidden file
assert node.split_ext(".gitignore") == (".gitignore", "")
# Test with empty string
assert node.split_ext("") == ("", "")
def test_path_basename():
node = PathBasename()
assert node.get_basename("folder/file.txt") == ("file.txt",)
assert node.get_basename("file.txt") == ("file.txt",)
# Test with trailing slash
assert node.get_basename("folder/") == ("",)
# Test with empty path
assert node.get_basename("") == ("",)
# Test with root directory
assert node.get_basename("/") == ("",)
# Test with multiple nested directories
assert node.get_basename("folder/subfolder/file.txt") == ("file.txt",)
def test_path_dirname():
node = PathDirname()
assert node.get_dirname("folder/file.txt") == ("folder",)
assert node.get_dirname("file.txt") == ("",)
# Test with trailing slash
assert node.get_dirname("folder/") == ("folder",)
# Test with empty path
assert node.get_dirname("") == ("",)
# Test with root directory
assert node.get_dirname("/") == ("/",)
# Test with nested directories
assert node.get_dirname("folder/subfolder/file.txt") == (os.path.join("folder", "subfolder"),)
def test_path_get_extension():
node = PathGetExtension()
assert node.get_extension("file.txt") == (".txt",)
assert node.get_extension("file") == ("",)
# Test with multiple dots
assert node.get_extension("file.tar.gz") == (".gz",)
# Test with hidden file
assert node.get_extension(".gitignore") == ("",)
# Test with path containing directory
assert node.get_extension("folder/file.txt") == (".txt",)
# Test with empty string
assert node.get_extension("") == ("",)
def test_path_normalize():
node = PathNormalize()
assert node.normalize_path("folder/../file.txt") == (os.path.normpath("folder/../file.txt"),)
assert node.normalize_path("folder//file.txt") == (os.path.normpath("folder//file.txt"),)
# Test with empty path
assert node.normalize_path("") == (".",)
# Test with current directory references
assert node.normalize_path("./folder/./file.txt") == (os.path.normpath("./folder/./file.txt"),)
# Test with parent directory references that cancel out
assert node.normalize_path("folder/../folder/file.txt") == (os.path.normpath("folder/../folder/file.txt"),)
# Test with mixed separators
mixed_sep_path = "folder" + os.sep + "subfolder" + ("/" if os.sep == "\\" else "\\") + "file.txt"
assert node.normalize_path(mixed_sep_path) == (os.path.normpath(mixed_sep_path),)
def test_path_relative(tmp_path):
@@ -119,6 +224,16 @@ def test_path_relative(tmp_path):
assert node.get_relative_path(str(target), str(start)) == (os.path.relpath(str(target), str(start)),)
assert node.get_relative_path(str(target)) == (os.path.relpath(str(target), os.getcwd()),)
# Test with same path
assert node.get_relative_path(str(start), str(start)) == (".",)
# Test with sibling paths
sibling = tmp_path / "sibling"
sibling.mkdir()
expected = os.path.relpath(str(sibling), str(start))
assert node.get_relative_path(str(sibling), str(start)) == (expected,)
# Test with parent directory
parent_rel = os.path.relpath(str(tmp_path), str(target))
assert node.get_relative_path(str(tmp_path), str(target)) == (parent_rel,)
def test_path_glob(tmp_path):
@@ -126,56 +241,162 @@ def test_path_glob(tmp_path):
(tmp_path / "file1.txt").write_text("content")
(tmp_path / "file2.txt").write_text("content")
(tmp_path / "file3.txt").write_text("content")
(tmp_path / "image1.jpg").write_text("image")
(tmp_path / "image2.png").write_text("image")
# Create subdirectory with files
subdir = tmp_path / "subdir"
subdir.mkdir()
(subdir / "subfile1.txt").write_text("sub content")
(subdir / "subfile2.txt").write_text("sub content")
# Test simple glob pattern
result = node.glob_paths(str(tmp_path / "*.txt"))
expected = sorted([str(tmp_path / f"file{i}.txt") for i in range(1, 4)])
assert sorted(result[0]) == expected
recursive_node = PathGlob()
assert recursive_node.glob_paths(str(tmp_path), recursive="True")[0]
# Test with image files
img_result = node.glob_paths(str(tmp_path / "image*"))
img_expected = sorted([str(tmp_path / "image1.jpg"), str(tmp_path / "image2.png")])
assert sorted(img_result[0]) == img_expected
# Test with character class
char_result = node.glob_paths(str(tmp_path / "file[1-2].txt"))
char_expected = sorted([str(tmp_path / f"file{i}.txt") for i in range(1, 3)])
assert sorted(char_result[0]) == char_expected
# Test recursive globbing
recursive_result = node.glob_paths(str(tmp_path / "**/*.txt"), recursive=True)
recursive_expected = sorted([
str(tmp_path / f"file{i}.txt") for i in range(1, 4)
] + [
str(subdir / f"subfile{i}.txt") for i in range(1, 3)
])
assert sorted(recursive_result[0]) == recursive_expected
# Test with no matches
no_match_result = node.glob_paths(str(tmp_path / "nomatch*.txt"))
assert len(no_match_result[0]) == 0
def test_path_expand_vars(monkeypatch):
node = PathExpandVars()
monkeypatch.setenv("TEST_VAR", "test_value")
monkeypatch.setenv("PATH_VAR", "path/to/somewhere")
monkeypatch.setenv("EMPTY_VAR", "")
assert node.expand_vars("$TEST_VAR/path") == ("test_value/path",)
assert node.expand_vars("${TEST_VAR}/path") == ("test_value/path",)
# Test with multiple variables
assert node.expand_vars("$TEST_VAR/$PATH_VAR") == ("test_value/path/to/somewhere",)
# Test with empty variable
assert node.expand_vars("$EMPTY_VAR/path") == ("/path",)
# Test with no variables
assert node.expand_vars("regular/path") == ("regular/path",)
# Test with empty string
assert node.expand_vars("") == ("",)
def test_path_get_cwd():
node = PathGetCwd()
assert node.get_cwd() == (os.getcwd(),)
# Verify that the result is an absolute path
assert os.path.isabs(node.get_cwd()[0])
# Verify that the directory exists
assert os.path.exists(node.get_cwd()[0])
assert os.path.isdir(node.get_cwd()[0])
def test_path_list_dir(tmp_path):
node = PathListDir()
# Set up a directory structure for testing
(tmp_path / "file1.txt").write_text("content")
(tmp_path / "file2.txt").write_text("content")
(tmp_path / "image.jpg").write_text("image")
(tmp_path / "dir1").mkdir()
(tmp_path / "dir2").mkdir()
(tmp_path / ".hidden_file").write_text("hidden")
(tmp_path / ".hidden_dir").mkdir()
# Test listing all entries
all_entries = node.list_directory(str(tmp_path))
assert "file1.txt" in all_entries[0]
assert "file2.txt" in all_entries[0]
assert "image.jpg" in all_entries[0]
assert "dir1" in all_entries[0]
assert "dir2" in all_entries[0]
assert ".hidden_file" in all_entries[0]
assert ".hidden_dir" in all_entries[0]
only_files = node.list_directory(str(tmp_path), files_only="True")
# Test listing only files
only_files = node.list_directory(str(tmp_path), files_only=True)
assert "file1.txt" in only_files[0]
assert "file2.txt" in only_files[0]
assert "image.jpg" in only_files[0]
assert ".hidden_file" in only_files[0]
assert "dir1" not in only_files[0]
assert "dir2" not in only_files[0]
assert ".hidden_dir" not in only_files[0]
only_dirs = node.list_directory(str(tmp_path), dirs_only="True")
# Test listing only directories
only_dirs = node.list_directory(str(tmp_path), dirs_only=True)
assert "file1.txt" not in only_dirs[0]
assert "file2.txt" not in only_dirs[0]
assert "image.jpg" not in only_dirs[0]
assert ".hidden_file" not in only_dirs[0]
assert "dir1" in only_dirs[0]
assert "dir2" in only_dirs[0]
assert ".hidden_dir" in only_dirs[0]
# Test with non-existent directory
with pytest.raises(FileNotFoundError):
node.list_directory(str(tmp_path / "nonexistent"))
# Test with file instead of directory
with pytest.raises(NotADirectoryError):
node.list_directory(str(tmp_path / "file1.txt"))
# Test with empty path (should use current directory)
assert len(node.list_directory("")[0]) > 0
def test_path_is_absolute():
node = PathIsAbsolute()
# Test with absolute paths
assert node.check_is_absolute("/") == (True,)
assert node.check_is_absolute(os.path.abspath(".")) == (True,)
# Test with relative paths
assert node.check_is_absolute("relative/path") == (False,)
assert node.check_is_absolute("./path") == (False,)
assert node.check_is_absolute("../path") == (False,)
# Test with empty path
assert node.check_is_absolute("") == (False,)
# Test Windows-specific paths if applicable
if platform.system() == "Windows":
assert node.check_is_absolute("C:\\Windows") == (True,)
assert node.check_is_absolute("C:/Windows") == (True,)
assert node.check_is_absolute("\\\\server\\share") == (True,)
assert node.check_is_absolute("C:relative\\path") == (False,)
def test_path_common_prefix():
node = PathCommonPrefix()
# Test with paths having common prefix
path1 = "/path/to/file1.txt"
path2 = "/path/to/file2.txt"
assert node.get_common_prefix(path1, path2) == ("/path/to/file",)
assert node.get_common_prefix(path1, path2) == (os.path.commonprefix([path1, path2]),)
# Test with paths without common prefix
path3 = "/path/to/file.txt"
path4 = "/different/path/file.txt"
assert node.get_common_prefix(path3, path4) == ("/",)
# Test with exact same paths
assert node.get_common_prefix(path1, path1) == (path1,)
# Test with one path being a prefix of the other
path5 = "/path/to"
path6 = "/path/to/subdir/file.txt"
assert node.get_common_prefix(path5, path6) == ("/path/to",)
# Test with both empty paths
assert node.get_common_prefix("", "") == ("",)
+29 -1
View File
@@ -5,7 +5,10 @@ from src.basic_data_handling.regex_nodes import (
RegexFindallDataList,
RegexSplitDataList,
RegexSub,
RegexTest
RegexTest,
RegexFindallList,
RegexSearchGroupsList,
RegexSplitList
)
def test_regex_search_groups():
@@ -16,6 +19,14 @@ def test_regex_search_groups():
assert node.search_groups(r"(\w+)", "word") == (["word"],) # Single group
def test_regex_search_groups_list():
node = RegexSearchGroupsList()
assert node.search_groups(r"(foo)(bar)", "foobar") == (["foo", "bar"],)
assert node.search_groups(r"(test)(\d+)", "test123") == (["test", "123"],)
assert node.search_groups(r"(nothing)", "no match here") == ([],) # No match
assert node.search_groups(r"(\w+)", "word") == (["word"],) # Single group
def test_regex_group_dict():
node = RegexGroupDict()
pattern = r"(?P<word>\w+)-(?P<number>\d+)"
@@ -33,6 +44,14 @@ def test_regex_findall():
assert node.findall(r"(a)(b)", "ab ab") == ([("a", "b"), ("a", "b")],) # Multiple groups
def test_regex_findall_list():
node = RegexFindallList()
assert node.findall(r"\d+", "abc 123 def 456") == (["123", "456"],)
assert node.findall(r"\b[a-zA-Z]+\b", "Multiple words here") == (["Multiple", "words", "here"],)
assert node.findall(r"nonexistent", "no match") == ([],) # No match
assert node.findall(r"(a)(b)", "ab ab") == ([("a", "b"), ("a", "b")],) # Multiple groups
def test_regex_split():
node = RegexSplitDataList()
pattern = r",\s*"
@@ -42,6 +61,15 @@ def test_regex_split():
assert node.split(r"z", "nozsplit") == (["no", "split"],) # Split by 'z'
def test_regex_split_list():
node = RegexSplitList()
pattern = r",\s*"
string = "one, two, three"
assert node.split(pattern, string) == (["one", "two", "three"],)
assert node.split(r"\s+", "split several words") == (["split", "several", "words"],) # Whitespace split
assert node.split(r"z", "nozsplit") == (["no", "split"],) # Split by 'z'
def test_regex_sub():
node = RegexSub()
assert node.sub(r"cat", "dog", "cat is here") == ("dog is here",)
+52 -1
View File
@@ -13,9 +13,52 @@ from src.basic_data_handling.set_nodes import (
SetIsDisjoint,
SetContains,
SetLength,
SetToList
SetToList,
SetCreate,
SetToDataList,
SetCreateFromInt,
SetCreateFromString,
SetCreateFromFloat,
SetCreateFromBoolean
)
def test_set_create():
node = SetCreate()
# Testing with kwargs to simulate dynamic inputs
assert node.create_set(item_0=1, item_1=2, item_2=3) == ({1, 2, 3},)
assert node.create_set(item_0="a", item_1="b") == ({"a", "b"},)
assert node.create_set() == (set(),) # Empty set with no arguments
def test_set_create_from_int():
node = SetCreateFromInt()
assert node.create_set(item_0=1, item_1=2, item_2=3) == ({1, 2, 3},)
assert node.create_set(item_0=5) == ({5},) # Single item set
assert node.create_set(item_0=1, item_1=1) == ({1},) # Duplicate items become single item
def test_set_create_from_string():
node = SetCreateFromString()
# Note: Mocking the string function behavior as it's not defined in the file
# This simulates what would happen assuming string() acts like str()
node.create_set = lambda **kwargs: (set([str(value) for value in kwargs.values()]),)
assert node.create_set(item_0="apple", item_1="banana") == ({"apple", "banana"},)
assert node.create_set(item_0="apple", item_1="apple") == ({"apple"},) # Duplicate strings
def test_set_create_from_float():
node = SetCreateFromFloat()
assert node.create_set(item_0=1.5, item_1=2.5) == ({1.5, 2.5},)
assert node.create_set(item_0=3.14) == ({3.14},) # Single item set
assert node.create_set(item_0=1.0, item_1=1.0) == ({1.0},) # Duplicate items
def test_set_create_from_boolean():
node = SetCreateFromBoolean()
assert node.create_set(item_0=True, item_1=False) == ({True, False},)
assert node.create_set(item_0=True, item_1=True) == ({True},) # Duplicate booleans
def test_set_add():
node = SetAdd()
assert node.add({1, 2}, 3) == ({1, 2, 3},)
@@ -108,3 +151,11 @@ def test_set_to_list():
result = node.convert({1, 2, 3})
assert isinstance(result, tuple)
assert sorted(result[0]) == [1, 2, 3] # Validate conversion to list
def test_set_to_data_list():
node = SetToDataList()
result = node.convert({1, 2, 3})
assert isinstance(result, tuple)
assert isinstance(result[0], list)
assert sorted(result[0]) == [1, 2, 3] # Validate conversion to data list
+293
View File
@@ -0,0 +1,293 @@
/**
* ComfyUI basic data handling.
* Copyright (C) 2025 StableLlama
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Purpose: Manage dynamic input nodes in ComfyUI.
* Inspired by cozy_ex_dynamic
*/
import { app } from "../../../scripts/app.js";
const TypeSlot = {
Input: 1,
Output: 2
};
const TypeSlotEvent = {
Connect: true,
Disconnect: false
};
app.registerExtension({
name: 'Basic data handling: dynamic input',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Filter: Only process nodes with class names starting with "Basic data handling:"
if (!nodeType.comfyClass.startsWith('Basic data handling:')) {
return;
}
const combinedInputData = {
...nodeData?.input?.required ?? {},
...nodeData?.input?.optional ?? {}
};
const dynamicInputs = [];
for (const name in combinedInputData) {
const dynamic = combinedInputData[name][1]?._dynamic;
if (dynamic) {
let matcher;
switch (dynamic) {
case 'number':
matcher = new RegExp(`^(${name.replace(/\d*$/, '')})(\\d+)$`);
break;
case 'letter':
matcher = new RegExp(`^()([a-zA-Z])$`);
break;
default:
continue;
}
const baseName = name.match(matcher)?.[1] ?? name;
const dynamicType = combinedInputData[name][0];
dynamicInputs.push({name, baseName, matcher, dynamic, dynamicType});
}
}
if (dynamicInputs.length === 0) {
return;
}
/**
* Utility: Check if an input is dynamic.
* @param {string} inputName - Name of the input to check.
*/
const isDynamicInput = (inputName) =>
dynamicInputs.some((di) => di.matcher.test(inputName));
/**
* Utility: Update inputs' slot indices after reordering.
* @param {Array} inputs - List of inputs for the node.
* @param {Object} graph - Graph containing the node.
*/
const updateSlotIndices = (inputs, graph) => {
inputs.forEach((input, index) => {
input.slot_index = index;
if (input.link !== null) {
const link = graph._links.get(input.link);
if (link)
link.target_slot = index;
else
console.error(`Input ${index} has an invalid link.`);
}
});
};
// Override onConfigure: Ensure dynamic inputs are continuously ordered
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function() {
const result = onConfigure?.apply(this, arguments);
let lastDynamicIdx = -1
for (let idx = 0; idx < this.inputs.length; idx++) {
const input = this.inputs[idx];
const isDynamic = isDynamicInput(input.name);
if (isDynamic) {
if (lastDynamicIdx < 0 || lastDynamicIdx === idx - 1) {
lastDynamicIdx = idx;
} else {
// non-continuous dynamic inputs -> move up
for (let i = idx-1; i > lastDynamicIdx; i--) {
this.swapInputs(i, i + 1);
}
lastDynamicIdx++;
}
}
}
return result;
}
// Add helper method to insert input at a specific position
nodeType.prototype.addInputAtPosition = function (name, type, position) {
console.warn("Adding input at position:", name, type, position);
this.addInput(name, type); // Add new input
const newInput = this.inputs.pop(); // Fetch the newly added input (last item)
this.inputs.splice(position, 0, newInput); // Place it at the desired position
updateSlotIndices(this.inputs, this.graph); // Update indices
return newInput;
};
// flag to prevent loops. It is ok to be "global" as the code is not
// running in parallel.
let isProcessingConnection = false;
// Override onConnectionsChange: Handle connections for dynamic inputs
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (type, slotIndex, isConnected, link, ioSlot) {
const result = onConnectionsChange?.apply(this, arguments);
if (type !== TypeSlot.Input || isProcessingConnection) {
return result;
}
isProcessingConnection = true;
try {
const baseName = dynamicInputs[0].baseName;
//const dynamicType = nodeData.input.required[dynamicInputs[0].name][0];
const dynamicType = dynamicInputs[0].dynamicType;
// Get dynamic input slots
const dynamicSlots = this.inputs
.map((input, idx) => ({
index: idx,
name: input.name,
connected: input.link !== null,
isDynamic: isDynamicInput(input.name),
}))
.filter((input) => input.isDynamic);
// Handle connection event
if (isConnected === TypeSlotEvent.Connect) {
const hasEmptyDynamic = dynamicSlots.some(di => !di.connected);
if (!hasEmptyDynamic) {
// No empty slot - add a new one after the last dynamic input
const lastDynamicIdx = Math.max(...dynamicSlots.map((slot) => slot.index), -1);
const insertPosition = lastDynamicIdx + 1;
let inputInRange = true;
let newName;
if (dynamicInputs[0].dynamic === 'letter') {
if (dynamicSlots.length > 26) {
inputInRange = false;
}
// For letter type, use the next letter in sequence
newName = String.fromCharCode(97 + dynamicSlots.length); // 97 is ASCII for 'a'
} else {
// For number type, use baseName + index as before
newName = `${baseName}${dynamicSlots.length}`;
}
if (inputInRange) {
// Insert the new empty input at the correct position
this.addInputAtPosition(newName, dynamicType, insertPosition);
// Renumber inputs after addition
this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic);
}
}
} else if (isConnected === TypeSlotEvent.Disconnect) {
let hasEmptyDynamic = false;
for (let idx = 0; idx < this.inputs.length; idx++) {
const input = this.inputs[idx];
const isDynamic = isDynamicInput(input.name);
if (hasEmptyDynamic && isDynamic) {
if (input.link === null) {
// last input is empty and this input is empty
this.removeInput(idx);
// continue with this ixd as it is now pointing to
// a new input
idx--;
continue;
}
// this input is dynamic and connected
this.swapInputs(idx, idx - 1);
continue;
}
if (isDynamic && input.link === null) {
hasEmptyDynamic = true;
}
}
this.renumberDynamicInputs(baseName, dynamicInputs, dynamicInputs[0].dynamic);
}
this.setDirtyCanvas(true, true);
} catch (e) {
console.error(e);
debugger;
alert(e);
} finally {
isProcessingConnection = false;
}
return result;
};
// Method to swap two inputs in the "this.inputs" array by their indices
nodeType.prototype.swapInputs = function(indexA, indexB) {
// Validate indices
if (
indexA < 0 || indexB < 0 ||
indexA >= this.inputs.length ||
indexB >= this.inputs.length ||
indexA === indexB
) {
console.warn("Invalid input indices for swapping:", indexA, indexB);
return;
}
// Swap the inputs in the array
[this.inputs[indexA], this.inputs[indexB]] = [this.inputs[indexB], this.inputs[indexA]];
updateSlotIndices(this.inputs, this.graph); // Refresh indices
// Redraw the node to ensure the graph updates properly
// -> not needed as the calling method must do it!
this.setDirtyCanvas(true, true);
};
// Add method to safely renumber dynamic inputs without breaking connections
nodeType.prototype.renumberDynamicInputs = function(baseName, dynamicInputs, dynamic) {
// Get current dynamic inputs info
const dynamicInputInfo = [];
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
const isDynamic = isDynamicInput(input.name);
if (isDynamic) {
dynamicInputInfo.push({
index: i,
name: input.name,
connected: input.link !== null
});
}
}
// Sort connected first, then by index
dynamicInputInfo.sort((a, b) => {
if (a.connected !== b.connected) {
return b.connected ? 1 : -1; // Connected first
}
return a.index - b.index; // Keep order for same connection status
});
// Just rename the inputs in place - don't remove/add to keep connections intact
for (let i = 0; i < dynamicInputInfo.length; i++) {
const info = dynamicInputInfo[i];
const newName = dynamic === "number" ? `${baseName}${i}` : String.fromCharCode(97 + i); // 97 is ASCII for 'a'
if (this.inputs[info.index].name !== newName) {
this.inputs[info.index].name = newName;
this.inputs[info.index].localized_name = newName;
}
}
};
}
});