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