update code formatting (ruff)

This commit is contained in:
StableLlama
2026-09-11 11:28:46 +02:00
parent de5b1350d9
commit f802173782
37 changed files with 1805 additions and 655 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.9
# Ruff version (keep in sync with the 'ruff' dev dependency / venv).
rev: v0.11.8
hooks:
# Run the linter.
- id: ruff
+19 -4
View File
@@ -1,7 +1,22 @@
from . import (boolean_nodes, casting_nodes, comparison_nodes, control_flow_nodes,
data_list_nodes, dict_nodes, float_nodes, int_nodes, list_nodes,
math_nodes, math_formula_node, path_nodes, regex_nodes, set_nodes,
string_nodes, tensor_nodes, time_nodes)
from . import (
boolean_nodes,
casting_nodes,
comparison_nodes,
control_flow_nodes,
data_list_nodes,
dict_nodes,
float_nodes,
int_nodes,
list_nodes,
math_nodes,
math_formula_node,
path_nodes,
regex_nodes,
set_nodes,
string_nodes,
tensor_nodes,
time_nodes,
)
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(boolean_nodes.NODE_CLASS_MAPPINGS)
+3 -4
View File
@@ -17,15 +17,14 @@ class ContainsDynamicDict(dict):
def __contains__(self, key):
# Check if key matches a dynamically handled prefix or exists normally
return (
any(key.startswith(prefix) and key[len(prefix):].isdigit() for prefix in self._dynamic_prefixes)
or super().__contains__(key)
return any(key.startswith(prefix) and key[len(prefix) :].isdigit() for prefix in self._dynamic_prefixes) or super().__contains__(
key
)
def __getitem__(self, key):
# Dynamically return the value for keys matching a `prefix<number>` pattern
for prefix, value in self._dynamic_prefixes.items():
if key.startswith(prefix) and key[len(prefix):].isdigit():
if key.startswith(prefix) and key[len(prefix) :].isdigit():
return value
# Fallback to normal dictionary behavior for other keys
return super().__getitem__(key)
+35 -6
View File
@@ -4,6 +4,7 @@ from typing import Any
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -11,6 +12,7 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
from ._dynamic_input import ContainsDynamicDict
@@ -23,6 +25,7 @@ class BooleanAnd(ComfyNodeABC):
Outputs True only when both inputs are True. This matches the behaviour of the
Python ``and`` operator applied to the two operands.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -49,6 +52,7 @@ class BooleanNand(ComfyNodeABC):
Outputs False only when both inputs are True; it is the negation of AND.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -75,6 +79,7 @@ class BooleanNor(ComfyNodeABC):
Outputs True only when both inputs are False; it is the negation of OR.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -101,6 +106,7 @@ class BooleanNot(ComfyNodeABC):
Outputs True when the input is False and False when the input is True.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -127,6 +133,7 @@ class BooleanOr(ComfyNodeABC):
Outputs True when at least one input is True. This matches the behaviour of the
Python ``or`` operator applied to the two operands.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -153,6 +160,7 @@ class BooleanXor(ComfyNodeABC):
Outputs True when the inputs differ from one another and False when they are equal.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -181,15 +189,25 @@ class GenericOr(ComfyNodeABC):
Python truthiness is used, values such as ``0``, ``""``, ``[]``, ``{}`` and ``None``
count as False. When *invert* is enabled the result is negated (NOR).
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"invert": (IO.BOOLEAN, {"default": False, "tooltip": "When enabled, negates the result (turns OR into NOR)."}),
},
"optional": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the values to combine. Connect more values to add operands."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.ANY,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the values to combine. Connect more values to add operands.",
},
),
}
),
}
RETURN_TYPES = (IO.BOOLEAN,)
@@ -211,15 +229,26 @@ class GenericAnd(ComfyNodeABC):
Python truthiness is used, values such as ``0``, ``""``, ``[]``, ``{}`` and ``None``
count as False. When *invert* is enabled the result is negated (NAND).
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"invert": (IO.BOOLEAN, {"default": False, "tooltip": "When enabled, negates the result (turns AND into NAND)."}),
},
"optional": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING", "default": "True", "tooltip": "One of the values to combine. Connect more values to add operands."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.ANY,
{
"_dynamic": "number",
"widgetType": "STRING",
"default": "True",
"tooltip": "One of the values to combine. Connect more values to add operands.",
},
),
}
),
}
RETURN_TYPES = (IO.BOOLEAN,)
+24 -36
View File
@@ -4,6 +4,7 @@ from inspect import cleandoc
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -11,8 +12,10 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
class CastToBoolean(ComfyNodeABC):
"""
Converts any value to a BOOLEAN using Python truthiness.
@@ -20,13 +23,10 @@ class CastToBoolean(ComfyNodeABC):
Truthy values (non-zero numbers, non-empty strings/lists/dicts/sets) become True;
falsy values (``0``, ``""``, empty containers, ``None``) become False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": (IO.ANY, {"tooltip": "The value to convert to a BOOLEAN."})
}
}
return {"required": {"input": (IO.ANY, {"tooltip": "The value to convert to a BOOLEAN."})}}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("boolean",)
@@ -47,13 +47,10 @@ class CastToDict(ComfyNodeABC):
example a LIST of two-element sequences such as ``[["a", 1], ["b", 2]]``).
Raises a ValueError for any other kind of input.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": (IO.ANY, {"tooltip": "The value to convert to a DICT."})
}
}
return {"required": {"input": (IO.ANY, {"tooltip": "The value to convert to a DICT."})}}
RETURN_TYPES = ("DICT",)
RETURN_NAMES = ("dict",)
@@ -76,13 +73,10 @@ class CastToFloat(ComfyNodeABC):
Accepts INT, FLOAT and numeric strings such as ``"3.14"``. Values that cannot be
parsed as a number raise a ValueError.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": (IO.ANY, {"tooltip": "The value to convert to a FLOAT."})
}
}
return {"required": {"input": (IO.ANY, {"tooltip": "The value to convert to a FLOAT."})}}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("float",)
@@ -105,13 +99,10 @@ class CastToInt(ComfyNodeABC):
Accepts INT, FLOAT (fractional part is dropped, like ``int()``) and numeric strings
such as ``"42"``. Values that cannot be converted raise a ValueError.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": (IO.ANY, {"tooltip": "The value to convert to an INT."})
}
}
return {"required": {"input": (IO.ANY, {"tooltip": "The value to convert to an INT."})}}
RETURN_TYPES = (IO.INT,)
RETURN_NAMES = ("int",)
@@ -134,13 +125,10 @@ class CastToList(ComfyNodeABC):
Values that are already a list (including ComfyUI data lists) are returned as-is;
any other single value is wrapped into a one-element list.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": (IO.ANY, {"tooltip": "The value to convert to a LIST."})
}
}
return {"required": {"input": (IO.ANY, {"tooltip": "The value to convert to a LIST."})}}
RETURN_TYPES = ("LIST",)
RETURN_NAMES = ("list",)
@@ -162,13 +150,10 @@ class CastToSet(ComfyNodeABC):
Sets are returned unchanged; a list or ComfyUI data list becomes the set of its
items (duplicates removed); any other single value is wrapped into a one-element set.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": (IO.ANY, {"tooltip": "The value to convert to a SET."})
}
}
return {"required": {"input": (IO.ANY, {"tooltip": "The value to convert to a SET."})}}
RETURN_TYPES = ("SET",)
RETURN_NAMES = ("set",)
@@ -180,7 +165,13 @@ class CastToSet(ComfyNodeABC):
def convert_to_set(self, input: Any) -> tuple[set]:
if isinstance(input, set):
return (input,)
return ({input,} if not isinstance(input, list) else set(input),)
return (
{
input,
}
if not isinstance(input, list)
else set(input),
)
class CastToString(ComfyNodeABC):
@@ -190,13 +181,10 @@ class CastToString(ComfyNodeABC):
Numbers, booleans, lists, dicts, sets and other values are rendered with ``str()``,
matching Python's default formatting.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": (IO.ANY, {"tooltip": "The value to convert to a STRING."})
}
}
return {"required": {"input": (IO.ANY, {"tooltip": "The value to convert to a STRING."})}}
RETURN_TYPES = (IO.STRING,)
RETURN_NAMES = ("string",)
+25 -5
View File
@@ -4,6 +4,7 @@ from inspect import cleandoc
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -11,8 +12,10 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
class Equal(ComfyNodeABC):
"""
Checks if two values are equal.
@@ -21,6 +24,7 @@ class Equal(ComfyNodeABC):
and False otherwise. For complex objects (lists, dicts, sets), structural
equality is tested.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -49,6 +53,7 @@ class NotEqual(ComfyNodeABC):
and False otherwise. For complex objects (lists, dicts, sets), structural
inequality is tested.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -80,6 +85,7 @@ class LessThan(ComfyNodeABC):
This node takes two numerical inputs and returns True if the first value
is strictly less than the second value, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -107,6 +113,7 @@ class LessThanOrEqual(ComfyNodeABC):
This node takes two numerical inputs and returns True if the first value
is less than or equal to the second value, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -134,6 +141,7 @@ class GreaterThan(ComfyNodeABC):
This node takes two numerical inputs and returns True if the first value
is strictly greater than the second value, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -161,6 +169,7 @@ class GreaterThanOrEqual(ComfyNodeABC):
This node takes two numerical inputs and returns True if the first value
is greater than or equal to the second value, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -188,6 +197,7 @@ class IsNull(ComfyNodeABC):
This node takes any input value and returns True if the value is None
(Python null), and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -215,6 +225,7 @@ class NumberInRange(ComfyNodeABC):
number lies within the range. The ``include_min`` and ``include_max`` options control
whether the boundaries themselves count as being inside the range.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -226,7 +237,7 @@ class NumberInRange(ComfyNodeABC):
"optional": {
"include_min": (IO.BOOLEAN, {"default": "True", "tooltip": "Treat the lower bound as inside the range (>=)."}),
"include_max": (IO.BOOLEAN, {"default": "True", "tooltip": "Treat the upper bound as inside the range (<=)."}),
}
},
}
RETURN_TYPES = (IO.BOOLEAN,)
@@ -236,8 +247,9 @@ class NumberInRange(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "check_range"
def check_range(self, value: float, min_value: float, max_value: float,
include_min: str = "True", include_max: str = "True") -> tuple[bool]:
def check_range(
self, value: float, min_value: float, max_value: float, include_min: str = "True", include_max: str = "True"
) -> tuple[bool]:
min_check = value >= min_value if include_min == "True" else value > min_value
max_check = value <= max_value if include_max == "True" else value < max_value
@@ -252,12 +264,16 @@ class CompareLength(ComfyNodeABC):
selected *operator*. Returns the boolean result and, as a convenience, the
actual measured length.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"container": (IO.ANY, {"tooltip": "The object whose length is measured (string, list, dict, set, ...)."}),
"operator": (["==", "!=", ">", "<", ">=", "<="], {"default": "==", "tooltip": "Comparison operator applied between the length and the given value."}),
"operator": (
["==", "!=", ">", "<", ">=", "<="],
{"default": "==", "tooltip": "Comparison operator applied between the length and the given value."},
),
"length": (IO.INT, {"default": 0, "min": 0, "tooltip": "The value the measured length is compared against."}),
}
}
@@ -300,13 +316,17 @@ class StringComparison(ComfyNodeABC):
comparison holds. Comparisons are lexicographic (dictionary order); enable
*case_sensitive* to treat uppercase and lowercase letters as distinct.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"string1": ("STRING", {"default": "", "tooltip": "First string to compare."}),
"string2": ("STRING", {"default": "", "tooltip": "Second string to compare."}),
"operator": (["==", "!=", ">", "<", ">=", "<="], {"default": "==", "tooltip": "Comparison operator applied between the two strings."}),
"operator": (
["==", "!=", ">", "<", ">=", "<="],
{"default": "==", "tooltip": "Comparison operator applied between the two strings."},
),
"case_sensitive": (IO.BOOLEAN, {"default": True, "tooltip": "When enabled, letter case is respected (e.g. 'A' != 'a')."}),
}
}
+83 -23
View File
@@ -5,6 +5,7 @@ try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
from comfy_execution.graph import ExecutionBlocker
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -12,6 +13,7 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
class ExecutionBlocker:
@@ -21,9 +23,11 @@ except:
the message, but its class name shows up in the default ``repr`` exactly
like the real one, so callers/tests can still detect a blocker.
"""
def __init__(self, message):
self.message = message
from ._dynamic_input import ContainsDynamicDict
@@ -35,6 +39,7 @@ class IfElse(ComfyNodeABC):
evaluates to True, the first value is returned; otherwise, the second value
is returned. This allows conditional data flow in ComfyUI workflows.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -74,6 +79,7 @@ class IfElifElse(ComfyNodeABC):
When none is true, the value of the else is returned.
This allows conditional data flow in ComfyUI workflows.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -81,11 +87,30 @@ class IfElifElse(ComfyNodeABC):
"if": (IO.BOOLEAN, {"forceInput": True, "tooltip": "Main condition; when True the 'then' value is returned."}),
"then": (IO.ANY, {"lazy": True, "tooltip": "Value returned when the 'if' condition is True."}),
},
"optional": ContainsDynamicDict({
"elif_0": (IO.BOOLEAN, {"forceInput": True, "lazy": True, "_dynamic": "number", "_dynamicGroup": 0, "tooltip": "Optional else-if condition. Connect more to chain additional branches."}),
"then_0": (IO.ANY, {"lazy": True, "_dynamic": "number", "_dynamicGroup": 0, "tooltip": "Value returned when the matching elif_<n> condition is True."}),
"else": (IO.ANY, {"lazy": True, "tooltip": "Value returned when no condition is True."}),
})
"optional": ContainsDynamicDict(
{
"elif_0": (
IO.BOOLEAN,
{
"forceInput": True,
"lazy": True,
"_dynamic": "number",
"_dynamicGroup": 0,
"tooltip": "Optional else-if condition. Connect more to chain additional branches.",
},
),
"then_0": (
IO.ANY,
{
"lazy": True,
"_dynamic": "number",
"_dynamicGroup": 0,
"tooltip": "Value returned when the matching elif_<n> condition is True.",
},
),
"else": (IO.ANY, {"lazy": True, "tooltip": "Value returned when no condition is True."}),
}
),
}
RETURN_TYPES = (IO.ANY,)
@@ -167,13 +192,22 @@ class SwitchCase(ComfyNodeABC):
@classmethod
def INPUT_TYPES(cls):
return {
"required": ContainsDynamicDict({
"select": (IO.INT, {"default": 0, "min": 0, "tooltip": "Zero-based index of the case to select."}),
"case_0": (IO.ANY, {"lazy": True, "_dynamic": "number", "tooltip": "Value returned when its index is selected. Connect more values to add cases."}),
}),
"required": ContainsDynamicDict(
{
"select": (IO.INT, {"default": 0, "min": 0, "tooltip": "Zero-based index of the case to select."}),
"case_0": (
IO.ANY,
{
"lazy": True,
"_dynamic": "number",
"tooltip": "Value returned when its index is selected. Connect more values to add cases.",
},
),
}
),
"optional": {
"default": (IO.ANY, {"lazy": True, "tooltip": "Value returned when the selected index is out of range."}),
}
},
}
RETURN_TYPES = (IO.ANY,)
@@ -241,6 +275,7 @@ class ContinueIfNotEmpty(ComfyNodeABC):
By default the block is silent; set ``message`` to show a dialog (like
"continue flow") when the execution is blocked.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -248,8 +283,11 @@ class ContinueIfNotEmpty(ComfyNodeABC):
"value": (IO.ANY, {"tooltip": "The value to guard; execution is blocked when the value is empty."}),
},
"optional": {
"message": (IO.STRING, {"default": "", "tooltip": "Optional message shown when the flow is blocked. Leave empty for silent operation."}),
}
"message": (
IO.STRING,
{"default": "", "tooltip": "Optional message shown when the flow is blocked. Leave empty for silent operation."},
),
},
}
RETURN_TYPES = (IO.ANY,)
@@ -270,9 +308,9 @@ class ContinueIfNotEmpty(ComfyNodeABC):
value = []
msg = message[0] if isinstance(message, (list, tuple)) and message else (message or "")
if isinstance(value, list):
empty = (len(value) == 0)
empty = len(value) == 0
else:
empty = (not value)
empty = not value
if empty:
return (ExecutionBlocker(msg if msg else None),)
# OUTPUT_IS_LIST expects a list, so wrap a single non-list value.
@@ -292,16 +330,26 @@ class ContinueFlow(ComfyNodeABC):
When a `message` is provided ComfyUI will display it in a dialog.
Leave it empty for silent operation.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.ANY, {"tooltip": "The value to pass through when the flow is enabled."}),
"select": (IO.BOOLEAN, {"default": True, "tooltip": "When True the value passes through; when False execution is blocked."}),
"select": (
IO.BOOLEAN,
{"default": True, "tooltip": "When True the value passes through; when False execution is blocked."},
),
},
"optional": {
"message": (IO.STRING, {"default": "", "tooltip": "Optional message shown in a dialog when the flow is blocked. Leave empty for silent operation."}),
}
"message": (
IO.STRING,
{
"default": "",
"tooltip": "Optional message shown in a dialog when the flow is blocked. Leave empty for silent operation.",
},
),
},
}
RETURN_TYPES = (IO.ANY,)
@@ -327,23 +375,30 @@ class FlowSelect(ComfyNodeABC):
Note: for dynamic switching in a Data Flow you might want to use
"filter select" instead.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.ANY, {"tooltip": "The value to route to one of the two outputs."}),
"select": (IO.BOOLEAN, {"tooltip": "When True the value is emitted on the 'true' output, otherwise on the 'false' output."}),
"select": (
IO.BOOLEAN,
{"tooltip": "When True the value is emitted on the 'true' output, otherwise on the 'false' output."},
),
}
}
RETURN_TYPES = (IO.ANY, IO.ANY)
RETURN_NAMES = ("true", "false")
OUTPUT_TOOLTIPS = ("Receives the value when select is True (otherwise blocked).", "Receives the value when select is False (otherwise blocked).")
OUTPUT_TOOLTIPS = (
"Receives the value when select is True (otherwise blocked).",
"Receives the value when select is False (otherwise blocked).",
)
CATEGORY = "Basic/flow control"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "select"
def select(self, value, select = True) -> tuple[Any, Any]:
def select(self, value, select=True) -> tuple[Any, Any]:
if select:
return value, ExecutionBlocker(None)
else:
@@ -378,7 +433,7 @@ class ForceCalculation(ComfyNodeABC):
@classmethod
def IS_CHANGED(s, value: Any):
return float("NaN") # not equal to anything -> trigger recalculation
return float("NaN") # not equal to anything -> trigger recalculation
def execute(self, value: Any) -> tuple[Any, int]:
return (value,)
@@ -396,12 +451,16 @@ class ExecutionOrder(ComfyNodeABC):
This node also passes through any input connected to "any node output" as
its second output.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"E/O": ("E/O", {"tooltip": "Chain these sockets together to force execution order."}),
"any node output": (IO.ANY, {"tooltip": "Connect any output of the nodes whose execution order you want to force; it is passed through."}),
"any node output": (
IO.ANY,
{"tooltip": "Connect any output of the nodes whose execution order you want to force; it is passed through."},
),
}
}
@@ -414,7 +473,7 @@ class ExecutionOrder(ComfyNodeABC):
FUNCTION = "execute"
def execute(self, **kwargs: list[Any]) -> tuple[None, Any]:
any_node_output = kwargs.get('any node output', [])
any_node_output = kwargs.get("any node output", [])
return (None, any_node_output)
@@ -425,6 +484,7 @@ class IsConnected(ComfyNodeABC):
This node returns True if the 'input' is connected to another node's output,
and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
+210 -93
View File
@@ -4,6 +4,7 @@ from inspect import cleandoc
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -11,11 +12,12 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
from ._dynamic_input import ContainsDynamicDict
INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes
INT_MAX = 2**15 - 1 # the computer can do more but be nice to the eyes
class DataListCreate(ComfyNodeABC):
@@ -25,12 +27,22 @@ class DataListCreate(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the items of the Data List. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.ANY,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the items of the Data List. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = (IO.ANY,)
@@ -55,12 +67,22 @@ class DataListListCreate(ComfyNodeABC):
Each input can be a list, so you'll get a list of lists.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the items of the Data List; each may itself be a list, producing a list of lists."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.ANY,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the items of the Data List; each may itself be a list, producing a list of lists.",
},
),
}
)
}
RETURN_TYPES = (IO.ANY,)
@@ -84,12 +106,22 @@ class DataListCreateFromBoolean(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the boolean items of the Data List. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.BOOLEAN,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the boolean items of the Data List. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = (IO.BOOLEAN,)
@@ -112,12 +144,22 @@ class DataListCreateFromFloat(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the float items of the Data List. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.FLOAT,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the float items of the Data List. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = (IO.FLOAT,)
@@ -140,12 +182,22 @@ class DataListCreateFromInt(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the integer items of the Data List. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.INT,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the integer items of the Data List. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = (IO.INT,)
@@ -168,12 +220,22 @@ class DataListCreateFromString(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.STRING, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the string items of the Data List. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.STRING,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the string items of the Data List. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = (IO.STRING,)
@@ -212,7 +274,7 @@ class DataListAll(ComfyNodeABC):
INPUT_IS_LIST = True
def check_all(self, **kwargs: list[Any]) -> tuple[bool]:
return (all(kwargs.get('list') or []),)
return (all(kwargs.get("list") or []),)
class DataListAny(ComfyNodeABC):
@@ -238,7 +300,7 @@ class DataListAny(ComfyNodeABC):
INPUT_IS_LIST = True
def check_any(self, **kwargs: list[Any]) -> tuple[bool]:
return (any(kwargs.get('list') or []),)
return (any(kwargs.get("list") or []),)
class DataListAppend(ComfyNodeABC):
@@ -248,12 +310,13 @@ class DataListAppend(ComfyNodeABC):
This node takes a list and any item as inputs, then returns the modified
list with the new item appended.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"list":(IO.ANY,{"tooltip": "The Data List to append to."}),
"item":(IO.ANY,{"tooltip": "The item to append at the end."}),
"list": (IO.ANY, {"tooltip": "The Data List to append to."}),
"item": (IO.ANY, {"tooltip": "The item to append at the end."}),
}
}
@@ -267,8 +330,8 @@ class DataListAppend(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def append(self, **kwargs: list[Any]) -> tuple[list[Any]]:
result = (kwargs.get('list') or []).copy()
item = kwargs.get('item') or []
result = (kwargs.get("list") or []).copy()
item = kwargs.get("item") or []
if len(item) > 0:
result.append(item[0])
return (result,)
@@ -281,6 +344,7 @@ class DataListContains(ComfyNodeABC):
This node takes a list and a value as inputs, then returns True if the value
is present in the list, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -299,10 +363,10 @@ class DataListContains(ComfyNodeABC):
INPUT_IS_LIST = True
def contains(self, **kwargs: list[Any]) -> tuple[bool]:
value = kwargs.get('value') or []
value = kwargs.get("value") or []
if len(value) == 0:
return (False,)
return (value[0] in (kwargs.get('list') or []),)
return (value[0] in (kwargs.get("list") or []),)
class DataListCount(ComfyNodeABC):
@@ -312,6 +376,7 @@ class DataListCount(ComfyNodeABC):
This node takes a list and a value as inputs, then returns the number of times
the value appears in the list.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -330,8 +395,8 @@ class DataListCount(ComfyNodeABC):
INPUT_IS_LIST = True
def count(self, **kwargs: list[Any]) -> tuple[int]:
value = (kwargs.get('value') or [None])[0]
return ((kwargs.get('list') or []).count(value),)
value = (kwargs.get("value") or [None])[0]
return ((kwargs.get("list") or []).count(value),)
class DataListEnumerate(ComfyNodeABC):
@@ -348,7 +413,7 @@ class DataListEnumerate(ComfyNodeABC):
},
"optional": {
"start": (IO.INT, {"default": 0, "tooltip": "Index assigned to the first element."}),
}
},
}
RETURN_TYPES = (IO.ANY,)
@@ -361,8 +426,8 @@ class DataListEnumerate(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def enumerate_list(self, **kwargs: list[Any]) -> tuple[list]:
input_list = kwargs.get('list') or []
start = (kwargs.get('start') or [0])[0]
input_list = kwargs.get("list") or []
start = (kwargs.get("start") or [0])[0]
return ([list(item) for item in enumerate(input_list, start=start)],)
@@ -373,12 +438,13 @@ class DataListExtend(ComfyNodeABC):
This node takes two lists as input and returns a new list that contains
all elements from both lists.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"list_a":(IO.ANY,{"tooltip": "First Data List (kept as-is)."}),
"list_b":(IO.ANY,{"tooltip": "Second Data List whose elements are appended."}),
"list_a": (IO.ANY, {"tooltip": "First Data List (kept as-is)."}),
"list_b": (IO.ANY, {"tooltip": "Second Data List whose elements are appended."}),
}
}
@@ -392,7 +458,7 @@ class DataListExtend(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def extend(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return ((kwargs.get('list_a') or []) + (kwargs.get('list_b') or []),)
return ((kwargs.get("list_a") or []) + (kwargs.get("list_b") or []),)
class DataListFilter(ComfyNodeABC):
@@ -406,6 +472,7 @@ class DataListFilter(ComfyNodeABC):
If the lists have different lengths, the last element of the shorter list is repeated
till the lengths are matching.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -425,8 +492,8 @@ class DataListFilter(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def filter_data(self, **kwargs: list[Any]) -> tuple[list[Any]]:
values = kwargs.get('value') or []
filters = kwargs.get('filter') or []
values = kwargs.get("value") or []
filters = kwargs.get("filter") or []
# Create a new list with only items where the filter is False
result = [_val for _val, _filter in zip(values, filters) if not _filter]
@@ -445,6 +512,7 @@ class DataListFilterSelect(ComfyNodeABC):
If the lists have different lengths, the last element of the shorter list is repeated
till the lengths are matching.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -461,11 +529,14 @@ class DataListFilterSelect(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "select"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True, True,)
OUTPUT_IS_LIST = (
True,
True,
)
def select(self, **kwargs: list[Any]) -> tuple[list[Any]]:
values = kwargs.get('value') or []
selects = kwargs.get('select') or []
values = kwargs.get("value") or []
selects = kwargs.get("select") or []
# Create a new list with only items where the filter is False
result_true, result_false = [], []
@@ -482,6 +553,7 @@ class DataListFirst(ComfyNodeABC):
This node takes a list as input and returns the first element of the list.
If the list is empty, it returns None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -499,7 +571,7 @@ class DataListFirst(ComfyNodeABC):
INPUT_IS_LIST = True
def get_first_element(self, **kwargs: list[Any]) -> tuple[Any]:
input_list = kwargs.get('list') or []
input_list = kwargs.get("list") or []
return (input_list[0] if input_list else None,)
@@ -511,6 +583,7 @@ class DataListGetItem(ComfyNodeABC):
Negative indices count from the end of the list.
Out of range indices return None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -529,9 +602,9 @@ class DataListGetItem(ComfyNodeABC):
INPUT_IS_LIST = True
def get_item(self, **kwargs: list[Any]) -> tuple[Any]:
index = (kwargs.get('index') or [0])[0]
index = (kwargs.get("index") or [0])[0]
try:
return ((kwargs.get('list') or [])[index],)
return ((kwargs.get("list") or [])[index],)
except IndexError:
return (None,)
@@ -544,6 +617,7 @@ class DataListIndex(ComfyNodeABC):
occurrence of the value. Optional start and end parameters limit the search to a slice
of the list. Returns -1 if the value is not present.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -554,7 +628,7 @@ class DataListIndex(ComfyNodeABC):
"optional": {
"start": (IO.INT, {"default": 0, "tooltip": "Start position of the search slice."}),
"end": (IO.INT, {"default": -1, "tooltip": "End position of the search slice (-1 means the end)."}),
}
},
}
RETURN_TYPES = (IO.INT,)
@@ -566,10 +640,10 @@ class DataListIndex(ComfyNodeABC):
INPUT_IS_LIST = True
def list_index(self, **kwargs: list[Any]) -> tuple[int]:
input_list = kwargs.get('list') or []
value = (kwargs.get('value') or [None])[0]
start = (kwargs.get('start') or [0])[0]
end = (kwargs.get('end') or [-1])[0]
input_list = kwargs.get("list") or []
value = (kwargs.get("value") or [None])[0]
start = (kwargs.get("start") or [0])[0]
end = (kwargs.get("end") or [-1])[0]
if end == -1:
end = len(input_list)
@@ -586,6 +660,7 @@ class DataListInsert(ComfyNodeABC):
This node takes a list, an index, and any item as inputs, then returns a new
list with the item inserted at the specified index.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -606,8 +681,8 @@ class DataListInsert(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def insert(self, **kwargs: list[Any]) -> tuple[list[Any]]:
result = (kwargs.get('list') or []).copy()
result.insert((kwargs.get('index') or [0])[0], (kwargs.get('item') or [None])[0])
result = (kwargs.get("list") or []).copy()
result.insert((kwargs.get("index") or [0])[0], (kwargs.get("item") or [None])[0])
return (result,)
@@ -618,6 +693,7 @@ class DataListLast(ComfyNodeABC):
This node takes a list as input and returns the last element of the list.
If the list is empty, it returns None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -635,7 +711,7 @@ class DataListLast(ComfyNodeABC):
INPUT_IS_LIST = True
def get_last_element(self, **kwargs: list[Any]) -> tuple[Any]:
input_list = kwargs.get('list') or []
input_list = kwargs.get("list") or []
return (input_list[-1] if input_list else None,)
@@ -645,6 +721,7 @@ class DataListLength(ComfyNodeABC):
This node takes a list as input and returns its length as an integer.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -662,7 +739,7 @@ class DataListLength(ComfyNodeABC):
INPUT_IS_LIST = True
def length(self, **kwargs: list[Any]) -> tuple[int]:
return (len(kwargs.get('list') or []),)
return (len(kwargs.get("list") or []),)
class DataListMax(ComfyNodeABC):
@@ -673,6 +750,7 @@ class DataListMax(ComfyNodeABC):
the maximum value. Returns None if the list is empty or if it contains
non-numeric values.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -690,7 +768,7 @@ class DataListMax(ComfyNodeABC):
INPUT_IS_LIST = True
def find_max(self, **kwargs: list[Any]) -> tuple[Any]:
values = kwargs.get('list') or []
values = kwargs.get("list") or []
if not values:
return (None,)
@@ -710,6 +788,7 @@ class DataListMin(ComfyNodeABC):
the minimum value. Returns None if the list is empty or if it contains
non-numeric values.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -727,7 +806,7 @@ class DataListMin(ComfyNodeABC):
INPUT_IS_LIST = True
def find_min(self, **kwargs: list[Any]) -> tuple[Any]:
values = kwargs.get('list') or []
values = kwargs.get("list") or []
if not values:
return (None,)
@@ -748,6 +827,7 @@ class DataListPop(ComfyNodeABC):
removes and returns the last item.
When the list is empty, the item is None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -756,7 +836,7 @@ class DataListPop(ComfyNodeABC):
},
"optional": {
"index": (IO.INT, {"default": -1, "tooltip": "Position of the item to remove (-1 = last item)."}),
}
},
}
RETURN_TYPES = (IO.ANY, IO.ANY)
@@ -769,8 +849,8 @@ class DataListPop(ComfyNodeABC):
OUTPUT_IS_LIST = (True, False)
def pop(self, **kwargs: list[Any]) -> tuple[list[Any], Any]:
result = (kwargs.get('list') or []).copy()
index = (kwargs.get('index') or [-1])[0]
result = (kwargs.get("list") or []).copy()
index = (kwargs.get("index") or [-1])[0]
try:
item = result.pop(index)
return result, item
@@ -786,6 +866,7 @@ class DataListPopRandom(ComfyNodeABC):
and the removed element itself. If the list is empty, it returns None for the element.
An optional seed can be provided for reproducible selection.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -793,7 +874,16 @@ class DataListPopRandom(ComfyNodeABC):
"list": (IO.ANY, {"tooltip": "The Data List to pop a random element from."}),
},
"optional": {
"seed": (IO.INT, {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "Seed for reproducible selection. Leave empty to pick randomly each run."}),
"seed": (
IO.INT,
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"control_after_generate": True,
"tooltip": "Seed for reproducible selection. Leave empty to pick randomly each run.",
},
),
},
}
@@ -820,8 +910,9 @@ class DataListPopRandom(ComfyNodeABC):
def pop_random_element(self, **kwargs: list[Any]) -> tuple[list[Any], Any]:
import random
input_list = (kwargs.get('list') or []).copy()
seed_values = kwargs.get('seed')
input_list = (kwargs.get("list") or []).copy()
seed_values = kwargs.get("seed")
seed = seed_values[0] if seed_values is not None else None
rng = random.Random(seed) if seed is not None else random
if input_list:
@@ -837,6 +928,7 @@ class DataListRange(ComfyNodeABC):
This node generates a sequence of numbers similar to Python's range() function.
It takes start, stop, and step parameters to define the sequence.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -846,7 +938,7 @@ class DataListRange(ComfyNodeABC):
},
"optional": {
"step": ("INT", {"default": 1, "tooltip": "Step between numbers; must not be 0."}),
}
},
}
RETURN_TYPES = ("INT",)
@@ -870,6 +962,7 @@ class DataListRemove(ComfyNodeABC):
This node takes a list and a value as inputs, then returns a new list with
the first occurrence of the value removed. Raises a ValueError if the value is not present.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -879,18 +972,27 @@ class DataListRemove(ComfyNodeABC):
}
}
RETURN_TYPES = (IO.ANY, IO.BOOLEAN,)
RETURN_NAMES = ("list", "success",)
RETURN_TYPES = (
IO.ANY,
IO.BOOLEAN,
)
RETURN_NAMES = (
"list",
"success",
)
OUTPUT_TOOLTIPS = ("The list with the first occurrence removed.", "True when the value was present and removed.")
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "remove"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True, False,)
OUTPUT_IS_LIST = (
True,
False,
)
def remove(self, **kwargs: list[Any]) -> tuple[list[Any], bool]:
result = (kwargs.get('list') or []).copy()
value = kwargs.get('value') or []
result = (kwargs.get("list") or []).copy()
value = kwargs.get("value") or []
try:
result.remove(value[0])
return result, True
@@ -904,6 +1006,7 @@ class DataListReverse(ComfyNodeABC):
This node takes a list as input and returns a new list with the items in reversed order.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -922,7 +1025,7 @@ class DataListReverse(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def reverse(self, **kwargs: list[Any]) -> tuple[list[Any]]:
result = (kwargs.get('list') or []).copy()
result = (kwargs.get("list") or []).copy()
result.reverse()
return (result,)
@@ -934,6 +1037,7 @@ class DataListSetItem(ComfyNodeABC):
This node takes a list, an index, and a value, then returns a new list with
the item at the specified index replaced by the value.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -954,9 +1058,9 @@ class DataListSetItem(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def set_item(self, **kwargs: list[Any]) -> tuple[Any]:
input_list = kwargs.get('list') or []
index = (kwargs.get('index') or [0])[0]
value = (kwargs.get('value') or [None])[0]
input_list = kwargs.get("list") or []
index = (kwargs.get("index") or [0])[0]
value = (kwargs.get("value") or [None])[0]
try:
result = input_list.copy()
result[index] = value
@@ -971,6 +1075,7 @@ class DataListShuffle(ComfyNodeABC):
This node takes a list and a seed as input and returns a new shuffled list.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -991,8 +1096,9 @@ class DataListShuffle(ComfyNodeABC):
def shuffle_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
import random
input_list = kwargs.get('list') or []
seed = (kwargs.get('seed') or [0])[0]
input_list = kwargs.get("list") or []
seed = (kwargs.get("seed") or [0])[0]
random.seed(seed)
result = input_list.copy()
random.shuffle(result)
@@ -1006,6 +1112,7 @@ class DataListSlice(ComfyNodeABC):
This node takes a list and start/stop/step parameters, and returns a new list
containing the specified slice of the original list.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1016,7 +1123,7 @@ class DataListSlice(ComfyNodeABC):
"start": (IO.INT, {"default": 0, "tooltip": "Start index (inclusive)."}),
"stop": (IO.INT, {"default": INT_MAX, "tooltip": "Stop index (exclusive); INT_MAX means the end."}),
"step": (IO.INT, {"default": 1, "tooltip": "Step between indices."}),
}
},
}
RETURN_TYPES = (IO.ANY,)
@@ -1029,10 +1136,10 @@ class DataListSlice(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def slice(self, **kwargs: list[Any]) -> tuple[list[Any]]:
input_list = kwargs.get('list') or []
start = (kwargs.get('start') or [0])[0]
stop = (kwargs.get('stop') or [INT_MAX])[0]
step = (kwargs.get('step') or [1])[0]
input_list = kwargs.get("list") or []
start = (kwargs.get("start") or [0])[0]
stop = (kwargs.get("stop") or [INT_MAX])[0]
step = (kwargs.get("step") or [1])[0]
return (input_list[start:stop:step],)
@@ -1044,6 +1151,7 @@ class DataListSort(ComfyNodeABC):
This node takes a list as input and returns a new sorted list.
Options include sorting in reverse order and using a key function.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1052,7 +1160,7 @@ class DataListSort(ComfyNodeABC):
},
"optional": {
"reverse": (["False", "True"], {"default": "False", "tooltip": "Sort in descending order when True."}),
}
},
}
RETURN_TYPES = (IO.ANY,)
@@ -1066,9 +1174,9 @@ class DataListSort(ComfyNodeABC):
def sort(self, **kwargs: list[Any]) -> tuple[list[Any]]:
# Convert string to boolean
reverse = (kwargs.get('reverse') or ["False"])[0] == "True"
reverse = (kwargs.get("reverse") or ["False"])[0] == "True"
result = sorted(kwargs.get('list') or [], reverse=reverse)
result = sorted(kwargs.get("list") or [], reverse=reverse)
return (result,)
@@ -1086,11 +1194,17 @@ class DataListSum(ComfyNodeABC):
},
"optional": {
"start": (IO.INT, {"default": 0, "tooltip": "Initial value added to the sum."}),
}
},
}
RETURN_TYPES = (IO.INT, IO.FLOAT,)
RETURN_NAMES = ("int_sum", "float_sum",)
RETURN_TYPES = (
IO.INT,
IO.FLOAT,
)
RETURN_NAMES = (
"int_sum",
"float_sum",
)
OUTPUT_TOOLTIPS = ("The total as an integer.", "The total as a float.")
CATEGORY = "Basic/Data List"
DESCRIPTION = cleandoc(__doc__ or "")
@@ -1098,8 +1212,8 @@ class DataListSum(ComfyNodeABC):
INPUT_IS_LIST = True
def sum_list(self, **kwargs: list[Any]) -> tuple[int, float]:
input_list = kwargs.get('list') or []
start = (kwargs.get('start') or [0])[0]
input_list = kwargs.get("list") or []
start = (kwargs.get("start") or [0])[0]
result = sum(input_list, start)
return int(result), float(result)
@@ -1112,6 +1226,7 @@ class DataListZip(ComfyNodeABC):
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.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1122,7 +1237,7 @@ class DataListZip(ComfyNodeABC):
"optional": {
"list3": (IO.ANY, {"tooltip": "Optional additional Data List."}),
"list4": (IO.ANY, {"tooltip": "Optional additional Data List."}),
}
},
}
RETURN_TYPES = (IO.ANY,)
@@ -1135,13 +1250,13 @@ class DataListZip(ComfyNodeABC):
OUTPUT_IS_LIST = (True,)
def zip_lists(self, **kwargs: list[Any]) -> tuple[list[Any]]:
lists = [(kwargs.get('list1') or []), (kwargs.get('list2') or [])]
lists = [(kwargs.get("list1") or []), (kwargs.get("list2") or [])]
if 'list3' in kwargs:
lists.append(kwargs['list3'] or [])
if "list3" in kwargs:
lists.append(kwargs["list3"] or [])
if 'list4' in kwargs:
lists.append(kwargs['list4'] or [])
if "list4" in kwargs:
lists.append(kwargs["list4"] or [])
# Zip the lists together and convert each tuple to a list
result = [list(item) for item in zip(*lists)]
@@ -1155,6 +1270,7 @@ class DataListToList(ComfyNodeABC):
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
def INPUT_TYPES(cls):
return {
@@ -1172,7 +1288,7 @@ class DataListToList(ComfyNodeABC):
INPUT_IS_LIST = True
def convert(self, **kwargs: list[Any]) -> tuple[list[Any]]:
return (list(kwargs.get('list') or []).copy(),)
return (list(kwargs.get("list") or []).copy(),)
class DataListToSet(ComfyNodeABC):
@@ -1182,6 +1298,7 @@ class DataListToSet(ComfyNodeABC):
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
def INPUT_TYPES(cls):
return {
@@ -1199,7 +1316,7 @@ class DataListToSet(ComfyNodeABC):
INPUT_IS_LIST = True
def convert(self, **kwargs: list[Any]) -> tuple[set[Any]]:
return (set(kwargs.get('list') or []),)
return (set(kwargs.get("list") or []),)
NODE_CLASS_MAPPINGS = {
+204 -68
View File
@@ -1,7 +1,4 @@
from typing import (
Any, Dict, List, Mapping, Tuple, Type, TypeVar,
Union as _U, Optional as _O
)
from typing import Any, Dict, List, Mapping, Tuple, Type, TypeVar, Union as _U, Optional as _O
from inspect import cleandoc
import json
@@ -10,6 +7,7 @@ import random
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -17,22 +15,23 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
from ._dynamic_input import ContainsDynamicDict
T = TypeVar('T') # Any type.
T = TypeVar("T") # Any type.
KT = TypeVar('KT') # Key type.
KT2 = TypeVar('KT2')
KT3 = TypeVar('KT3')
KT4 = TypeVar('KT4')
KT = TypeVar("KT") # Key type.
KT2 = TypeVar("KT2")
KT3 = TypeVar("KT3")
KT4 = TypeVar("KT4")
VT = TypeVar('VT') # Value type.
VT2 = TypeVar('VT2')
VT3 = TypeVar('VT3')
VT4 = TypeVar('VT4')
VT = TypeVar("VT") # Value type.
VT2 = TypeVar("VT2")
VT3 = TypeVar("VT3")
VT4 = TypeVar("VT4")
def _output_dict_preserving_type(result: Dict[KT, VT], in_type: Type[Mapping]) -> _U[Dict[KT, VT], Mapping[KT, VT]]:
@@ -62,13 +61,32 @@ class DictCreate(ComfyNodeABC):
This node creates and returns a new empty dictionary object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Key for a key-value pair. Connect more keys to add more pairs."}),
"value_0": (IO.ANY, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Value paired with the key of the same index."}),
})
"optional": ContainsDynamicDict(
{
"key_0": (
IO.STRING,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Key for a key-value pair. Connect more keys to add more pairs.",
},
),
"value_0": (
IO.ANY,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Value paired with the key of the same index.",
},
),
}
)
}
RETURN_TYPES = ("DICT",)
@@ -95,13 +113,32 @@ class DictCreateFromBoolean(ComfyNodeABC):
This node creates and returns a new empty dictionary object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Key for a key-value pair. Connect more keys to add more pairs."}),
"value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Boolean value paired with the key of the same index."}),
})
"optional": ContainsDynamicDict(
{
"key_0": (
IO.STRING,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Key for a key-value pair. Connect more keys to add more pairs.",
},
),
"value_0": (
IO.BOOLEAN,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Boolean value paired with the key of the same index.",
},
),
}
)
}
RETURN_TYPES = ("DICT",)
@@ -128,13 +165,32 @@ class DictCreateFromFloat(ComfyNodeABC):
This node creates and returns a new empty dictionary object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Key for a key-value pair. Connect more keys to add more pairs."}),
"value_0": (IO.FLOAT, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Float value paired with the key of the same index."}),
})
"optional": ContainsDynamicDict(
{
"key_0": (
IO.STRING,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Key for a key-value pair. Connect more keys to add more pairs.",
},
),
"value_0": (
IO.FLOAT,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Float value paired with the key of the same index.",
},
),
}
)
}
RETURN_TYPES = ("DICT",)
@@ -161,13 +217,32 @@ class DictCreateFromInt(ComfyNodeABC):
This node creates and returns a new empty dictionary object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Key for a key-value pair. Connect more keys to add more pairs."}),
"value_0": (IO.INT, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Integer value paired with the key of the same index."}),
})
"optional": ContainsDynamicDict(
{
"key_0": (
IO.STRING,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Key for a key-value pair. Connect more keys to add more pairs.",
},
),
"value_0": (
IO.INT,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Integer value paired with the key of the same index.",
},
),
}
)
}
RETURN_TYPES = ("DICT",)
@@ -194,13 +269,32 @@ class DictCreateFromString(ComfyNodeABC):
This node creates and returns a new empty dictionary object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": ContainsDynamicDict({
"key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "Key for a key-value pair. Connect more keys to add more pairs."}),
"value_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING", "tooltip": "String value paired with the key of the same index."}),
})
"optional": ContainsDynamicDict(
{
"key_0": (
IO.STRING,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "Key for a key-value pair. Connect more keys to add more pairs.",
},
),
"value_0": (
IO.STRING,
{
"_dynamic": "number",
"_dynamicGroup": 0,
"widgetType": "STRING",
"tooltip": "String value paired with the key of the same index.",
},
),
}
)
}
RETURN_TYPES = ("DICT",)
@@ -227,6 +321,7 @@ class DictCreateFromItemsDataList(ComfyNodeABC):
This node takes a list of key-value pairs (tuples) and builds a dictionary from them.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -246,7 +341,7 @@ class DictCreateFromItemsDataList(ComfyNodeABC):
def create_from_items(self, **kwargs: list[Any]) -> tuple[dict]:
try:
# Check if items are valid (key-value pairs)
items = kwargs.get('item') or []
items = kwargs.get("item") or []
for item in items:
if not isinstance(item, tuple) and len(item) != 2:
raise ValueError("Each item must be a (key, value) pair")
@@ -262,6 +357,7 @@ class DictCreateFromItemsList(ComfyNodeABC):
This node takes a list of key-value pairs (tuples) and builds a dictionary from them.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -298,6 +394,7 @@ class DictCreateFromLists(ComfyNodeABC):
by pairing corresponding elements from each list. If the lists are of different
lengths, only pairs up to the length of the shorter list are used.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -330,6 +427,7 @@ class DictCompare(ComfyNodeABC):
their equality, any keys that exist in only one dictionary, and any keys with
different values.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -341,7 +439,12 @@ class DictCompare(ComfyNodeABC):
RETURN_TYPES = (IO.BOOLEAN, "LIST", "LIST", "LIST")
RETURN_NAMES = ("are_equal", "only_in_dict1", "only_in_dict2", "different_values")
OUTPUT_TOOLTIPS = ("True when the two DICTs are equal.", "Keys present only in the first DICT.", "Keys present only in the second DICT.", "Shared keys whose values differ.")
OUTPUT_TOOLTIPS = (
"True when the two DICTs are equal.",
"Keys present only in the first DICT.",
"Keys present only in the second DICT.",
"Shared keys whose values differ.",
)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "compare"
@@ -350,7 +453,7 @@ class DictCompare(ComfyNodeABC):
dict1 = dict1 or {}
dict2 = dict2 or {}
are_equal = (dict1 == dict2)
are_equal = dict1 == dict2
keys1 = set(dict1.keys())
keys2 = set(dict2.keys())
@@ -373,6 +476,7 @@ class DictContainsKey(ComfyNodeABC):
This node takes a dictionary and a key as inputs, then returns True if the key
exists in the dictionary, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -401,6 +505,7 @@ class DictExcludeKeys(ComfyNodeABC):
This node takes a dictionary and a list of keys, then returns a new dictionary
containing all key-value pairs except those with keys in the provided list.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -421,7 +526,7 @@ class DictExcludeKeys(ComfyNodeABC):
input_dict = input_dict if input_dict is not None else {}
keys_to_exclude = keys_to_exclude or []
if not(input_dict and keys_to_exclude):
if not (input_dict and keys_to_exclude):
return (input_dict,)
# `in` check is faster with sets + remove duplicates:
@@ -429,10 +534,7 @@ class DictExcludeKeys(ComfyNodeABC):
if len(input_dict) <= len(keys_to_exclude_set):
# It's faster to rebuild the dict with filter.
result = {
k: v for k, v in input_dict.items()
if k not in keys_to_exclude_set
}
result = {k: v for k, v in input_dict.items() if k not in keys_to_exclude_set}
else:
# It's faster to duplicate the dict, then pop all the exclusions one-by-one.
result = dict(input_dict)
@@ -455,6 +557,7 @@ class DictFilterByKeys(ComfyNodeABC):
This node takes a dictionary and a list of keys, then returns a new dictionary
containing only the key-value pairs for the keys that exist in the original dictionary.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -485,16 +588,10 @@ class DictFilterByKeys(ComfyNodeABC):
if len(keys_set) <= len(input_dict):
# It's faster to iterate over preserved keys
result = {
k: input_dict[k] for k in keys_set
if k in input_dict
}
result = {k: input_dict[k] for k in keys_set if k in input_dict}
else:
# It's faster to iterate over the input dict items
result = {
k: v for k, v in input_dict.items()
if k in keys_set
}
result = {k: v for k, v in input_dict.items() if k in keys_set}
if len(result) == len(input_dict):
# No changes made
@@ -512,6 +609,7 @@ class DictFromKeys(ComfyNodeABC):
dictionary where each key is associated with the value. If no value is
provided, None is used.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -520,7 +618,7 @@ class DictFromKeys(ComfyNodeABC):
},
"optional": {
"value": (IO.ANY, {"tooltip": "Default value assigned to every key (None when not provided)."}),
}
},
}
RETURN_TYPES = ("DICT",)
@@ -543,6 +641,7 @@ class DictGet(ComfyNodeABC):
If the key is not found and a default value is provided, that default is returned.
Otherwise, it returns None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -552,7 +651,7 @@ class DictGet(ComfyNodeABC):
},
"optional": {
"default": (IO.ANY, {"tooltip": "Value returned when the key is missing (None when not provided)."}),
}
},
}
RETURN_TYPES = (IO.ANY,)
@@ -574,6 +673,7 @@ class DictGetKeysValues(ComfyNodeABC):
This node takes a dictionary and returns two lists: one containing
all keys and another containing all corresponding values.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -604,6 +704,7 @@ class DictGetMultiple(ComfyNodeABC):
containing the corresponding values. If a key is not found, the default
value is used for that position.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -613,7 +714,7 @@ class DictGetMultiple(ComfyNodeABC):
},
"optional": {
"default": (IO.ANY, {"tooltip": "Value used for keys that are missing (None when not provided)."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -623,10 +724,7 @@ class DictGetMultiple(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_multiple"
def get_multiple(
self,
input_dict: Mapping[Any, VT], keys: list, default: T = None
) -> tuple[List[_U[VT, T]]]:
def get_multiple(self, input_dict: Mapping[Any, VT], keys: list, default: T = None) -> tuple[List[_U[VT, T]]]:
input_dict = input_dict if input_dict is not None else {}
keys = keys or []
values = [input_dict.get(key, default) for key in keys]
@@ -641,6 +739,7 @@ class DictInvert(ComfyNodeABC):
the keys become values and values become keys. Note that values must be
hashable to be used as keys in the new dictionary.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -675,6 +774,7 @@ class DictItems(ComfyNodeABC):
This node takes a dictionary and returns a list of tuples, where each tuple
contains a key-value pair from the dictionary.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -701,6 +801,7 @@ class DictKeys(ComfyNodeABC):
This node takes a dictionary and returns a list containing all of its keys.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -727,6 +828,7 @@ class DictLength(ComfyNodeABC):
This node takes a dictionary as input and returns its length (number of items).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -754,6 +856,7 @@ class DictMerge(ComfyNodeABC):
This node takes multiple dictionaries as input and combines them into a single
dictionary. If there are duplicate keys, values from later dictionaries take precedence.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -764,7 +867,7 @@ class DictMerge(ComfyNodeABC):
"dict2": ("DICT", {"tooltip": "Optional DICT to merge; its values win over dict1."}),
"dict3": ("DICT", {"tooltip": "Optional DICT to merge; its values win over earlier ones."}),
"dict4": ("DICT", {"tooltip": "Optional DICT to merge; its values win over earlier ones."}),
}
},
}
RETURN_TYPES = ("DICT",)
@@ -806,6 +909,7 @@ class DictPop(ComfyNodeABC):
associated with the key. If the key is not found and a default value is provided,
that default is returned. Otherwise, an error is raised.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -815,7 +919,7 @@ class DictPop(ComfyNodeABC):
},
"optional": {
"default_value": (IO.ANY, {"tooltip": "Value returned when the key is absent (None when not provided)."}),
}
},
}
RETURN_TYPES = ("DICT", IO.ANY)
@@ -852,6 +956,7 @@ class DictPopItem(ComfyNodeABC):
If operation fails, no error is thrown, but the last argument is `False`,
and the dictionary is returned intact.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -862,7 +967,12 @@ class DictPopItem(ComfyNodeABC):
RETURN_TYPES = ("DICT", IO.STRING, IO.ANY, IO.BOOLEAN)
RETURN_NAMES = ("dict", "key", "value", "success")
OUTPUT_TOOLTIPS = ("The DICT with one entry removed.", "The removed key.", "The removed value.", "False when the DICT was empty or the operation failed.")
OUTPUT_TOOLTIPS = (
"The DICT with one entry removed.",
"The removed key.",
"The removed value.",
"False when the DICT was empty or the operation failed.",
)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "popitem"
@@ -892,6 +1002,7 @@ class DictPopRandom(ComfyNodeABC):
If the dictionary is empty, it returns empty values.
An optional seed can be provided for reproducible selection.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -899,13 +1010,27 @@ class DictPopRandom(ComfyNodeABC):
"input_dict": ("DICT", {"tooltip": "The DICT to pop a random entry from."}),
},
"optional": {
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "Seed for reproducible selection. Leave empty to pick randomly each run."}),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"control_after_generate": True,
"tooltip": "Seed for reproducible selection. Leave empty to pick randomly each run.",
},
),
},
}
RETURN_TYPES = ("DICT", IO.STRING, IO.ANY, IO.BOOLEAN)
RETURN_NAMES = ("dict", "key", "value", "success")
OUTPUT_TOOLTIPS = ("The DICT with a random entry removed.", "The removed key.", "The removed value.", "False when the DICT was empty or the operation failed.")
OUTPUT_TOOLTIPS = (
"The DICT with a random entry removed.",
"The removed key.",
"The removed value.",
"False when the DICT was empty or the operation failed.",
)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "pop_random"
@@ -942,6 +1067,7 @@ class DictRemove(ComfyNodeABC):
dictionary with the specified key removed. If the key doesn't exist,
the dictionary remains unchanged.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -977,6 +1103,7 @@ class DictSet(ComfyNodeABC):
This node takes a dictionary, key, and value as inputs, then returns
a modified dictionary with the new key-value pair.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1011,6 +1138,7 @@ class DictSetDefault(ComfyNodeABC):
in the dictionary, the corresponding value is returned. If the key doesn't
exist, the default value is inserted for the key and returned.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1023,12 +1151,17 @@ class DictSetDefault(ComfyNodeABC):
RETURN_TYPES = ("DICT", IO.ANY)
RETURN_NAMES = ("DICT", "value")
OUTPUT_TOOLTIPS = ("The DICT, with the default inserted when the key was missing.", "The value for the key (existing or the inserted default).")
OUTPUT_TOOLTIPS = (
"The DICT, with the default inserted when the key was missing.",
"The value for the key (existing or the inserted default).",
)
CATEGORY = "Basic/DICT"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "setdefault"
def setdefault(self, input_dict: Mapping[KT, VT], key: KT2, default_value: VT2 = None) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]], _U[VT, VT2]]:
def setdefault(
self, input_dict: Mapping[KT, VT], key: KT2, default_value: VT2 = None
) -> tuple[Mapping[_U[KT, KT2], _U[VT, VT2]], _U[VT, VT2]]:
input_dict = input_dict if input_dict is not None else {}
result = dict(input_dict)
value = result.setdefault(key, default_value)
@@ -1045,6 +1178,7 @@ class DictUpdate(ComfyNodeABC):
contains all key-value pairs from both dictionaries. If there are duplicate
keys, the values from the second dictionary take precedence.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1079,6 +1213,7 @@ class DictValues(ComfyNodeABC):
This node takes a dictionary and returns a list containing all of its values.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1107,11 +1242,15 @@ class DictCreateFromJSONString(ComfyNodeABC):
parses it into a DICT. The input must be a JSON object (enclosed in curly
braces); otherwise a clear error is raised.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_string": (IO.STRING, {"default": "{}", "widgetType": "STRING", "tooltip": "STRING containing a JSON object to parse, e.g. {\"a\": 1}."}),
"json_string": (
IO.STRING,
{"default": "{}", "widgetType": "STRING", "tooltip": 'STRING containing a JSON object to parse, e.g. {"a": 1}.'},
),
}
}
@@ -1128,10 +1267,7 @@ class DictCreateFromJSONString(ComfyNodeABC):
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in input string: {e}")
if not isinstance(result, dict):
raise ValueError(
"The JSON string must be a JSON object (enclosed in curly braces), "
f"not {type(result).__name__}."
)
raise ValueError(f"The JSON string must be a JSON object (enclosed in curly braces), not {type(result).__name__}.")
return (result,)
+33 -6
View File
@@ -3,6 +3,7 @@ from inspect import cleandoc
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -10,6 +11,7 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
@@ -20,11 +22,19 @@ class FloatCreate(ComfyNodeABC):
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", "tooltip": "Textual form of the number to parse, e.g. \"3.14\". Must be a valid float."}),
"value": (
IO.ANY,
{
"default": "0.0",
"widgetType": "STRING",
"tooltip": 'Textual form of the number to parse, e.g. "3.14". Must be a valid float.',
},
),
}
}
@@ -45,6 +55,7 @@ class FloatAdd(ComfyNodeABC):
This node takes two floats as input and returns their sum.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -72,6 +83,7 @@ class FloatSubtract(ComfyNodeABC):
This node takes two floats as input and returns the result of subtracting
the second float from the first.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -98,6 +110,7 @@ class FloatMultiply(ComfyNodeABC):
This node takes two floats as input and returns their product.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -125,6 +138,7 @@ class FloatDivide(ComfyNodeABC):
This node takes two floats as input and returns the result of division.
It raises a ValueError if the divisor is 0.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -154,6 +168,7 @@ class FloatDivideSafe(ComfyNodeABC):
This node takes two floats as input and returns the result of the division.
It returns positive or negative infinity if the divisor is 0 (assumed to be +0.0).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -173,8 +188,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 (float("nan"),)
return (float("inf") if float1 > 0 else float("-inf"),)
return (float1 / float2,)
@@ -185,6 +200,7 @@ class FloatAsIntegerRatio(ComfyNodeABC):
This node takes a floating-point number and returns two integers,
which represent the ratio as numerator and denominator.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -212,11 +228,15 @@ class FloatFromHex(ComfyNodeABC):
This node takes a hexadecimal float string as input and returns the float.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"hex_value": (IO.STRING, {"default": "0x0.0p+0", "tooltip": "Hexadecimal float string as produced by the \"to hex\" node, e.g. 0x1.8p+1."}),
"hex_value": (
IO.STRING,
{"default": "0x0.0p+0", "tooltip": 'Hexadecimal float string as produced by the "to hex" node, e.g. 0x1.8p+1.'},
),
}
}
@@ -237,6 +257,7 @@ class FloatHex(ComfyNodeABC):
This node takes a float as input and returns its hexadecimal string representation.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -263,6 +284,7 @@ class FloatIsInteger(ComfyNodeABC):
This node takes a floating-point number as input and returns True if the number
is an integer, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -289,6 +311,7 @@ class FloatPower(ComfyNodeABC):
This node takes two floats as input and returns the result of raising
the first float to the power of the second.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -306,7 +329,7 @@ class FloatPower(ComfyNodeABC):
FUNCTION = "power"
def power(self, base: float, exponent: float) -> tuple[float]:
return (base ** exponent,)
return (base**exponent,)
class FloatRound(ComfyNodeABC):
@@ -316,12 +339,16 @@ class FloatRound(ComfyNodeABC):
This node takes a float and an integer for decimal places as inputs,
and returns the rounded result.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"float_value": (IO.FLOAT, {"default": 0.0, "tooltip": "The float to round."}),
"decimal_places": (IO.INT, {"default": 2, "min": 0, "tooltip": "Number of decimal places to keep (0 rounds to a whole number)."}),
"decimal_places": (
IO.INT,
{"default": 2, "min": 0, "tooltip": "Number of decimal places to keep (0 rounds to a whole number)."},
),
}
}
+56 -11
View File
@@ -4,6 +4,7 @@ from typing import Literal
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -11,6 +12,7 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
@@ -26,11 +28,19 @@ class IntCreate(ComfyNodeABC):
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", "tooltip": "Textual form of the integer to parse. Prefixes 0b/0o/0x select binary/octal/hexadecimal."}),
"value": (
IO.ANY,
{
"default": "0",
"widgetType": "STRING",
"tooltip": "Textual form of the integer to parse. Prefixes 0b/0o/0x select binary/octal/hexadecimal.",
},
),
}
}
@@ -52,12 +62,16 @@ class IntCreateWithBase(ComfyNodeABC):
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", "tooltip": "Textual form of the integer as written in the chosen base."}),
"base": (IO.INT, {"default": "10", "min": 2, "tooltip": "Numeric base to interpret the string in (>= 2), e.g. 2, 8, 10 or 16."}),
"base": (
IO.INT,
{"default": "10", "min": 2, "tooltip": "Numeric base to interpret the string in (>= 2), e.g. 2, 8, 10 or 16."},
),
}
}
@@ -78,6 +92,7 @@ class IntAdd(ComfyNodeABC):
This node takes two integers as input and returns their sum.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -105,6 +120,7 @@ class IntSubtract(ComfyNodeABC):
This node takes two integers as input and returns the result of subtracting
the second integer from the first.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -131,6 +147,7 @@ class IntMultiply(ComfyNodeABC):
This node takes two integers as input and returns their product.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -158,6 +175,7 @@ class IntDivide(ComfyNodeABC):
This node takes two integers as input and returns the result of integer
division. It raises a ValueError if the divisor is 0.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -187,13 +205,20 @@ class IntDivideSafe(ComfyNodeABC):
This node takes two integers as input and returns the result of the integer
division. It returns the positive or negative infinity value if the divisor is 0.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"int1": (IO.INT, {"default": 1, "tooltip": "Dividend (numerator)."}),
"int2": (IO.INT, {"default": 1, "tooltip": "Divisor; a value of 0 returns the infinity sentinel instead of an error."}),
"infinity": (IO.INT, {"default": 9223372036854775807, "tooltip": "Value returned as +infinity when dividing by zero (negated for negative results)."}), # 2**63 - 1
"infinity": (
IO.INT,
{
"default": 9223372036854775807,
"tooltip": "Value returned as +infinity when dividing by zero (negated for negative results).",
},
), # 2**63 - 1
}
}
@@ -216,6 +241,7 @@ class IntBitCount(ComfyNodeABC):
This node takes an integer as input and returns the count of set bits.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -243,6 +269,7 @@ class IntBitLength(ComfyNodeABC):
This node takes an integer as input and returns the number of bits needed
to represent it, excluding the sign and leading zeros.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -269,13 +296,20 @@ class IntFromBytes(ComfyNodeABC):
This class method takes bytes, byte order, and signed flag as inputs and
returns an integer.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"bytes_value": ("BYTES", {"tooltip": "The bytes object to decode into an integer."}),
"byteorder": (["big", "little"], {"default": "big", "tooltip": "Byte order: 'big' (most significant byte first) or 'little'."}),
"signed": (["True", "False"], {"default": "False", "tooltip": "When True the bytes are read as a two's complement signed integer."}),
"byteorder": (
["big", "little"],
{"default": "big", "tooltip": "Byte order: 'big' (most significant byte first) or 'little'."},
),
"signed": (
["True", "False"],
{"default": "False", "tooltip": "When True the bytes are read as a two's complement signed integer."},
),
}
}
@@ -287,7 +321,7 @@ class IntFromBytes(ComfyNodeABC):
FUNCTION = "from_bytes"
def from_bytes(self, bytes_value, byteorder: Literal["big", "little"], signed: Literal["True", "False"]) -> tuple[int]:
signed_bool = (signed == "True")
signed_bool = signed == "True"
return (int.from_bytes(bytes_value, byteorder=byteorder, signed=signed_bool),)
@@ -298,6 +332,7 @@ class IntModulus(ComfyNodeABC):
This node takes two integers as input and returns the remainder when the
first integer is divided by the second.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -327,6 +362,7 @@ class IntPower(ComfyNodeABC):
This node takes two integers as input and returns the result of raising
the first integer to the power of the second.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -354,14 +390,24 @@ class IntToBytes(ComfyNodeABC):
This node takes an integer, byte length, and byte order as inputs and
returns the bytes object representation of the integer.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"int_value": (IO.INT, {"default": 0, "tooltip": "The integer to convert."}),
"length": (IO.INT, {"default": 4, "min": 1, "tooltip": "Number of bytes in the result (must be large enough to hold the value)."}),
"byteorder": (["big", "little"], {"default": "big", "tooltip": "Byte order of the output: 'big' (most significant byte first) or 'little'."}),
"signed": (["True", "False"], {"default": "False", "tooltip": "When True the value is encoded as a two's complement signed integer."}),
"length": (
IO.INT,
{"default": 4, "min": 1, "tooltip": "Number of bytes in the result (must be large enough to hold the value)."},
),
"byteorder": (
["big", "little"],
{"default": "big", "tooltip": "Byte order of the output: 'big' (most significant byte first) or 'little'."},
),
"signed": (
["True", "False"],
{"default": "False", "tooltip": "When True the value is encoded as a two's complement signed integer."},
),
}
}
@@ -373,7 +419,7 @@ class IntToBytes(ComfyNodeABC):
FUNCTION = "to_bytes"
def to_bytes(self, int_value: int, length: int, byteorder: Literal["big", "little"], signed: Literal["True", "False"]) -> tuple[bytes]:
signed_bool = (signed == "True")
signed_bool = signed == "True"
return (int_value.to_bytes(length, byteorder=byteorder, signed=signed_bool),)
@@ -408,4 +454,3 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: IntPower": "power",
"Basic data handling: IntToBytes": "to bytes",
}
+124 -35
View File
@@ -5,6 +5,7 @@ import json
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -12,11 +13,12 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
from ._dynamic_input import ContainsDynamicDict
INT_MAX = 2**15-1 # the computer can do more but be nice to the eyes
INT_MAX = 2**15 - 1 # the computer can do more but be nice to the eyes
class ListCreate(ComfyNodeABC):
@@ -26,12 +28,22 @@ class ListCreate(ComfyNodeABC):
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 {
"optional": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the items of the LIST. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.ANY,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the items of the LIST. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("LIST",)
@@ -53,12 +65,22 @@ class ListCreateFromBoolean(ComfyNodeABC):
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 {
"optional": ContainsDynamicDict({
"item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the boolean items of the LIST. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.BOOLEAN,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the boolean items of the LIST. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("LIST",)
@@ -80,12 +102,22 @@ class ListCreateFromFloat(ComfyNodeABC):
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 {
"optional": ContainsDynamicDict({
"item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the float items of the LIST. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.FLOAT,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the float items of the LIST. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("LIST",)
@@ -107,12 +139,22 @@ class ListCreateFromInt(ComfyNodeABC):
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 {
"optional": ContainsDynamicDict({
"item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the integer items of the LIST. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.INT,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the integer items of the LIST. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("LIST",)
@@ -134,12 +176,18 @@ class ListCreateFromString(ComfyNodeABC):
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 {
"optional": ContainsDynamicDict({
"item_0": (IO.STRING, {"_dynamic": "number", "tooltip": "One of the string items of the LIST. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.STRING,
{"_dynamic": "number", "tooltip": "One of the string items of the LIST. Connect more values to add more items."},
),
}
)
}
RETURN_TYPES = ("LIST",)
@@ -213,6 +261,7 @@ class ListAppend(ComfyNodeABC):
This node takes a LIST and any item as inputs, then returns a new LIST
with the item appended to the end.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -243,6 +292,7 @@ class ListContains(ComfyNodeABC):
This node takes a LIST and a value as inputs, then returns True if the value
is present in the LIST, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -271,6 +321,7 @@ class ListCount(ComfyNodeABC):
This node takes a LIST and a value as inputs, then returns the number of times
the value appears in the LIST.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -306,7 +357,7 @@ class ListEnumerate:
},
"optional": {
"start": ("INT", {"default": 0, "tooltip": "Index assigned to the first element."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -318,7 +369,7 @@ class ListEnumerate:
def enumerate_list(self, list: list[Any], start: int = 0) -> tuple[list]:
list = list if list is not None else []
return ([__builtins__['list'](enumerate(list, start=start))],)
return ([__builtins__["list"](enumerate(list, start=start))],)
class ListExtend(ComfyNodeABC):
@@ -328,6 +379,7 @@ class ListExtend(ComfyNodeABC):
This node takes two LIST objects as input and returns a new LIST that contains
all elements from both lists.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -359,6 +411,7 @@ class ListFirst(ComfyNodeABC):
This node takes a LIST as input and returns the first element of the list.
If the LIST is empty, it returns None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -386,6 +439,7 @@ class ListGetItem(ComfyNodeABC):
Negative indices count from the end of the LIST.
Out of range indices return None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -418,6 +472,7 @@ class ListIndex(ComfyNodeABC):
occurrence of the value. Optional start and end parameters limit the search to a slice
of the LIST. Returns -1 if the value is not present.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -428,7 +483,7 @@ class ListIndex(ComfyNodeABC):
"optional": {
"start": ("INT", {"default": 0, "tooltip": "Start position of the search slice."}),
"end": ("INT", {"default": -1, "tooltip": "End position of the search slice (-1 means the end of the LIST)."}),
}
},
}
RETURN_TYPES = ("INT",)
@@ -456,6 +511,7 @@ class ListInsert(ComfyNodeABC):
This node takes a LIST, an index, and any item as inputs, then returns a new
LIST with the item inserted at the specified index.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -487,6 +543,7 @@ class ListLast(ComfyNodeABC):
This node takes a LIST as input and returns the last element of the list.
If the LIST is empty, it returns None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -512,6 +569,7 @@ class ListLength(ComfyNodeABC):
This node takes a LIST as input and returns its length as an integer.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -539,6 +597,7 @@ class ListMax(ComfyNodeABC):
This node takes a LIST of comparable items and returns the maximum value.
Returns None if the LIST is empty or if items are not comparable.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -572,6 +631,7 @@ class ListMin(ComfyNodeABC):
This node takes a LIST of comparable items and returns the minimum value.
Returns None if the LIST is empty or if items are not comparable.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -607,6 +667,7 @@ class ListPop(ComfyNodeABC):
removes and returns the last item.
When the LIST is empty, the item is None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -615,7 +676,7 @@ class ListPop(ComfyNodeABC):
},
"optional": {
"index": ("INT", {"default": -1, "tooltip": "Position of the item to remove (-1 = last item)."}),
}
},
}
RETURN_TYPES = ("LIST", IO.ANY)
@@ -643,6 +704,7 @@ class ListPopRandom(ComfyNodeABC):
and the removed element itself. If the LIST is empty, it returns None for the element.
An optional seed can be provided for reproducible selection.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -650,7 +712,16 @@ class ListPopRandom(ComfyNodeABC):
"list": ("LIST", {"tooltip": "The LIST to pop a random element from."}),
},
"optional": {
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "Seed for reproducible selection. Leave empty to pick randomly each run."}),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"control_after_generate": True,
"tooltip": "Seed for reproducible selection. Leave empty to pick randomly each run.",
},
),
},
}
@@ -670,6 +741,7 @@ class ListPopRandom(ComfyNodeABC):
def pop_random_element(self, list: list[Any], seed=None) -> tuple[list[Any], Any]:
import random
list = list if list is not None else []
rng = random.Random(seed) if seed is not None else random
result = list.copy()
@@ -687,6 +759,7 @@ class ListRange(ComfyNodeABC):
This node generates a LIST of numbers similar to Python's range() function.
It takes start, stop, and step parameters to define the sequence.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -696,7 +769,7 @@ class ListRange(ComfyNodeABC):
},
"optional": {
"step": ("INT", {"default": 1, "tooltip": "Step between numbers; must not be 0."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -720,6 +793,7 @@ class ListRemove(ComfyNodeABC):
the first occurrence of the value removed and a success indicator. If the value
is not present, the original LIST is returned with success set to False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -752,6 +826,7 @@ class ListReverse(ComfyNodeABC):
This node takes a LIST as input and returns a new LIST with the items in reversed order.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -781,6 +856,7 @@ class ListSetItem(ComfyNodeABC):
This node takes a LIST, an index, and a value, then returns a new LIST with
the item at the specified index replaced by the value.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -814,6 +890,7 @@ class ListShuffle(ComfyNodeABC):
This node takes a LIST and a seed as input and returns a new shuffled LIST.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -832,6 +909,7 @@ class ListShuffle(ComfyNodeABC):
def shuffle_list(self, list: list[Any], seed: int) -> tuple[list[Any]]:
import random
list = list if list is not None else []
random.seed(seed)
result = list.copy()
@@ -846,6 +924,7 @@ class ListSlice(ComfyNodeABC):
This node takes a LIST and start/stop/step parameters, and returns a new LIST
containing the specified slice of the original LIST.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -856,7 +935,7 @@ class ListSlice(ComfyNodeABC):
"start": ("INT", {"default": 0, "tooltip": "Start index (inclusive)."}),
"stop": ("INT", {"default": INT_MAX, "tooltip": "Stop index (exclusive); INT_MAX means the end."}),
"step": ("INT", {"default": 1, "tooltip": "Step between indices."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -866,11 +945,11 @@ class ListSlice(ComfyNodeABC):
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "slice"
def slice(self, list: list[Any], start: int = 0, stop: int = INT_MAX,
step: int = 1) -> tuple[list[Any]]:
def slice(self, list: list[Any], start: int = 0, stop: int = INT_MAX, step: int = 1) -> tuple[list[Any]]:
list = list if list is not None else []
return (list[start:stop:step],)
class ListSort(ComfyNodeABC):
"""
Sorts the items in a LIST.
@@ -878,6 +957,7 @@ class ListSort(ComfyNodeABC):
This node takes a LIST as input and returns a new sorted LIST.
Option includes sorting in reverse order.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -886,7 +966,7 @@ class ListSort(ComfyNodeABC):
},
"optional": {
"reverse": (["False", "True"], {"default": "False", "tooltip": "Sort in descending order when True."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -898,7 +978,7 @@ class ListSort(ComfyNodeABC):
def sort(self, list: list[Any], reverse: str = "False") -> tuple[list[Any]]:
# Convert string to boolean
reverse_bool = (reverse == "True")
reverse_bool = reverse == "True"
list = list if list is not None else []
# Use sorted to create a new sorted list
@@ -924,11 +1004,17 @@ class ListSum:
},
"optional": {
"start": ("INT", {"default": 0, "tooltip": "Initial value added to the sum."}),
}
},
}
RETURN_TYPES = ("INT", "FLOAT",)
RETURN_NAMES = ("sum_int", "sum_float",)
RETURN_TYPES = (
"INT",
"FLOAT",
)
RETURN_NAMES = (
"sum_int",
"sum_float",
)
OUTPUT_TOOLTIPS = ("The total as an integer.", "The total as a float.")
CATEGORY = "Basic/LIST"
DESCRIPTION = cleandoc(__doc__ or "")
@@ -948,6 +1034,7 @@ class ListToDataList(ComfyNodeABC):
converts it to a ComfyUI data list, allowing its items to be processed
individually by nodes that accept data lists.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -976,6 +1063,7 @@ class ListToSet(ComfyNodeABC):
This node takes a LIST input and creates a new SET containing all unique elements
from the LIST, removing any duplicates.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1004,11 +1092,15 @@ class ListCreateFromJSONString(ComfyNodeABC):
parses it into a LIST. The input must be a JSON array (enclosed in square
brackets); otherwise a clear error is raised.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_string": (IO.STRING, {"default": "[]", "widgetType": "STRING", "tooltip": "STRING containing a JSON array to parse, e.g. [1, 2, 3]."}),
"json_string": (
IO.STRING,
{"default": "[]", "widgetType": "STRING", "tooltip": "STRING containing a JSON array to parse, e.g. [1, 2, 3]."},
),
}
}
@@ -1025,10 +1117,7 @@ class ListCreateFromJSONString(ComfyNodeABC):
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in input string: {e}")
if not isinstance(result, list):
raise ValueError(
"The JSON string must be a JSON array (enclosed in square brackets), "
f"not {type(result).__name__}."
)
raise ValueError(f"The JSON string must be a JSON array (enclosed in square brackets), not {type(result).__name__}.")
return (result,)
+131 -56
View File
@@ -13,10 +13,12 @@ except ImportError:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
from ._dynamic_input import ContainsDynamicDict
class MathFormula(ComfyNodeABC):
"""
A node that evaluates a mathematical formula provided as a string without using eval.
@@ -44,11 +46,26 @@ class MathFormula(ComfyNodeABC):
def INPUT_TYPES(cls):
return {
"required": {
"formula": (IO.STRING, {"default": "-pi() ** 2", "tooltip": "Expression to evaluate using single-letter variables (a, b, c, ...), supported operators, parentheses and functions. Example: sqrt(a**2 + b**2)."}),
"formula": (
IO.STRING,
{
"default": "-pi() ** 2",
"tooltip": "Expression to evaluate using single-letter variables (a, b, c, ...), supported operators, parentheses and functions. Example: sqrt(a**2 + b**2).",
},
),
},
"optional": ContainsDynamicDict({
"a": (IO.NUMBER, {"default": 0.0, "_dynamic": "letter", "tooltip": "A numeric value bound to a single-letter variable in the formula. Connect more values to add the variables b, c, d, ..."}),
}),
"optional": ContainsDynamicDict(
{
"a": (
IO.NUMBER,
{
"default": 0.0,
"_dynamic": "letter",
"tooltip": "A numeric value bound to a single-letter variable in the formula. Connect more values to add the variables b, c, d, ...",
},
),
}
),
}
RETURN_TYPES = (IO.FLOAT,)
@@ -59,10 +76,35 @@ class MathFormula(ComfyNodeABC):
FUNCTION = "evaluate"
FUNC_ARITIES = {
"pi": 0, "e": 0, "abs": 1, "floor": 1, "ceil": 1, "round": 1, "sin": 1, "cos": 1,
"tan": 1, "asin": 1, "acos": 1, "atan": 1, "degrees": 1, "radians": 1, "sinh": 1,
"cosh": 1, "tanh": 1, "asinh": 1, "acosh": 1, "atanh": 1, "exp": 1, "log": 1,
"log10": 1, "log2": 1, "sqrt": 1, "pow": 2, "atan2": 2, "min": 2, "max": 2,
"pi": 0,
"e": 0,
"abs": 1,
"floor": 1,
"ceil": 1,
"round": 1,
"sin": 1,
"cos": 1,
"tan": 1,
"asin": 1,
"acos": 1,
"atan": 1,
"degrees": 1,
"radians": 1,
"sinh": 1,
"cosh": 1,
"tanh": 1,
"asinh": 1,
"acosh": 1,
"atanh": 1,
"exp": 1,
"log": 1,
"log10": 1,
"log2": 1,
"sqrt": 1,
"pow": 2,
"atan2": 2,
"min": 2,
"max": 2,
}
SUPPORTED_FUNCTIONS = set(FUNC_ARITIES.keys())
@@ -75,15 +117,11 @@ class MathFormula(ComfyNodeABC):
"//": (3, "LEFT", 2),
"%": (3, "LEFT", 2),
"**": (4, "RIGHT", 2),
"_NEG": (5, "RIGHT", 1), # Unary has higher precedence than exponentiation
"_NEG": (5, "RIGHT", 1), # Unary has higher precedence than exponentiation
}
OPERATORS = set(OPERATOR_PROPS.keys())
TOKEN_REGEX = re.compile(
r"([a-zA-Z_][a-zA-Z0-9_]*)"
r"|(\d+(?:\.\d*)?|\.\d+)"
r"|(\*\*|//|[+\-*/%(),])"
)
TOKEN_REGEX = re.compile(r"([a-zA-Z_][a-zA-Z0-9_]*)" r"|(\d+(?:\.\d*)?|\.\d+)" r"|(\*\*|//|[+\-*/%(),])")
def evaluate(self, formula: str, **kwargs) -> tuple[float]:
tokens = self.tokenize_formula(formula)
@@ -108,14 +146,14 @@ class MathFormula(ComfyNodeABC):
if self.is_number(token):
output_queue.append(float(token))
elif token.isalnum() and not token.isdigit():
is_function_call = (i + 1 < len(tokens) and tokens[i+1] == '(')
is_function_call = i + 1 < len(tokens) and tokens[i + 1] == "("
if is_function_call:
if token not in self.SUPPORTED_FUNCTIONS:
raise ValueError(f"Unknown function: '{token}'")
op_stack.append(token)
else:
if len(token) == 1 and token.isalpha():
output_queue.append(('VAR', token))
output_queue.append(("VAR", token))
else:
if token in self.SUPPORTED_FUNCTIONS:
raise ValueError(f"Function '{token}' must be called with parentheses, e.g., {token}()")
@@ -138,12 +176,12 @@ class MathFormula(ComfyNodeABC):
if op_stack and op_stack[-1] in self.SUPPORTED_FUNCTIONS:
output_queue.append(op_stack.pop())
elif token in self.OPERATORS:
is_unary = token == '-' and (prev_token is None or prev_token in binary_operators or prev_token in ['(', ','])
is_unary = token == "-" and (prev_token is None or prev_token in binary_operators or prev_token in ["(", ","])
op_to_push = "_NEG" if is_unary else token
props = self.OPERATOR_PROPS[op_to_push]
prec = props[0]
while (op_stack and op_stack[-1] != '(' and op_stack[-1] in self.OPERATORS):
while op_stack and op_stack[-1] != "(" and op_stack[-1] in self.OPERATORS:
stack_op_props = self.OPERATOR_PROPS[op_stack[-1]]
stack_op_prec = stack_op_props[0]
stack_op_assoc = stack_op_props[1]
@@ -169,7 +207,7 @@ class MathFormula(ComfyNodeABC):
for token in postfix:
if isinstance(token, float):
stack.append(token)
elif isinstance(token, tuple) and token[0] == 'VAR':
elif isinstance(token, tuple) and token[0] == "VAR":
var_name = token[1]
if var_name not in variables:
raise ValueError(f"Variable '{var_name}' was not provided.")
@@ -202,51 +240,87 @@ class MathFormula(ComfyNodeABC):
return stack[0]
def apply_operator(self, a: float, b: float, operator: str) -> float:
if operator == "+": return a + b
if operator == "-": return a - b
if operator == "*": return a * b
if operator == "**": return a ** b
if b == 0 and operator in ['/', '//', '%']:
if operator == "+":
return a + b
if operator == "-":
return a - b
if operator == "*":
return a * b
if operator == "**":
return a**b
if b == 0 and operator in ["/", "//", "%"]:
raise ZeroDivisionError(f"Division by zero in operator '{operator}'.")
if operator == "/": return a / b
if operator == "//": return a // b
if operator == "%": return a % b
if operator == "/":
return a / b
if operator == "//":
return a // b
if operator == "%":
return a % b
raise ValueError(f"Unsupported operator: {operator}")
def apply_function(self, func: str, args: list) -> float:
if func == "pi": return math.pi
if func == "e": return math.e
if func == "pi":
return math.pi
if func == "e":
return math.e
if len(args) == 1:
arg = args[0]
if func == "abs": return abs(arg)
if func == "floor": return math.floor(arg)
if func == "ceil": return math.ceil(arg)
if func == "round": return round(arg)
if func == "sin": return math.sin(arg)
if func == "cos": return math.cos(arg)
if func == "tan": return math.tan(arg)
if func == "asin": return math.asin(arg)
if func == "acos": return math.acos(arg)
if func == "atan": return math.atan(arg)
if func == "degrees": return math.degrees(arg)
if func == "radians": return math.radians(arg)
if func == "sinh": return math.sinh(arg)
if func == "cosh": return math.cosh(arg)
if func == "tanh": return math.tanh(arg)
if func == "asinh": return math.asinh(arg)
if func == "acosh": return math.acosh(arg)
if func == "atanh": return math.atanh(arg)
if func == "exp": return math.exp(arg)
if func == "log": return math.log(arg)
if func == "log10": return math.log10(arg)
if func == "log2": return math.log2(arg)
if func == "sqrt": return math.sqrt(arg)
if func == "abs":
return abs(arg)
if func == "floor":
return math.floor(arg)
if func == "ceil":
return math.ceil(arg)
if func == "round":
return round(arg)
if func == "sin":
return math.sin(arg)
if func == "cos":
return math.cos(arg)
if func == "tan":
return math.tan(arg)
if func == "asin":
return math.asin(arg)
if func == "acos":
return math.acos(arg)
if func == "atan":
return math.atan(arg)
if func == "degrees":
return math.degrees(arg)
if func == "radians":
return math.radians(arg)
if func == "sinh":
return math.sinh(arg)
if func == "cosh":
return math.cosh(arg)
if func == "tanh":
return math.tanh(arg)
if func == "asinh":
return math.asinh(arg)
if func == "acosh":
return math.acosh(arg)
if func == "atanh":
return math.atanh(arg)
if func == "exp":
return math.exp(arg)
if func == "log":
return math.log(arg)
if func == "log10":
return math.log10(arg)
if func == "log2":
return math.log2(arg)
if func == "sqrt":
return math.sqrt(arg)
if len(args) == 2:
a, b = args[0], args[1]
if func == "pow": return math.pow(a, b)
if func == "atan2": return math.atan2(a, b)
if func == "min": return min(a, b)
if func == "max": return max(a, b)
if func == "pow":
return math.pow(a, b)
if func == "atan2":
return math.atan2(a, b)
if func == "min":
return min(a, b)
if func == "max":
return max(a, b)
raise ValueError(f"Internal error: apply_function called with wrong number of args for '{func}'")
def is_number(self, value: str) -> bool:
@@ -256,6 +330,7 @@ class MathFormula(ComfyNodeABC):
except (ValueError, TypeError):
return False
NODE_CLASS_MAPPINGS = {
"Basic data handling: MathFormula": MathFormula,
}
+58 -7
View File
@@ -5,6 +5,7 @@ from typing import Literal, Union
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -12,14 +13,17 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
class MathAbs(ComfyNodeABC):
"""
Returns the absolute value of a number.
This node takes a number and returns its absolute value (magnitude without sign).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -46,11 +50,15 @@ class MathAcos(ComfyNodeABC):
This node takes a value between -1 and 1, and returns the arc cosine in radians or degrees.
The arc cosine is the inverse operation of cosine, returning the angle whose cosine is the input value.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The value (in [-1, 1]) whose arc cosine is computed."}),
"value": (
IO.NUMBER,
{"default": 0.0, "widgetType": "STRING", "tooltip": "The value (in [-1, 1]) whose arc cosine is computed."},
),
"unit": (["radians", "degrees"], {"default": "degrees", "tooltip": "Unit of the returned angle."}),
}
}
@@ -76,11 +84,15 @@ class MathAsin(ComfyNodeABC):
This node takes a value between -1 and 1, and returns the arc sine in radians or degrees.
The arc sine is the inverse operation of sine, returning the angle whose sine is the input value.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The value (in [-1, 1]) whose arc sine is computed."}),
"value": (
IO.NUMBER,
{"default": 0.0, "widgetType": "STRING", "tooltip": "The value (in [-1, 1]) whose arc sine is computed."},
),
"unit": (["radians", "degrees"], {"default": "degrees", "tooltip": "Unit of the returned angle."}),
}
}
@@ -106,6 +118,7 @@ class MathAtan(ComfyNodeABC):
This node takes a value and returns the arc tangent in radians or degrees.
The arc tangent is the inverse operation of tangent, returning the angle whose tangent is the input value.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -137,6 +150,7 @@ class MathAtan2(ComfyNodeABC):
Unlike atan(y/x), this function handles the case where x is zero and correctly determines
the quadrant of the resulting angle.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -167,6 +181,7 @@ class MathCeil(ComfyNodeABC):
This node takes a number and returns the smallest integer greater than or equal to the input value.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -194,6 +209,7 @@ class MathCos(ComfyNodeABC):
The cosine of an angle is the ratio of the length of the adjacent side to the length
of the hypotenuse in a right-angled triangle.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -223,6 +239,7 @@ class MathDegrees(ComfyNodeABC):
This node takes an angle in radians and returns the equivalent angle in degrees.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -248,6 +265,7 @@ class MathE(ComfyNodeABC):
This node returns the value of e (Euler's number), which is approximately 2.71828.
"""
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
@@ -269,6 +287,7 @@ class MathExp(ComfyNodeABC):
This node takes a number and returns e (Euler's number) raised to the power of that number.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -294,6 +313,7 @@ class MathFloor(ComfyNodeABC):
This node takes a number and returns the largest integer less than or equal to the input value.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -320,15 +340,27 @@ class MathLog(ComfyNodeABC):
This node takes a positive number and returns its natural logarithm.
If the specified base is not 'e', calculates the logarithm with the specified base.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING", "tooltip": "The positive number to take the logarithm of."}),
"value": (
IO.NUMBER,
{"default": 1.0, "min": 0.0000001, "widgetType": "STRING", "tooltip": "The positive number to take the logarithm of."},
),
},
"optional": {
"base": (IO.NUMBER, {"default": math.e, "min": 0.0000001, "widgetType": "STRING", "tooltip": "Logarithm base; e gives the natural logarithm."}),
}
"base": (
IO.NUMBER,
{
"default": math.e,
"min": 0.0000001,
"widgetType": "STRING",
"tooltip": "Logarithm base; e gives the natural logarithm.",
},
),
},
}
RETURN_TYPES = (IO.FLOAT,)
@@ -348,11 +380,20 @@ class MathLog10(ComfyNodeABC):
This node takes a positive number and returns its base-10 logarithm.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING", "tooltip": "The positive number to take the base-10 logarithm of."}),
"value": (
IO.NUMBER,
{
"default": 1.0,
"min": 0.0000001,
"widgetType": "STRING",
"tooltip": "The positive number to take the base-10 logarithm of.",
},
),
}
}
@@ -374,6 +415,7 @@ class MathMax(ComfyNodeABC):
This node takes two or more values and returns the maximum value among them.
The function works with both FLOAT and INT types.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -401,6 +443,7 @@ class MathMin(ComfyNodeABC):
This node takes two or more values and returns the minimum value among them.
The function works with both FLOAT and INT types.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -427,6 +470,7 @@ class MathPi(ComfyNodeABC):
This node returns the value of π, which is approximately 3.14159.
"""
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
@@ -448,6 +492,7 @@ class MathRadians(ComfyNodeABC):
This node takes an angle in degrees and returns the equivalent angle in radians.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -475,6 +520,7 @@ class MathSin(ComfyNodeABC):
The sine of an angle is the ratio of the length of the opposite side to the length
of the hypotenuse in a right-angled triangle.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -504,11 +550,15 @@ class MathSqrt(ComfyNodeABC):
This node takes a non-negative number and returns its square root.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "STRING", "tooltip": "The non-negative number to take the square root of."}),
"value": (
IO.NUMBER,
{"default": 0.0, "min": 0.0, "widgetType": "STRING", "tooltip": "The non-negative number to take the square root of."},
),
}
}
@@ -531,6 +581,7 @@ class MathTan(ComfyNodeABC):
The tangent of an angle is the ratio of the length of the opposite side to the length
of the adjacent side in a right-angled triangle.
"""
@classmethod
def INPUT_TYPES(cls):
return {
+169 -56
View File
@@ -5,6 +5,7 @@ import glob
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -14,11 +15,13 @@ except:
IMAGE = "IMAGE"
MASK = "MASK"
ANY = "*"
ComfyNodeABC = object
try:
from folder_paths import get_input_directory, get_output_directory
except:
def get_input_directory():
return "./"
@@ -38,6 +41,7 @@ except Exception:
def _require_numpy():
try:
import numpy as np
return np
except ModuleNotFoundError as e:
raise ModuleNotFoundError(
@@ -49,6 +53,7 @@ def _require_numpy():
def _require_torch():
try:
import torch
return torch
except ModuleNotFoundError as e:
raise ModuleNotFoundError(
@@ -60,6 +65,7 @@ def _require_torch():
def _require_pillow():
try:
from PIL import Image, ImageOps
return Image, ImageOps
except ModuleNotFoundError as e:
raise ModuleNotFoundError(
@@ -70,6 +76,7 @@ def _require_pillow():
# helper functions:
def load_image_helper(path: str):
"""Helper function to load an image from a path"""
Image, ImageOps = _require_pillow()
@@ -95,11 +102,11 @@ def extract_mask_from_alpha(img):
np = _require_numpy()
torch = _require_torch()
if 'A' in img.getbands():
alpha = np.array(img.getchannel('A')).astype(np.float32) / 255.0
if "A" in img.getbands():
alpha = np.array(img.getchannel("A")).astype(np.float32) / 255.0
mask_tensor = 1.0 - torch.from_numpy(alpha)
elif img.mode == 'P' and 'transparency' in img.info:
alpha = np.array(img.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0
elif img.mode == "P" and "transparency" in img.info:
alpha = np.array(img.convert("RGBA").getchannel("A")).astype(np.float32) / 255.0
mask_tensor = 1.0 - torch.from_numpy(alpha)
else:
# Create a blank mask if no alpha channel
@@ -116,15 +123,15 @@ def extract_mask_from_greyscale(img):
np = _require_numpy()
torch = _require_torch()
if img.mode == 'L':
if img.mode == "L":
# Image is already greyscale
gray = np.array(img).astype(np.float32) / 255.0
elif img.mode == 'RGB' or img.mode == 'RGBA':
elif img.mode == "RGB" or img.mode == "RGBA":
# Use the red channel of RGB or RGBA
gray = np.array(img.getchannel('R')).astype(np.float32) / 255.0
gray = np.array(img.getchannel("R")).astype(np.float32) / 255.0
else:
# Convert to greyscale if it's another format
gray_img = img.convert('L')
gray_img = img.convert("L")
gray = np.array(gray_img).astype(np.float32) / 255.0
# Convert to tensor and invert (white pixels in image = transparent in mask)
@@ -135,8 +142,10 @@ def extract_mask_from_greyscale(img):
return mask_tensor
# the nodes:
class PathAbspath(ComfyNodeABC):
"""
Returns the absolute path of a file or directory.
@@ -144,6 +153,7 @@ class PathAbspath(ComfyNodeABC):
This node takes a path and returns its absolute (full) path
by resolving any relative path components and symbolic links.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -170,6 +180,7 @@ class PathBasename(ComfyNodeABC):
This node extracts the filename component from a path,
removing any directory information.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -195,6 +206,7 @@ class PathCommonPrefix(ComfyNodeABC):
This node returns the longest common leading component of the given paths.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -203,7 +215,7 @@ class PathCommonPrefix(ComfyNodeABC):
},
"optional": {
"path2": (IO.STRING, {"default": "", "tooltip": "Second path."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -225,6 +237,7 @@ class PathDirname(ComfyNodeABC):
This node extracts the directory component from a path,
removing the filename.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -251,6 +264,7 @@ class PathExists(ComfyNodeABC):
This node returns True if the path exists (either as a file or a directory),
and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -277,6 +291,7 @@ class PathExpandVars(ComfyNodeABC):
This node replaces environment variables in a path with their values.
For example, $HOME or ${HOME} on Unix, or %USERPROFILE% on Windows.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -302,6 +317,7 @@ class PathGetCwd(ComfyNodeABC):
This node returns the current working directory as an absolute path.
"""
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
@@ -324,6 +340,7 @@ class PathGetExtension(ComfyNodeABC):
This node extracts the file extension from a path,
including the dot (e.g., '.txt').
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -350,6 +367,7 @@ class PathGetSize(ComfyNodeABC):
This node returns the size in bytes of the file at the given path.
Raises an error if the path doesn't exist or isn't a file.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -384,6 +402,7 @@ class PathGlob(ComfyNodeABC):
[seq] - matches any character in seq
[!seq] - matches any character not in seq
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -392,7 +411,7 @@ class PathGlob(ComfyNodeABC):
},
"optional": {
"recursive": (IO.BOOLEAN, {"default": False, "tooltip": "When True, '**' also matches inside subdirectories."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -428,6 +447,7 @@ class PathGlob(ComfyNodeABC):
# No changes, return a consistent value
import hashlib
m = hashlib.md5()
m.update(str(current_paths).encode())
return m.hexdigest()
@@ -443,6 +463,7 @@ class PathIsAbsolute(ComfyNodeABC):
This node returns True if the path is absolute (begins at the root directory),
and False if it's relative.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -469,6 +490,7 @@ class PathIsDir(ComfyNodeABC):
This node returns True if the path exists and is a directory,
and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -495,6 +517,7 @@ class PathIsFile(ComfyNodeABC):
This node returns True if the path exists and is a regular file,
and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -522,6 +545,7 @@ class PathJoin(ComfyNodeABC):
to form a single path. It handles directory separators correctly
for the operating system.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -530,7 +554,7 @@ class PathJoin(ComfyNodeABC):
},
"optional": {
"path2": (IO.STRING, {"default": "", "tooltip": "Second path component."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -554,6 +578,7 @@ class PathListDir(ComfyNodeABC):
If 'dirs_only' is True, it only returns directories.
If both are False, it returns all contents.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -563,7 +588,7 @@ class PathListDir(ComfyNodeABC):
"optional": {
"files_only": (IO.BOOLEAN, {"default": False, "tooltip": "When True, only files are returned."}),
"dirs_only": (IO.BOOLEAN, {"default": False, "tooltip": "When True, only directories are returned."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -601,6 +626,7 @@ class PathNormalize(ComfyNodeABC):
resolving up-level references, and converting to the correct
separator for the operating system.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -627,6 +653,7 @@ class PathSetExtension(ComfyNodeABC):
This node replaces the current extension in a path with a new one.
The extension should include the dot (e.g., '.jpg').
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -645,8 +672,8 @@ class PathSetExtension(ComfyNodeABC):
def set_extension(self, path: str, extension: str) -> tuple[str]:
# Make sure extension starts with a dot
if not extension.startswith('.') and extension:
extension = '.' + extension
if not extension.startswith(".") and extension:
extension = "." + extension
root, _ = os.path.splitext(path)
return (root + extension,)
@@ -659,6 +686,7 @@ class PathRelative(ComfyNodeABC):
This node computes a relative path from the 'start' path to the 'path'.
If 'start' is not provided, the current working directory is used.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -667,7 +695,7 @@ class PathRelative(ComfyNodeABC):
},
"optional": {
"start": (IO.STRING, {"default": "", "tooltip": "Base path; the current working directory is used when empty."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -690,6 +718,7 @@ class PathSplit(ComfyNodeABC):
This node takes a path and returns a tuple containing the directory path
and the filename.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -716,6 +745,7 @@ class PathSplitExt(ComfyNodeABC):
This node takes a path and returns a tuple containing the path without
the extension and the extension (including the dot).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -740,6 +770,7 @@ class PathLoadStringFile(ComfyNodeABC):
Loads a text file in UTF-8 encoding and returns its content as a STRING
without any further processing.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -785,6 +816,7 @@ class PathLoadImageRGB(ComfyNodeABC):
This node loads an image from the specified path and processes it to
return only the RGB channels as a tensor, ignoring any alpha channel.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -838,6 +870,7 @@ class PathLoadImageRGBA(ComfyNodeABC):
return the RGB channels as a tensor and the Alpha channel as a mask tensor.
If the image has no alpha channel, a blank mask is returned.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -848,7 +881,11 @@ class PathLoadImageRGBA(ComfyNodeABC):
RETURN_TYPES = (IO.IMAGE, IO.MASK, IO.BOOLEAN)
RETURN_NAMES = ("image", "mask", "exists")
OUTPUT_TOOLTIPS = ("The RGB image as a tensor.", "The alpha channel as a mask (blank when the image has none).", "True when the image was loaded.")
OUTPUT_TOOLTIPS = (
"The RGB image as a tensor.",
"The alpha channel as a mask (blank when the image has none).",
"True when the image was loaded.",
)
CATEGORY = "Basic/Path"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "load_image_rgba"
@@ -895,6 +932,7 @@ class PathLoadMaskFromAlpha(ComfyNodeABC):
channel to use as a mask. If the image has no alpha channel, a blank mask
is returned.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -941,6 +979,7 @@ class PathLoadMaskFromGreyscale(ComfyNodeABC):
If the image is greyscale, the intensity is used directly.
If the image is RGB, the red channel is used.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -996,6 +1035,7 @@ class PathSaveStringFile(ComfyNodeABC):
If 'append' is True, the text is appended to an existing file instead of
overwriting it.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1007,7 +1047,7 @@ class PathSaveStringFile(ComfyNodeABC):
"create_dirs": (IO.BOOLEAN, {"default": True, "tooltip": "Create missing parent directories."}),
"append": (IO.BOOLEAN, {"default": False, "tooltip": "Append to an existing file instead of overwriting it."}),
"encoding": (IO.STRING, {"default": "utf-8", "tooltip": "Text encoding to use when writing."}),
}
},
}
RETURN_TYPES = (IO.BOOLEAN,)
@@ -1117,16 +1157,17 @@ def build_xmp_packet(metadata_text: str) -> bytes:
``dc:description`` tag, as expected for JPEG XL ``xml `` boxes.
"""
from xml.sax.saxutils import escape
body = escape(metadata_text)
packet = (
'<?xpacket begin="\ufeff" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
'<x:xmpmeta xmlns:x="adobe:ns:meta/">\n'
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n'
'<rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/">\n'
'<dc:description><rdf:Alt><rdf:li xml:lang="x-default">' + body + '</rdf:li></rdf:Alt></dc:description>\n'
'</rdf:Description>\n'
'</rdf:RDF>\n'
'</x:xmpmeta>\n'
'<dc:description><rdf:Alt><rdf:li xml:lang="x-default">' + body + "</rdf:li></rdf:Alt></dc:description>\n"
"</rdf:Description>\n"
"</rdf:RDF>\n"
"</x:xmpmeta>\n"
'<?xpacket end="w"?>'
)
return packet.encode("utf-8")
@@ -1173,12 +1214,12 @@ def metadata_save_kwargs(metadata_text: str, fmt: str) -> dict:
_PIL_FORMAT_BY_TOKEN: dict[str, str] = {
"png": "PNG",
"jpg": "JPEG",
"jpeg": "JPEG", # alias; normalised to "jpg"
"jpeg": "JPEG", # alias; normalised to "jpg"
"webp": "WEBP",
"jxl": "JXL",
"bmp": "BMP",
"tif": "TIFF",
"tiff": "TIFF", # alias; normalised to "tif"
"tiff": "TIFF", # alias; normalised to "tif"
"gif": "GIF",
"jp2": "JPEG2000",
"jpeg2000": "JPEG2000", # alias; normalised to "jp2"
@@ -1197,8 +1238,22 @@ _IMAGE_SAVE_ORDER = ["png", "jpg", "webp", "jxl", "bmp", "tif", "gif", "jp2", "p
# save node (icons/cursors, GPU textures, vector/postscript, single-multi-image
# containers, scientific dumps). Never offered even if Pillow can write them.
_NON_PHOTOGRAPHIC_RASTER_IDS = {
"ICO", "CUR", "ICNS", "DDS", "DIB", "EPS", "IM", "MPO", "SGI",
"PDF", "WMF", "EMF", "BUFR", "GRIB", "HDF5", "FITS",
"ICO",
"CUR",
"ICNS",
"DDS",
"DIB",
"EPS",
"IM",
"MPO",
"SGI",
"PDF",
"WMF",
"EMF",
"BUFR",
"GRIB",
"HDF5",
"FITS",
}
# canonical token for a couple of common user-spelled aliases
@@ -1221,6 +1276,7 @@ def _writable_pillow_formats(mode: str) -> set[str]:
"""
import io as _io
from PIL import Image as _PIL
_load_image_plugins()
ids = set(_PIL.SAVE.keys())
ids.update(str(v).upper() for v in _PIL.registered_extensions().values())
@@ -1247,10 +1303,7 @@ def _discover_save_formats() -> tuple[list[str], list[str]]:
writable_rgba = _writable_pillow_formats("RGBA")
# 1) tokens we already know about and that are actually writable
offered = [
t for t in _IMAGE_SAVE_ORDER
if _PIL_FORMAT_BY_TOKEN[t] in writable_rgb
]
offered = [t for t in _IMAGE_SAVE_ORDER if _PIL_FORMAT_BY_TOKEN[t] in writable_rgb]
known_ids = {_PIL_FORMAT_BY_TOKEN[t] for t in offered}
# 2) any additional writable photographic format in the library/plugins
@@ -1313,6 +1366,7 @@ def _comfy_format_date(fmt_text: str, when) -> str:
unpadded, doubled letters are zero-padded.
"""
import re
parts = {
"d": when.day,
"M": when.month,
@@ -1367,7 +1421,6 @@ def _expand_filename_tokens(text: str, width: int = 0, height: int = 0) -> str:
return text
def _official_save_image_path(filename_prefix, output_dir, image_width=0, image_height=0):
"""
Resolve ``filename_prefix`` to concrete output folder/filename/counter using
@@ -1384,6 +1437,7 @@ def _official_save_image_path(filename_prefix, output_dir, image_width=0, image_
# --- fallback (only reached outside ComfyUI) -------------------------
def expand(text, w, h):
import time
now = time.localtime()
text = text.replace("%width%", str(w)).replace("%height%", str(h))
text = text.replace("%year%", str(now.tm_year))
@@ -1404,8 +1458,8 @@ def _official_save_image_path(filename_prefix, output_dir, image_width=0, image_
try:
for entry in os.listdir(full_output_folder):
stem, _, _ = entry.rpartition(".")
if stem.startswith(filename + "_") and stem[len(filename) + 1:].rstrip("_").isdigit():
digits = stem[len(filename) + 1:].rstrip("_")
if stem.startswith(filename + "_") and stem[len(filename) + 1 :].rstrip("_").isdigit():
digits = stem[len(filename) + 1 :].rstrip("_")
if digits.isdigit():
highest = max(highest, int(digits))
except FileNotFoundError:
@@ -1413,8 +1467,7 @@ def _official_save_image_path(filename_prefix, output_dir, image_width=0, image_
return full_output_folder, filename, highest + 1, subfolder, filename_prefix
def _plan_save_paths(path: str, format: str, use_prefix_mode: bool,
frame_count: int, width: int, height: int) -> tuple[list[str], bool]:
def _plan_save_paths(path: str, format: str, use_prefix_mode: bool, frame_count: int, width: int, height: int) -> tuple[list[str], bool]:
"""
Compute the absolute destination path(s) for ``frame_count`` images.
@@ -1432,9 +1485,7 @@ def _plan_save_paths(path: str, format: str, use_prefix_mode: bool,
path = _expand_filename_tokens(path, width, height)
if use_prefix_mode:
full_output_folder, filename, counter, _, _ = _official_save_image_path(
path, get_output_directory(), width, height
)
full_output_folder, filename, counter, _, _ = _official_save_image_path(path, get_output_directory(), width, height)
paths = []
for _ in range(frame_count):
name = f"{filename}_{counter:05}_.{fmt}"
@@ -1466,6 +1517,7 @@ def _has_jxl_support() -> bool:
"""Return True when the pillow-jxl plugin is importable."""
try:
import pillow_jxl # noqa: F401 - imported but unused, kept for JPEG XL support
return True
except ModuleNotFoundError:
return False
@@ -1518,15 +1570,28 @@ class PathSaveImageRGB(ComfyNodeABC):
the EXIF + XMP boxes for JPEG XL. Formats that cannot carry text metadata
ignore the prompts.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": (IO.IMAGE,),
"path": (IO.STRING, {"default": "", "tooltip": "Destination file path (an extension is added from the format when missing), or a ComfyUI filename_prefix under the output folder when \"use prefix mode\" is enabled."}),
"path": (
IO.STRING,
{
"default": "",
"tooltip": 'Destination file path (an extension is added from the format when missing), or a ComfyUI filename_prefix under the output folder when "use prefix mode" is enabled.',
},
),
},
"optional": {
"format": (_IMAGE_SAVE_FORMATS, {"default": "png", "tooltip": "Image save format (png, jpg, webp, bmp, ...). The list is derived from what the installed Pillow/plugins can write. Drag a STRING onto this to override it."}),
"format": (
_IMAGE_SAVE_FORMATS,
{
"default": "png",
"tooltip": "Image save format (png, jpg, webp, bmp, ...). The list is derived from what the installed Pillow/plugins can write. Drag a STRING onto this to override it.",
},
),
"quality": (IO.INT, {"default": 95, "min": 1, "max": 100, "tooltip": "Quality for lossy formats (jpg/webp/jxl)."}),
"create_dirs": (IO.BOOLEAN, {"default": True, "tooltip": "Create missing parent directories (plain path mode only)."}),
"prompt": (IO.STRING, {"default": "", "tooltip": "Optional positive prompt embedded as parameters metadata."}),
@@ -1534,8 +1599,14 @@ class PathSaveImageRGB(ComfyNodeABC):
# Appended last on purpose: the node stores widget values
# positionally, so adding before the existing widgets would shift
# old workflows and mis-assign their saved values.
"use_prefix_mode": (IO.BOOLEAN, {"default": False, "tooltip": "When True, 'path' is treated as a ComfyUI filename_prefix under the output folder and files are named/auto-numbered exactly like ComfyUI's \"Save Image\" node."}),
}
"use_prefix_mode": (
IO.BOOLEAN,
{
"default": False,
"tooltip": "When True, 'path' is treated as a ComfyUI filename_prefix under the output folder and files are named/auto-numbered exactly like ComfyUI's \"Save Image\" node.",
},
),
},
}
RETURN_TYPES = (IO.BOOLEAN,)
@@ -1546,9 +1617,17 @@ class PathSaveImageRGB(ComfyNodeABC):
FUNCTION = "save_image"
OUTPUT_NODE = True
def save_image(self, images, path: str = "", use_prefix_mode: bool = False, format: str = "png",
quality: int = 95, create_dirs: bool = True,
prompt: str = "", negative_prompt: str = ""):
def save_image(
self,
images,
path: str = "",
use_prefix_mode: bool = False,
format: str = "png",
quality: int = 95,
create_dirs: bool = True,
prompt: str = "",
negative_prompt: str = "",
):
if not path:
print("Basic data handling: Save failed - no path specified")
return (False,)
@@ -1558,8 +1637,10 @@ class PathSaveImageRGB(ComfyNodeABC):
fmt = _normalize_image_format(format)
if fmt == "jxl" and not _has_jxl_support():
print("Basic data handling: JPEG XL format requested but pillow_jxl module is not installed. "
"Please install it with 'pip install pillow-jxl-plugin'.")
print(
"Basic data handling: JPEG XL format requested but pillow_jxl module is not installed. "
"Please install it with 'pip install pillow-jxl-plugin'."
)
return (False,)
batch = len(images)
@@ -1604,16 +1685,29 @@ class PathSaveImageRGBA(ComfyNodeABC):
the EXIF + XMP boxes for JPEG XL. Formats that cannot carry text metadata
ignore the prompts.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": (IO.IMAGE,),
"mask": (IO.MASK,),
"path": (IO.STRING, {"default": "", "tooltip": "Destination file path (an extension is added from the format when missing), or a ComfyUI filename_prefix under the output folder when \"use prefix mode\" is enabled."}),
"path": (
IO.STRING,
{
"default": "",
"tooltip": 'Destination file path (an extension is added from the format when missing), or a ComfyUI filename_prefix under the output folder when "use prefix mode" is enabled.',
},
),
},
"optional": {
"format": (_IMAGE_SAVE_ALPHA_FORMATS, {"default": "png", "tooltip": "Image save format that can hold an alpha channel (png, webp, ...; jpg is coerced to png). Derived from what the installed Pillow/plugins can write. Drag a STRING onto this to override it."}),
"format": (
_IMAGE_SAVE_ALPHA_FORMATS,
{
"default": "png",
"tooltip": "Image save format that can hold an alpha channel (png, webp, ...; jpg is coerced to png). Derived from what the installed Pillow/plugins can write. Drag a STRING onto this to override it.",
},
),
"quality": (IO.INT, {"default": 95, "min": 1, "max": 100, "tooltip": "Quality for lossy formats (webp/jxl)."}),
"invert_mask": (IO.BOOLEAN, {"default": False, "tooltip": "Invert the mask before using it as the alpha channel."}),
"create_dirs": (IO.BOOLEAN, {"default": True, "tooltip": "Create missing parent directories (plain path mode only)."}),
@@ -1622,8 +1716,14 @@ class PathSaveImageRGBA(ComfyNodeABC):
# Appended last on purpose: the node stores widget values
# positionally, so adding before the existing widgets would shift
# old workflows and mis-assign their saved values.
"use_prefix_mode": (IO.BOOLEAN, {"default": False, "tooltip": "When True, 'path' is treated as a ComfyUI filename_prefix under the output folder and files are named/auto-numbered exactly like ComfyUI's \"Save Image\" node."}),
}
"use_prefix_mode": (
IO.BOOLEAN,
{
"default": False,
"tooltip": "When True, 'path' is treated as a ComfyUI filename_prefix under the output folder and files are named/auto-numbered exactly like ComfyUI's \"Save Image\" node.",
},
),
},
}
RETURN_TYPES = (IO.BOOLEAN,)
@@ -1634,10 +1734,19 @@ class PathSaveImageRGBA(ComfyNodeABC):
FUNCTION = "save_image_with_mask"
OUTPUT_NODE = True
def save_image_with_mask(self, images, mask, path: str = "", use_prefix_mode: bool = False,
format: str = "png", quality: int = 95,
invert_mask: bool = False, create_dirs: bool = True,
prompt: str = "", negative_prompt: str = ""):
def save_image_with_mask(
self,
images,
mask,
path: str = "",
use_prefix_mode: bool = False,
format: str = "png",
quality: int = 95,
invert_mask: bool = False,
create_dirs: bool = True,
prompt: str = "",
negative_prompt: str = "",
):
if not path:
print("Basic data handling: Save failed - no path specified")
return (False,)
@@ -1651,8 +1760,10 @@ class PathSaveImageRGBA(ComfyNodeABC):
print("Basic data handling: JPEG format doesn't support transparency. Using PNG instead.")
fmt = "png"
if fmt == "jxl" and not _has_jxl_support():
print("Basic data handling: JPEG XL format requested but pillow_jxl module is not installed. "
"Please install it with 'pip install pillow-jxl-plugin'.")
print(
"Basic data handling: JPEG XL format requested but pillow_jxl module is not installed. "
"Please install it with 'pip install pillow-jxl-plugin'."
)
return (False,)
batch = len(images)
@@ -1705,6 +1816,7 @@ class PathInputDir(ComfyNodeABC):
This is where input images are usually stored when using ComfyUI
"""
@classmethod
def INPUT_TYPES(cls):
return {}
@@ -1727,6 +1839,7 @@ class PathOutputDir(ComfyNodeABC):
This is where output images are usually stored when using ComfyUI
"""
@classmethod
def INPUT_TYPES(cls):
return {}
+16 -1
View File
@@ -4,6 +4,7 @@ import re
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -11,12 +12,15 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
class RegexFindallDataList(ComfyNodeABC):
"""
Returns all non-overlapping matches of a pattern in the string as a data list of strings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -42,6 +46,7 @@ class RegexFindallList(ComfyNodeABC):
"""
Returns all non-overlapping matches of a pattern in the string as a Python LIST.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -67,6 +72,7 @@ class RegexGroupDict(ComfyNodeABC):
Searches the string with the given pattern and returns a DICT of named groups.
If no match is found, it returns an empty DICT.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -95,6 +101,7 @@ class RegexSearchGroupsDataList(ComfyNodeABC):
Searches the string for a match to the pattern and returns a data list of the captured groups.
If no match is found, it returns an empty data list.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -124,6 +131,7 @@ class RegexSearchGroupsList(ComfyNodeABC):
Searches the string for a match to the pattern and returns a Python LIST of the captured groups.
If no match is found, it returns an empty LIST.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -151,6 +159,7 @@ class RegexSplitDataList(ComfyNodeABC):
"""
Splits the string at each match of the pattern and returns a data list of substrings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -176,6 +185,7 @@ class RegexSplitList(ComfyNodeABC):
"""
Splits the string at each match of the pattern and returns a Python LIST of substrings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -200,6 +210,7 @@ class RegexSub(ComfyNodeABC):
"""
Substitutes matches of the pattern in the string with a replacement string.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -207,7 +218,10 @@ class RegexSub(ComfyNodeABC):
"string": (IO.STRING, {"tooltip": "The text to modify."}),
"pattern": (IO.STRING, {"tooltip": "Regular expression pattern whose matches are replaced."}),
"repl": (IO.STRING, {"tooltip": "Replacement text. Backreferences to groups are supported."}),
"count": ("INT", {"default": 0, "tooltip": "Maximum number of replacements; 0 replaces all occurrences."}), # 0 means replace all occurrences
"count": (
"INT",
{"default": 0, "tooltip": "Maximum number of replacements; 0 replaces all occurrences."},
), # 0 means replace all occurrences
}
}
@@ -227,6 +241,7 @@ class RegexTest(ComfyNodeABC):
Tests whether a given regex pattern matches any part of the input string.
Returns True if a match is found, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
+110 -22
View File
@@ -4,6 +4,7 @@ from inspect import cleandoc
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -11,6 +12,7 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
from ._dynamic_input import ContainsDynamicDict
@@ -28,12 +30,22 @@ class SetCreate(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the items to add to the SET. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.ANY,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the items to add to the SET. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("SET",)
@@ -55,12 +67,22 @@ class SetCreateFromBoolean(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the boolean items to add to the SET. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.BOOLEAN,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the boolean items to add to the SET. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("SET",)
@@ -82,12 +104,22 @@ class SetCreateFromFloat(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the float items to add to the SET. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.FLOAT,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the float items to add to the SET. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("SET",)
@@ -109,12 +141,22 @@ class SetCreateFromInt(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the integer items to add to the SET. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.INT,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the integer items to add to the SET. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("SET",)
@@ -136,12 +178,22 @@ class SetCreateFromString(ComfyNodeABC):
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": ContainsDynamicDict({
"item_0": (IO.STRING, {"_dynamic": "number", "widgetType": "STRING", "tooltip": "One of the string items to add to the SET. Connect more values to add more items."}),
})
"optional": ContainsDynamicDict(
{
"item_0": (
IO.STRING,
{
"_dynamic": "number",
"widgetType": "STRING",
"tooltip": "One of the string items to add to the SET. Connect more values to add more items.",
},
),
}
)
}
RETURN_TYPES = ("SET",)
@@ -163,6 +215,7 @@ class SetAdd(ComfyNodeABC):
This node takes a SET and any item as inputs, then returns a new SET
with the item added. If the item is already present, the SET remains unchanged.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -193,6 +246,7 @@ class SetAll(ComfyNodeABC):
This node takes a SET as input and returns True if all elements in the SET
evaluate to True (or if the SET is empty), and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -220,6 +274,7 @@ class SetAny(ComfyNodeABC):
This node takes a SET as input and returns True if at least one element
in the SET evaluates to True, and False otherwise (including if the SET is empty).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -247,6 +302,7 @@ class SetContains(ComfyNodeABC):
This node takes a SET and a value as inputs, then returns True if the value
is present in the SET, and False otherwise.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -275,6 +331,7 @@ class SetDifference(ComfyNodeABC):
This node takes two SETs as input and returns a new SET containing
elements in the first SET but not in the second SET.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -306,6 +363,7 @@ class SetDiscard(ComfyNodeABC):
This node takes a SET and any item as inputs, then returns a new SET
with the item removed. Unlike remove, no error is raised if the item is not present.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -340,6 +398,7 @@ class SetEnumerate(ComfyNodeABC):
Note: Since SETs are unordered, the enumeration order is arbitrary but consistent
within a single operation.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -348,7 +407,7 @@ class SetEnumerate(ComfyNodeABC):
},
"optional": {
"start": ("INT", {"default": 0, "tooltip": "Index value assigned to the first element."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -370,6 +429,7 @@ class SetIntersection(ComfyNodeABC):
This node takes multiple SETs as input and returns a new SET containing
only elements present in all input SETs.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -380,7 +440,7 @@ class SetIntersection(ComfyNodeABC):
"optional": {
"set3": ("SET", {"tooltip": "Optional additional SET."}),
"set4": ("SET", {"tooltip": "Optional additional SET."}),
}
},
}
RETURN_TYPES = ("SET",)
@@ -411,6 +471,7 @@ class SetIsDisjoint(ComfyNodeABC):
This node takes two SETs as input and returns True if they have no elements in common.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -440,6 +501,7 @@ class SetIsSubset(ComfyNodeABC):
This node takes two SETs as input and returns True if set1 is a subset of set2
(all elements in set1 are also in set2).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -469,6 +531,7 @@ class SetIsSuperset(ComfyNodeABC):
This node takes two SETs as input and returns True if set1 is a superset of set2
(set1 contains all elements in set2).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -497,6 +560,7 @@ class SetLength(ComfyNodeABC):
This node takes a SET as input and returns its length (number of elements) as an integer.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -525,6 +589,7 @@ class SetPop(ComfyNodeABC):
with an arbitrary item removed and the removed item.
When the SET is empty, the item is None.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -558,6 +623,7 @@ class SetPopRandom(ComfyNodeABC):
and the removed element itself. If the SET is empty, it returns None for the element.
An optional seed can be provided for reproducible selection.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -565,7 +631,16 @@ class SetPopRandom(ComfyNodeABC):
"set": ("SET", {"tooltip": "The SET to pop a random element from."}),
},
"optional": {
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "Seed for reproducible selection. Leave empty to pick randomly each run."}),
"seed": (
"INT",
{
"default": 0,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"control_after_generate": True,
"tooltip": "Seed for reproducible selection. Leave empty to pick randomly each run.",
},
),
},
}
@@ -585,6 +660,7 @@ class SetPopRandom(ComfyNodeABC):
def pop_random_element(self, set: set[Any], seed=None) -> tuple[set[Any], Any]:
import random
set = set if set is not None else _empty_set()
rng = random.Random(seed) if seed is not None else random
result = set.copy()
@@ -603,6 +679,7 @@ class SetRemove(ComfyNodeABC):
with the item removed and a success indicator. If the item is not present,
the original SET is returned with success set to False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -640,6 +717,7 @@ class SetSum(ComfyNodeABC):
compatible with addition. If the SET contains mixed or incompatible types,
it may raise an error.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -648,11 +726,17 @@ class SetSum(ComfyNodeABC):
},
"optional": {
"start": ("INT", {"default": 0, "tooltip": "Initial value added to the sum."}),
}
},
}
RETURN_TYPES = ("INT", "FLOAT",)
RETURN_NAMES = ("sum_int", "sum_float",)
RETURN_TYPES = (
"INT",
"FLOAT",
)
RETURN_NAMES = (
"sum_int",
"sum_float",
)
OUTPUT_TOOLTIPS = ("The total as an integer.", "The total as a float.")
CATEGORY = "Basic/SET"
DESCRIPTION = cleandoc(__doc__ or "")
@@ -671,6 +755,7 @@ class SetSymmetricDifference(ComfyNodeABC):
This node takes two SETs as input and returns a new SET containing
elements in either SET but not in both.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -702,6 +787,7 @@ class SetUnion(ComfyNodeABC):
This node takes multiple SETs as input and returns a new SET containing
all elements from all the input SETs.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -712,7 +798,7 @@ class SetUnion(ComfyNodeABC):
"optional": {
"set3": ("SET", {"tooltip": "Optional additional SET."}),
"set4": ("SET", {"tooltip": "Optional additional SET."}),
}
},
}
RETURN_TYPES = ("SET",)
@@ -745,6 +831,7 @@ class SetToDataList(ComfyNodeABC):
converts it to a ComfyUI data list, allowing its items to be processed
individually by nodes that accept data lists.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -774,6 +861,7 @@ class SetToList(ComfyNodeABC):
from the SET. Note that the order of elements in the resulting LIST is arbitrary
since SETs are unordered collections.
"""
@classmethod
def INPUT_TYPES(cls):
return {
+97 -40
View File
@@ -3,6 +3,7 @@ from inspect import cleandoc
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -10,10 +11,13 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
class StringCapitalize(ComfyNodeABC):
"""Converts the first character of the input string to uppercase and all other characters to lowercase."""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -41,6 +45,7 @@ class StringCasefold(ComfyNodeABC):
Unlike standard lowercase, this handles special cases like converting the German 'ß' to "ss".
Use this node when you need the most accurate case-insensitive text matching.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -68,6 +73,7 @@ class StringCenter(ComfyNodeABC):
By default, it uses spaces as padding characters, but you can specify any
single character to use as padding.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -77,7 +83,7 @@ class StringCenter(ComfyNodeABC):
},
"optional": {
"fillchar": (IO.STRING, {"default": " ", "tooltip": "Padding character (only its first character is used)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -99,6 +105,7 @@ class StringCenter(ComfyNodeABC):
class StringConcat(ComfyNodeABC):
"""Combines two text strings together, joining them end-to-end."""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -126,6 +133,7 @@ class StringCount(ComfyNodeABC):
This node returns the number of times the specified substring appears in the input string.
You can optionally specify start and end positions to limit the search range.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -136,7 +144,7 @@ class StringCount(ComfyNodeABC):
"optional": {
"start": (IO.INT, {"default": 0, "min": 0, "tooltip": "Start position of the search range."}),
"end": (IO.INT, {"default": 0, "min": 0, "tooltip": "End position of the search range (0 means the end)."}),
}
},
}
RETURN_TYPES = (IO.INT,)
@@ -162,12 +170,19 @@ class StringDecode(ComfyNodeABC):
and converts it back to a regular text string using the specified encoding.
Useful for processing encoded data received from files or network sources.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"bytes_string": (IO.STRING, {"default": "b''", "tooltip": "String representation of bytes, e.g. b'text', as produced by the encode node."}),
"encoding": (["utf-8", "ascii", "latin-1", "utf-16", "utf-32", "cp1252"], {"tooltip": "Character encoding to decode with."}),
"bytes_string": (
IO.STRING,
{"default": "b''", "tooltip": "String representation of bytes, e.g. b'text', as produced by the encode node."},
),
"encoding": (
["utf-8", "ascii", "latin-1", "utf-16", "utf-32", "cp1252"],
{"tooltip": "Character encoding to decode with."},
),
"errors": (["strict", "ignore", "replace", "backslashreplace"], {"tooltip": "How to handle decoding errors."}),
}
}
@@ -194,7 +209,7 @@ class StringDecode(ComfyNodeABC):
return (f"Invalid bytes string format: {bytes_string}",)
# Handle escape sequences
content = content.encode('utf-8').decode('unicode_escape').encode('latin-1')
content = content.encode("utf-8").decode("unicode_escape").encode("latin-1")
# Decode using the specified encoding
result = content.decode(encoding, errors)
@@ -213,13 +228,17 @@ class StringEncode(ComfyNodeABC):
ASCII, or others. It's useful for preparing text for file saving or
network transmission in specific formats.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"string": (IO.STRING, {"default": "", "tooltip": "The text to encode."}),
"encoding": (["utf-8", "ascii", "latin-1", "utf-16", "utf-32", "cp1252"], {"tooltip": "Character encoding to use."}),
"errors": (["strict", "ignore", "replace", "xmlcharrefreplace", "backslashreplace"], {"tooltip": "How to handle characters that cannot be encoded."}),
"errors": (
["strict", "ignore", "replace", "xmlcharrefreplace", "backslashreplace"],
{"tooltip": "How to handle characters that cannot be encoded."},
),
}
}
@@ -251,6 +270,7 @@ class StringEndswith(ComfyNodeABC):
Returns True if the string ends with the specified suffix, otherwise False.
Optional start and end parameters allow you to check only a specific portion of the string.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -261,7 +281,7 @@ class StringEndswith(ComfyNodeABC):
"optional": {
"start": (IO.INT, {"default": 0, "min": 0, "tooltip": "Start position of the checked range."}),
"end": (IO.INT, {"default": 0, "min": 0, "tooltip": "End position of the checked range (0 means the end)."}),
}
},
}
RETURN_TYPES = (IO.BOOLEAN,)
@@ -287,6 +307,7 @@ class StringExpandtabs(ComfyNodeABC):
The tab size (number of spaces per tab) can be adjusted,
with a default value of 8 characters.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -295,7 +316,7 @@ class StringExpandtabs(ComfyNodeABC):
},
"optional": {
"tabsize": (IO.INT, {"default": 8, "min": 1, "max": 100, "tooltip": "Number of spaces each tab expands to."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -317,6 +338,7 @@ class StringFind(ComfyNodeABC):
Returns the lowest index where the substring is found. If the substring is not found, it returns -1.
Optional start and end parameters allow you to limit the search to a specific portion of the string.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -327,7 +349,7 @@ class StringFind(ComfyNodeABC):
"optional": {
"start": (IO.INT, {"default": 0, "min": 0, "tooltip": "Start position of the search range."}),
"end": (IO.INT, {"default": 0, "min": 0, "tooltip": "End position of the search range (0 means the end)."}),
}
},
}
RETURN_TYPES = (IO.INT,)
@@ -354,11 +376,15 @@ class StringFormatMap(ComfyNodeABC):
mapping keys to values. For example, the string "Hello, {name}" with a dictionary {"name": "World"}
would produce "Hello, World".
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"template": (IO.STRING, {"default": "Hello, {key}", "tooltip": "Format string with {placeholder} fields referencing mapping keys."}),
"template": (
IO.STRING,
{"default": "Hello, {key}", "tooltip": "Format string with {placeholder} fields referencing mapping keys."},
),
"mapping": ("DICT", {"default": {}, "tooltip": "Dictionary providing the values for the placeholders."}),
}
}
@@ -389,6 +415,7 @@ class StringIn(ComfyNodeABC):
exists within the input string. Returns True if the substring is found,
otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -416,6 +443,7 @@ class StringIsAlnum(ComfyNodeABC):
This node returns True if all characters in the string are alphanumeric
(letters or numbers) and there is at least one character, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -442,6 +470,7 @@ class StringIsAlpha(ComfyNodeABC):
This node returns True if all characters in the string are alphabetic
(letters) and there is at least one character, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -468,6 +497,7 @@ class StringIsAscii(ComfyNodeABC):
This node returns True if all characters in the string are in the ASCII character
set and there is at least one character, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -496,6 +526,7 @@ class StringIsDecimal(ComfyNodeABC):
Decimal characters include digit characters and characters that can be
used to form decimal numbers in various locales.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -522,6 +553,7 @@ class StringIsDigit(ComfyNodeABC):
This node returns True if all characters in the string are digits
and there is at least one character, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -549,6 +581,7 @@ class StringIsIdentifier(ComfyNodeABC):
otherwise False. A valid identifier must start with a letter or underscore
and can only contain letters, digits, or underscores.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -575,6 +608,7 @@ class StringIsLower(ComfyNodeABC):
This node returns True if all cased characters in the string are lowercase
and there is at least one cased character, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -602,6 +636,7 @@ class StringIsNumeric(ComfyNodeABC):
and there is at least one character, otherwise False. Numeric characters include
digit characters and characters that have the Unicode numeric value property.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -628,6 +663,7 @@ class StringIsPrintable(ComfyNodeABC):
This node returns True if all characters in the string are printable or the string is empty,
otherwise False. Printable characters are those which are not control characters.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -654,6 +690,7 @@ class StringIsSpace(ComfyNodeABC):
This node returns True if all characters in the string are whitespace
and there is at least one character, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -681,6 +718,7 @@ class StringIsTitle(ComfyNodeABC):
character, otherwise False. A titlecased string has all words start with an
uppercase character and continue with lowercase.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -707,6 +745,7 @@ class StringIsUpper(ComfyNodeABC):
This node returns True if all cased characters in the string are uppercase
and there is at least one cased character, otherwise False.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -733,6 +772,7 @@ class StringLength(ComfyNodeABC):
This node calculates and returns the number of characters in the input string.
It works the same way as Python's built-in len() function for strings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -760,6 +800,7 @@ class StringDataListJoin(ComfyNodeABC):
separator string between each element. The separator is inserted between the strings,
not at the beginning or end of the result.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -778,7 +819,7 @@ class StringDataListJoin(ComfyNodeABC):
INPUT_IS_LIST = True # The "strings" input accepts a data list
def join(self, sep, strings):
separator = sep[0] # everything comes as a list, so sep is list[str]
separator = sep[0] # everything comes as a list, so sep is list[str]
strings = strings if strings is not None else []
return (separator.join(strings),)
@@ -791,6 +832,7 @@ class StringListJoin(ComfyNodeABC):
separator string between each element. The separator is inserted between the strings,
not at the beginning or end of the result.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -819,6 +861,7 @@ class StringLjust(ComfyNodeABC):
This node returns a string left-aligned in a field of the specified width.
If fillchar is provided, it is used as the padding character instead of a space.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -828,7 +871,7 @@ class StringLjust(ComfyNodeABC):
},
"optional": {
"fillchar": (IO.STRING, {"default": " ", "tooltip": "Padding character (only its first character is used)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -851,6 +894,7 @@ class StringLower(ComfyNodeABC):
This node returns a copy of the string with all uppercase characters converted to lowercase.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -878,6 +922,7 @@ class StringLstrip(ComfyNodeABC):
If chars is provided, it specifies the set of characters to be removed.
If chars is not provided, whitespace characters are removed.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -886,7 +931,7 @@ class StringLstrip(ComfyNodeABC):
},
"optional": {
"chars": (IO.STRING, {"default": "", "tooltip": "Set of characters to remove from the start (whitespace when empty)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -909,6 +954,7 @@ class StringRemoveprefix(ComfyNodeABC):
This node returns a copy of the string with the specified prefix removed
if the string starts with that prefix, otherwise returns the original string.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -936,6 +982,7 @@ class StringRemovesuffix(ComfyNodeABC):
This node returns a copy of the string with the specified suffix removed
if the string ends with that suffix, otherwise returns the original string.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -964,6 +1011,7 @@ class StringUnescape(ComfyNodeABC):
'\t' to tabs, '\\' to backslashes, etc. Useful for processing strings where escape sequences
are represented literally rather than interpreted.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -982,13 +1030,12 @@ class StringUnescape(ComfyNodeABC):
def unescape(self, string):
# Decode escaped sequences only for control characters
result = (
string
.replace(r'\\', '\u0000')
.replace(r'\n', '\n')
.replace(r'\t', '\t')
.replace(r'\"', '"')
.replace(r'\'', "'")
.replace('\u0000', '\\')
string.replace(r"\\", "\u0000")
.replace(r"\n", "\n")
.replace(r"\t", "\t")
.replace(r"\"", '"')
.replace(r"\'", "'")
.replace("\u0000", "\\")
)
return (result,)
@@ -1001,6 +1048,7 @@ class StringEscape(ComfyNodeABC):
representation (like '\n', '\t', '\"', '\\'). Useful when you need to prepare strings
for formats that require escaped sequences instead of literal special characters.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1017,14 +1065,7 @@ class StringEscape(ComfyNodeABC):
FUNCTION = "escape"
def escape(self, string):
result = (
string
.replace('\\', r'\\')
.replace('\n', r'\n')
.replace('\t', r'\t')
.replace('"', r'\"')
.replace("'", r"\'")
)
result = string.replace("\\", r"\\").replace("\n", r"\n").replace("\t", r"\t").replace('"', r"\"").replace("'", r"\'")
return (result,)
@@ -1036,6 +1077,7 @@ class StringReplace(ComfyNodeABC):
old replaced by new. If the optional argument count is given, only the
first count occurrences are replaced.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1046,7 +1088,7 @@ class StringReplace(ComfyNodeABC):
},
"optional": {
"count": (IO.INT, {"default": -1, "min": -1, "tooltip": "Maximum number of replacements (-1 replaces all)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -1068,6 +1110,7 @@ class StringRfind(ComfyNodeABC):
Returns the highest index where the substring is found. If the substring is not found, it returns -1.
Optional start and end parameters allow you to limit the search to a specific portion of the string.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1078,7 +1121,7 @@ class StringRfind(ComfyNodeABC):
"optional": {
"start": (IO.INT, {"default": 0, "min": 0, "tooltip": "Start position of the search range."}),
"end": (IO.INT, {"default": 0, "min": 0, "tooltip": "End position of the search range (0 means the end)."}),
}
},
}
RETURN_TYPES = (IO.INT,)
@@ -1103,6 +1146,7 @@ class StringRjust(ComfyNodeABC):
This node returns a string right-aligned in a field of the specified width.
If fillchar is provided, it is used as the padding character instead of a space.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1112,7 +1156,7 @@ class StringRjust(ComfyNodeABC):
},
"optional": {
"fillchar": (IO.STRING, {"default": " ", "tooltip": "Padding character (only its first character is used)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -1138,6 +1182,7 @@ class StringRsplitDataList(ComfyNodeABC):
If the separator is not specified or is None, any whitespace string is a separator.
Creates a data list of strings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1147,7 +1192,7 @@ class StringRsplitDataList(ComfyNodeABC):
"optional": {
"sep": (IO.STRING, {"default": "", "tooltip": "Separator to split on (whitespace when empty)."}),
"maxsplit": (IO.INT, {"default": -1, "min": -1, "tooltip": "Maximum number of splits from the right (-1 = no limit)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -1173,6 +1218,7 @@ class StringRsplitList(ComfyNodeABC):
If the separator is not specified or is None, any whitespace string is a separator.
Creates a LIST of strings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1182,7 +1228,7 @@ class StringRsplitList(ComfyNodeABC):
"optional": {
"sep": (IO.STRING, {"default": "", "tooltip": "Separator to split on (whitespace when empty)."}),
"maxsplit": (IO.INT, {"default": -1, "min": -1, "tooltip": "Maximum number of splits from the right (-1 = no limit)."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -1206,6 +1252,7 @@ class StringRstrip(ComfyNodeABC):
If chars is provided, it specifies the set of characters to be removed.
If chars is not provided, whitespace characters are removed.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1214,7 +1261,7 @@ class StringRstrip(ComfyNodeABC):
},
"optional": {
"chars": (IO.STRING, {"default": "", "tooltip": "Set of characters to remove from the end (whitespace when empty)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -1241,6 +1288,7 @@ class StringSplitDataList(ComfyNodeABC):
If the separator is not specified or is None, any whitespace string is a separator.
Creates a data list of strings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1250,7 +1298,7 @@ class StringSplitDataList(ComfyNodeABC):
"optional": {
"sep": (IO.STRING, {"default": "", "tooltip": "Separator to split on (whitespace when empty)."}),
"maxsplit": (IO.INT, {"default": -1, "min": -1, "tooltip": "Maximum number of splits (-1 = no limit)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -1281,6 +1329,7 @@ class StringSplitList(ComfyNodeABC):
If the separator is not specified or is None, any whitespace string is a separator.
Creates a LIST of strings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1290,7 +1339,7 @@ class StringSplitList(ComfyNodeABC):
"optional": {
"sep": (IO.STRING, {"default": "", "tooltip": "Separator to split on (whitespace when empty)."}),
"maxsplit": (IO.INT, {"default": -1, "min": -1, "tooltip": "Maximum number of splits (-1 = no limit)."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -1317,6 +1366,7 @@ class StringSplitlinesDataList(ComfyNodeABC):
If keepends is True, line breaks are included in the resulting list.
Creates a data list of strings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1325,7 +1375,7 @@ class StringSplitlinesDataList(ComfyNodeABC):
},
"optional": {
"keepends": (IO.BOOLEAN, {"default": False, "tooltip": "Keep the line break characters in the resulting lines."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -1349,6 +1399,7 @@ class StringSplitlinesList(ComfyNodeABC):
If keepends is True, line breaks are included in the resulting list.
Creates a LIST of strings.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1357,7 +1408,7 @@ class StringSplitlinesList(ComfyNodeABC):
},
"optional": {
"keepends": (IO.BOOLEAN, {"default": False, "tooltip": "Keep the line break characters in the resulting lines."}),
}
},
}
RETURN_TYPES = ("LIST",)
@@ -1380,6 +1431,7 @@ class StringStartswith(ComfyNodeABC):
Returns True if the string starts with the specified prefix, otherwise False.
Optional start and end parameters allow you to check only a specific portion of the string.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1390,7 +1442,7 @@ class StringStartswith(ComfyNodeABC):
"optional": {
"start": (IO.INT, {"default": 0, "min": 0, "tooltip": "Start position of the checked range."}),
"end": (IO.INT, {"default": 0, "min": 0, "tooltip": "End position of the checked range (0 means the end)."}),
}
},
}
RETURN_TYPES = (IO.BOOLEAN,)
@@ -1416,6 +1468,7 @@ class StringStrip(ComfyNodeABC):
If chars is provided, it specifies the set of characters to be removed.
If chars is not provided, whitespace characters are removed.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1424,7 +1477,7 @@ class StringStrip(ComfyNodeABC):
},
"optional": {
"chars": (IO.STRING, {"default": "", "tooltip": "Set of characters to remove from both ends (whitespace when empty)."}),
}
},
}
RETURN_TYPES = (IO.STRING,)
@@ -1447,6 +1500,7 @@ class StringSwapcase(ComfyNodeABC):
This node returns a copy of the string with uppercase characters converted to lowercase
and lowercase characters converted to uppercase.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1473,6 +1527,7 @@ class StringTitle(ComfyNodeABC):
This node returns a titlecased version of the string where words start with an uppercase
character and the remaining characters are lowercase.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1498,6 +1553,7 @@ class StringUpper(ComfyNodeABC):
This node returns a copy of the string with all lowercase characters converted to uppercase.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -1525,6 +1581,7 @@ class StringZfill(ComfyNodeABC):
of length width. A leading sign prefix ('+'/'-') is handled by inserting the padding
after the sign character rather than before.
"""
@classmethod
def INPUT_TYPES(cls):
return {
+59 -12
View File
@@ -12,6 +12,7 @@ from typing import Any
try:
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
except:
class IO:
BOOLEAN = "BOOLEAN"
INT = "INT"
@@ -19,13 +20,16 @@ except:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
class TensorCreate(ComfyNodeABC):
"""
Creates a PyTorch tensor from various input types.
Can accept numbers, lists, or existing tensors.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -49,17 +53,22 @@ class TensorCreate(ComfyNodeABC):
except Exception as e:
raise ValueError(f"Failed to create tensor from {type(input)}: {str(e)}")
class TensorBinaryOp(ComfyNodeABC):
"""
Performs binary operations between two tensors or a tensor and a scalar.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": (IO.ANY, {"tooltip": "Left operand: a tensor, number or list."}),
"b": (IO.ANY, {"tooltip": "Right operand: a tensor, number or list."}),
"operation": (["add", "subtract", "multiply", "divide", "power", "remainder", "floor_divide"], {"default": "add", "tooltip": "Element-wise operation to apply."}),
"operation": (
["add", "subtract", "multiply", "divide", "power", "remainder", "floor_divide"],
{"default": "add", "tooltip": "Element-wise operation to apply."},
),
}
}
@@ -91,16 +100,21 @@ class TensorBinaryOp(ComfyNodeABC):
else:
raise ValueError(f"Unknown operation: {operation}")
class TensorUnaryOp(ComfyNodeABC):
"""
Performs unary operations on a tensor.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": (IO.ANY, {"tooltip": "A tensor, number or list to transform."}),
"operation": (["abs", "neg", "exp", "log", "sin", "cos", "sqrt", "sigmoid", "relu"], {"default": "abs", "tooltip": "Element-wise unary operation to apply."}),
"operation": (
["abs", "neg", "exp", "log", "sin", "cos", "sqrt", "sigmoid", "relu"],
{"default": "abs", "tooltip": "Element-wise unary operation to apply."},
),
}
}
@@ -135,16 +149,21 @@ class TensorUnaryOp(ComfyNodeABC):
else:
raise ValueError(f"Unknown operation: {operation}")
class TensorSlice(ComfyNodeABC):
"""
Slices a tensor using a slice string (e.g., ':, 0:10, 5').
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"tensor": (IO.ANY, {"tooltip": "The tensor to slice."}),
"slice_str": (IO.STRING, {"default": ":", "tooltip": "Python-style slice per dimension, comma-separated, e.g. ':, 0:10, 5'."}),
"slice_str": (
IO.STRING,
{"default": ":", "tooltip": "Python-style slice per dimension, comma-separated, e.g. ':, 0:10, 5'."},
),
}
}
@@ -164,14 +183,14 @@ class TensorSlice(ComfyNodeABC):
# We'll use eval in a restricted way or manual parsing.
# Manual parsing is safer.
def parse_slice(s):
parts = s.split(':')
parts = s.split(":")
if len(parts) == 1:
return int(parts[0])
return slice(*(int(p) if p.strip() else None for p in parts))
try:
dims = [d.strip() for d in slice_str.split(',')]
indices = tuple(parse_slice(d) if ':' in d or d.isdigit() or (d.startswith('-') and d[1:].isdigit()) else d for d in dims)
dims = [d.strip() for d in slice_str.split(",")]
indices = tuple(parse_slice(d) if ":" in d or d.isdigit() or (d.startswith("-") and d[1:].isdigit()) else d for d in dims)
# Re-index with parsed slices
# Note: this is a simplification. torch supports more complex indexing.
# But for basic usage, this covers most cases.
@@ -179,16 +198,24 @@ class TensorSlice(ComfyNodeABC):
except Exception as e:
raise ValueError(f"Failed to slice tensor with '{slice_str}': {str(e)}")
class TensorReshape(ComfyNodeABC):
"""
Reshapes a tensor.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"tensor": (IO.ANY, {"tooltip": "The tensor to reshape."}),
"shape": (IO.STRING, {"default": "-1", "tooltip": "Comma-separated target dimensions; -1 infers that dimension automatically, e.g. '2, -1'."}),
"shape": (
IO.STRING,
{
"default": "-1",
"tooltip": "Comma-separated target dimensions; -1 infers that dimension automatically, e.g. '2, -1'.",
},
),
}
}
@@ -204,21 +231,26 @@ class TensorReshape(ComfyNodeABC):
tensor = torch.tensor(tensor)
try:
shape_tuple = tuple(int(s.strip()) for s in shape.split(','))
shape_tuple = tuple(int(s.strip()) for s in shape.split(","))
return (tensor.reshape(shape_tuple),)
except Exception as e:
raise ValueError(f"Failed to reshape tensor to {shape}: {str(e)}")
class TensorPermute(ComfyNodeABC):
"""
Permutes tensor dimensions.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"tensor": (IO.ANY, {"tooltip": "The tensor whose dimensions are reordered."}),
"dims": (IO.STRING, {"default": "0, 1", "tooltip": "New order of the dimensions as comma-separated indices, e.g. '0, 2, 1'."}),
"dims": (
IO.STRING,
{"default": "0, 1", "tooltip": "New order of the dimensions as comma-separated indices, e.g. '0, 2, 1'."},
),
}
}
@@ -234,15 +266,17 @@ class TensorPermute(ComfyNodeABC):
tensor = torch.tensor(tensor)
try:
dims_tuple = tuple(int(d.strip()) for d in dims.split(','))
dims_tuple = tuple(int(d.strip()) for d in dims.split(","))
return (tensor.permute(dims_tuple),)
except Exception as e:
raise ValueError(f"Failed to permute tensor with dims {dims}: {str(e)}")
class TensorJoin(ComfyNodeABC):
"""
Joins multiple tensors (concatenate or stack).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -250,7 +284,13 @@ class TensorJoin(ComfyNodeABC):
"tensor1": (IO.ANY, {"tooltip": "First tensor to join."}),
"tensor2": (IO.ANY, {"tooltip": "Second tensor to join."}),
"dim": (IO.INT, {"default": 0, "tooltip": "Dimension along which the tensors are joined."}),
"mode": (["concatenate", "stack"], {"default": "concatenate", "tooltip": "'concatenate' joins along an existing dimension; 'stack' inserts a new dimension."}),
"mode": (
["concatenate", "stack"],
{
"default": "concatenate",
"tooltip": "'concatenate' joins along an existing dimension; 'stack' inserts a new dimension.",
},
),
}
}
@@ -270,10 +310,12 @@ class TensorJoin(ComfyNodeABC):
else:
return (torch.stack([t1, t2], dim=dim),)
class TensorInfo(ComfyNodeABC):
"""
Returns information about a tensor.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -284,7 +326,11 @@ class TensorInfo(ComfyNodeABC):
RETURN_TYPES = (IO.ANY, IO.STRING, IO.STRING)
RETURN_NAMES = ("shape", "dtype", "device")
OUTPUT_TOOLTIPS = ("Tensor shape as a list of dimension sizes.", "Data type of the tensor, e.g. torch.float32.", "Device the tensor lives on, e.g. cuda:0 or cpu.")
OUTPUT_TOOLTIPS = (
"Tensor shape as a list of dimension sizes.",
"Data type of the tensor, e.g. torch.float32.",
"Device the tensor lives on, e.g. cuda:0 or cpu.",
)
CATEGORY = "Basic/tensor"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "get_info"
@@ -295,6 +341,7 @@ class TensorInfo(ComfyNodeABC):
return (list(tensor.shape), str(tensor.dtype), str(tensor.device))
NODE_CLASS_MAPPINGS = {
"TensorCreate": TensorCreate,
"TensorBinaryOp": TensorBinaryOp,
+41 -17
View File
@@ -13,6 +13,7 @@ except ImportError:
STRING = "STRING"
NUMBER = "FLOAT,INT"
ANY = "*"
ComfyNodeABC = object
# Add custom IO types for DATETIME and TIMEDELTA
@@ -25,13 +26,10 @@ class TimeNow(ComfyNodeABC):
Returns the current time and date as a DATETIME object.
Note: Output changes for every run, providing a fresh timestamp each time.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"trigger": (IO.ANY, {"tooltip": "Optional input to trigger a fresh evaluation."})
}
}
return {"optional": {"trigger": (IO.ANY, {"tooltip": "Optional input to trigger a fresh evaluation."})}}
RETURN_TYPES = (IO.DATETIME,)
RETURN_NAMES = ("now",)
@@ -58,13 +56,10 @@ class TimeNowUTC(ComfyNodeABC):
Returns the current time and date as a DATETIME object, in the UTC timezone.
Note: Output changes for every run, providing a fresh timestamp each time.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"trigger": (IO.ANY, {"tooltip": "Optional input to trigger a fresh evaluation."})
}
}
return {"optional": {"trigger": (IO.ANY, {"tooltip": "Optional input to trigger a fresh evaluation."})}}
RETURN_TYPES = (IO.DATETIME,)
RETURN_NAMES = ("now",)
@@ -90,6 +85,7 @@ class TimeToUnix(ComfyNodeABC):
"""
Converts a DATETIME object to a Unix timestamp (a float representing seconds since the epoch).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -116,6 +112,7 @@ class UnixToTime(ComfyNodeABC):
"""
Converts a Unix timestamp (float or int) to a DATETIME object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -143,6 +140,7 @@ class TimeFormat(ComfyNodeABC):
Formats a DATETIME object into a string using a specified format code.
Common format codes: %Y (year), %m (month), %d (day), %H (hour), %M (minute), %S (second).
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -170,12 +168,16 @@ class TimeParse(ComfyNodeABC):
"""
Parses a string containing a date and time into a DATETIME object, using a specified format code.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"time_string": (IO.STRING, {"tooltip": "The date/time string to parse."}),
"format_string": (IO.STRING, {"default": "%Y-%m-%d %H:%M:%S", "tooltip": "strptime format code matching the input string, e.g. %Y-%m-%d %H:%M:%S."}),
"format_string": (
IO.STRING,
{"default": "%Y-%m-%d %H:%M:%S", "tooltip": "strptime format code matching the input string, e.g. %Y-%m-%d %H:%M:%S."},
),
}
}
@@ -197,6 +199,7 @@ class TimeDelta(ComfyNodeABC):
"""
Creates a TIMEDELTA object, which represents a duration and can be used for date calculations.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -229,6 +232,7 @@ class TimeAddDelta(ComfyNodeABC):
"""
Adds a TIMEDELTA (duration) to a DATETIME object, resulting in a new DATETIME.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -256,6 +260,7 @@ class TimeSubtractDelta(ComfyNodeABC):
"""
Subtracts a TIMEDELTA (duration) from a DATETIME object, resulting in a new DATETIME.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -283,6 +288,7 @@ class TimeDifference(ComfyNodeABC):
"""
Calculates the difference between two DATETIME objects, returning a TIMEDELTA object.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -311,15 +317,23 @@ class TimeExtract(ComfyNodeABC):
Extracts individual components (year, month, day, hour, etc.) from a DATETIME object.
Weekday is returned as an integer, where Monday is 0 and Sunday is 6.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": { "datetime": (IO.DATETIME, {"tooltip": "The DATETIME to decompose."}) }
}
return {"required": {"datetime": (IO.DATETIME, {"tooltip": "The DATETIME to decompose."})}}
RETURN_TYPES = (IO.INT, IO.INT, IO.INT, IO.INT, IO.INT, IO.INT, IO.INT, IO.INT)
RETURN_NAMES = ("year", "month", "day", "hour", "minute", "second", "microsecond", "weekday")
OUTPUT_TOOLTIPS = ("Year.", "Month (1-12).", "Day of the month.", "Hour (0-23).", "Minute (0-59).", "Second (0-59).", "Microsecond.", "Weekday (Monday = 0 .. Sunday = 6).")
OUTPUT_TOOLTIPS = (
"Year.",
"Month (1-12).",
"Day of the month.",
"Hour (0-23).",
"Minute (0-59).",
"Second (0-59).",
"Microsecond.",
"Weekday (Monday = 0 .. Sunday = 6).",
)
CATEGORY = "Basic/time"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "extract"
@@ -328,8 +342,16 @@ class TimeExtract(ComfyNodeABC):
"""
Extracts all time components from a datetime object.
"""
return (datetime.year, datetime.month, datetime.day, datetime.hour,
datetime.minute, datetime.second, datetime.microsecond, datetime.weekday())
return (
datetime.year,
datetime.month,
datetime.day,
datetime.hour,
datetime.minute,
datetime.second,
datetime.microsecond,
datetime.weekday(),
)
class TimeDeltaToSeconds(ComfyNodeABC):
@@ -337,6 +359,7 @@ class TimeDeltaToSeconds(ComfyNodeABC):
Converts a TIMEDELTA object to a float representing its duration in seconds.
A value of 1.0 corresponds to a time delta of one second.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -364,6 +387,7 @@ class TimeDeltaToMilliseconds(ComfyNodeABC):
Converts a TIMEDELTA object to an integer representing its duration in milliseconds.
A value of 1 corresponds to a time delta of one millisecond, rounded to the nearest millisecond.
"""
@classmethod
def INPUT_TYPES(cls):
return {
+3 -3
View File
@@ -1,10 +1,10 @@
#import os
#import sys
# import os
# import sys
# from unittest.mock import MagicMock
#
# # Add the project root directory to Python path
# # This allows the tests to import the project
#sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
#
# print("=======================================================================")
# print("Mocking 'comfy.comfy_types.node_typing' via conftest.py")
+1 -1
View File
@@ -1,4 +1,4 @@
#import pytest
# import pytest
from src.basic_data_handling.boolean_nodes import (
BooleanAnd,
+1 -2
View File
@@ -1,6 +1,5 @@
import pytest
from src.basic_data_handling.casting_nodes import (CastToString, CastToInt, CastToFloat, CastToBoolean,
CastToList, CastToSet, CastToDict)
from src.basic_data_handling.casting_nodes import CastToString, CastToInt, CastToFloat, CastToBoolean, CastToList, CastToSet, CastToDict
def test_cast_to_string():
+43 -12
View File
@@ -1,6 +1,14 @@
#import pytest
# import pytest
from src.basic_data_handling.control_flow_nodes import (
IfElse, SwitchCase, IfElifElse, ContinueFlow, ContinueIfNotEmpty, FlowSelect, ForceCalculation, ExecutionOrder, IsConnected
IfElse,
SwitchCase,
IfElifElse,
ContinueFlow,
ContinueIfNotEmpty,
FlowSelect,
ForceCalculation,
ExecutionOrder,
IsConnected,
)
@@ -88,9 +96,21 @@ def test_if_elif_else():
# if is False, and one elif that is False, else
assert node.execute(**{"if": False, "then": "if_val", "elif_0": False, "then_0": "elif0_val", "else": "else_val"}) == ("else_val",)
# if is False, multiple elifs, one is true
assert node.execute(**{"if": False, "then": "if_val", "elif_0": False, "then_0": "elif0_val", "elif_1": True, "then_1": "elif1_val", "else": "else_val"}) == ("elif1_val",)
assert node.execute(
**{"if": False, "then": "if_val", "elif_0": False, "then_0": "elif0_val", "elif_1": True, "then_1": "elif1_val", "else": "else_val"}
) == ("elif1_val",)
# all false, else
assert node.execute(**{"if": False, "then": "if_val", "elif_0": False, "then_0": "elif0_val", "elif_1": False, "then_1": "elif1_val", "else": "else_val"}) == ("else_val",)
assert node.execute(
**{
"if": False,
"then": "if_val",
"elif_0": False,
"then_0": "elif0_val",
"elif_1": False,
"then_1": "elif1_val",
"else": "else_val",
}
) == ("else_val",)
# all false, no else
assert node.execute(**{"if": False, "then": "if_val", "elif_0": False, "then_0": "elif0_val"}) == (None,)
# Missing then for a true condition
@@ -136,13 +156,24 @@ def test_if_elif_else_lazy_status():
# if is False, elif_0 False, elif_1 False, else needs evaluation but is already fulfilled
assert node.check_lazy_status(**{"if": False, "then": "if_val", "elif_0": False, "elif_1": False, "else": "else_val"}) == []
# if is False, elif_0 False, elif_1 is True, then_1 is needed
assert node.check_lazy_status(**{"if": False, "then": "if_val", "elif_0": False, "elif_1": True, "then_1": None, "else": None}) == ["then_1"]
assert node.check_lazy_status(**{"if": False, "then": "if_val", "elif_0": False, "elif_1": True, "then_1": None, "else": None}) == [
"then_1"
]
# if is False, elif_0 False, elif_1 is True, then_1 is needed
assert node.check_lazy_status(**{"if": False, "then": "if_val", "elif_0": False, "elif_1": True, "then_1": None, "else": "else_val"}) == ["then_1"]
assert node.check_lazy_status(
**{"if": False, "then": "if_val", "elif_0": False, "elif_1": True, "then_1": None, "else": "else_val"}
) == ["then_1"]
# if is False, elif_0 False, elif_1 is True, then_1 is not needed, but else is needed
assert node.check_lazy_status(**{"if": False, "then": "if_val", "elif_0": False, "elif_1": True, "then_1": "elif1_val", "else": None}) == ["else"]
assert node.check_lazy_status(
**{"if": False, "then": "if_val", "elif_0": False, "elif_1": True, "then_1": "elif1_val", "else": None}
) == ["else"]
# if is False, elif_0 False, elif_1 is True, then_1 is not needed, but else is needed
assert node.check_lazy_status(**{"if": False, "then": "if_val", "elif_0": False, "elif_1": True, "then_1": "elif1_val", "else": "else_val"}) == []
assert (
node.check_lazy_status(
**{"if": False, "then": "if_val", "elif_0": False, "elif_1": True, "then_1": "elif1_val", "else": "else_val"}
)
== []
)
# all false, else is needed
assert node.check_lazy_status(**{"if": False, "then": "if_val", "elif_0": False, "then_0": "elif1_val", "else": None}) == ["else"]
# all false, else is not needed
@@ -204,9 +235,9 @@ def test_flow_select():
res = node.select(42, select=True)
assert res[0] == 42
assert res[1] is None or "ExecutionBlocker" in str(res[1])
res = node.select({'a': 1}, select=False)
res = node.select({"a": 1}, select=False)
assert res[0] is None or "ExecutionBlocker" in str(res[0])
assert res[1] == {'a': 1}
assert res[1] == {"a": 1}
def test_force_calculation():
@@ -219,13 +250,13 @@ def test_force_calculation():
def test_execution_order():
node = ExecutionOrder()
# with "any node output"
assert node.execute(**{'any node output': "passthrough_val"}) == (None, "passthrough_val")
assert node.execute(**{"any node output": "passthrough_val"}) == (None, "passthrough_val")
# without "any node output"
assert node.execute() == (None, [])
# with other kwargs
assert node.execute(other_kwarg="some_val") == (None, [])
# with multiple kwargs
assert node.execute(**{'any node output': "passthrough_val", "E/O": "ignored"}) == (None, "passthrough_val")
assert node.execute(**{"any node output": "passthrough_val", "E/O": "ignored"}) == (None, "passthrough_val")
def test_is_connected():
+4 -6
View File
@@ -279,12 +279,10 @@ def test_create_from_string():
def test_list_create():
node = DataListListCreate()
# Testing with string values
assert (node.create_list(item_0=["hello", "world"], item_1=["bye", "bye!"], item_2="") ==
([["hello", "world"], ["bye", "bye!"]],))
assert node.create_list(item_0=["hello", "world"], item_1=["bye", "bye!"], item_2="") == ([["hello", "world"], ["bye", "bye!"]],)
# Testing with mixed values
assert (node.create_list(item_0=[123, 456], item_1=[True, False], item_2="") ==
([[123, 456], [True, False]],))
assert node.create_list(item_0=[123, 456], item_1=[True, False], item_2="") == ([[123, 456], [True, False]],)
# Testing with empty list
assert node.create_list(item_0="") == ([],)
@@ -370,9 +368,9 @@ def test_any():
def test_enumerate():
node = DataListEnumerate()
# Basic enumeration starting from 0
assert node.enumerate_list(list=['a', 'b', 'c']) == ([[0, 'a'], [1, 'b'], [2, 'c']],)
assert node.enumerate_list(list=["a", "b", "c"]) == ([[0, "a"], [1, "b"], [2, "c"]],)
# Custom start index
assert node.enumerate_list(list=['x', 'y', 'z'], start=[10]) == ([[10, 'x'], [11, 'y'], [12, 'z']],)
assert node.enumerate_list(list=["x", "y", "z"], start=[10]) == ([[10, "x"], [11, "y"], [12, "z"]],)
# Empty list
assert node.enumerate_list(list=[]) == ([],)
+32 -21
View File
@@ -49,11 +49,12 @@ def test_dict_create():
@pytest.mark.parametrize(
"in_dict, key, default, expected, message", [
"in_dict, key, default, expected, message",
[
(_dict_x3, "key1", None, "value1", "existing key"),
(_dict_x3, "non_existent", None, None, "missing key, no default"),
(_dict_x3, "key99", "default_value", "default_value", "missing key with default"),
]
],
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_get(dict_type, in_dict, key, default, expected, message):
@@ -63,11 +64,12 @@ def test_dict_get(dict_type, in_dict, key, default, expected, message):
@pytest.mark.parametrize(
"in_dict, key, value, expected, message", [
"in_dict, key, value, expected, message",
[
(_dict_x1, "key2", "value2", _dict_x2, "base case"),
(_dict_x1, "key1", "new_value", {"key1": "new_value"}, "overwriting existing key"),
({}, "key", "value", {"key": "value"}, "empty dict"),
]
],
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_set(dict_type, in_dict, key, value, expected, message):
@@ -77,6 +79,7 @@ def test_dict_set(dict_type, in_dict, key, value, expected, message):
assert result == (dict_type(expected),), f"Wrong result: {message}"
assert type(result[0]) == dict_type, f"Wrong type: {message}"
def test_dict_create_from_boolean():
node = DictCreateFromBoolean()
# Test with dynamic inputs
@@ -223,12 +226,13 @@ def test_dict_from_keys():
@pytest.mark.parametrize(
"in_dict, key, default, out_dict, pop_value, message", [
"in_dict, key, default, out_dict, pop_value, message",
[
(_dict_x2, "key1", None, _dict_b, "value1", "base case"),
(_dict_x2, "non_existent", "default", _dict_x2, "default", "non-existent key (with default)"),
({"a": 1}, "b", None, {"a": 1}, None, "non-existent key (no default)"),
({}, "key", None, {}, None, "empty dict"),
]
],
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_pop(dict_type, in_dict, key, default, out_dict, pop_value, message):
@@ -255,11 +259,12 @@ def test_dict_pop_item(dict_type):
@pytest.mark.parametrize(
"in_dict, key, default, out_dict, out_value, message", [
"in_dict, key, default, out_dict, out_value, message",
[
(_dict_x1, "key2", "default", {"key1": "value1", "key2": "default"}, "default", "key that doesn't exist"),
(_dict_x1, "key1", "new_default", _dict_x1, "value1", "key that already exists"),
({}, "key", "value", {"key": "value"}, "value", "empty dict"),
]
],
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_set_default(dict_type, in_dict, key, default, out_dict, out_value, message):
@@ -271,12 +276,13 @@ def test_dict_set_default(dict_type, in_dict, key, default, out_dict, out_value,
@pytest.mark.parametrize(
"in_dict, update_dict, expected, message", [
"in_dict, update_dict, expected, message",
[
(_dict_x1, _dict_b, _dict_x2, "base case"),
({"a": 1, "b": 2}, {"b": 3, "c": 4}, {"a": 1, "b": 3, "c": 4}, "overlapping keys"),
(_dict_x1, {}, _dict_x1, "empty update dict"),
({}, _dict_b, _dict_b, "empty original dict"),
]
],
)
@pytest.mark.parametrize("update_type", _tested_dict_types)
@pytest.mark.parametrize("in_type", _tested_dict_types)
@@ -291,12 +297,13 @@ def test_dict_update(in_type, update_type, in_dict, update_dict, expected, messa
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_length(dict_type, num):
node = DictLength()
my_dict = dict_type({f"key{x}": f"value{x}" for x in range(1, num+1)})
my_dict = dict_type({f"key{x}": f"value{x}" for x in range(1, num + 1)})
assert node.length(my_dict) == (num,)
@pytest.mark.parametrize(
"dict_a, other_dicts, expected, message", [
"dict_a, other_dicts, expected, message",
[
(_dict_x1, [_dict_b], _dict_x2, "basic merge"),
({"a": 1}, [{"a": 2}], {"a": 2}, "overlapping keys (later dicts override earlier ones)"),
({"a": 1}, [{"b": 2}, {"c": 3}], {"a": 1, "b": 2, "c": 3}, "more than two dicts"),
@@ -306,7 +313,7 @@ def test_dict_length(dict_type, num):
(_dict_x1, [{}, {}, {}], _dict_x1, "empty dict - v4"),
({}, [_dict_x1], _dict_x1, "empty dict - v5"),
({}, [_dict_x1, _dict_b], _dict_x2, "empty first dict"),
]
],
)
@pytest.mark.parametrize("type_b", _tested_dict_types)
@pytest.mark.parametrize("type_a", _tested_dict_types)
@@ -332,11 +339,12 @@ def test_dict_get_keys_values(dict_type):
@pytest.mark.parametrize(
"in_dict, key, expected, success, message", [
"in_dict, key, expected, success, message",
[
(_dict_x2, "key1", _dict_b, True, "successful removal"),
(_dict_x2, "non_existent", _dict_x2, False, "removal of non-existent key"),
({}, "any_key", {}, False, "empty dict"),
]
],
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_remove(dict_type, in_dict, key, expected, success, message):
@@ -348,13 +356,14 @@ def test_dict_remove(dict_type, in_dict, key, expected, success, message):
@pytest.mark.parametrize(
"in_dict, keys, expected, message", [
"in_dict, keys, expected, message",
[
(_dict_x3, ["key1", "key3"], {"key1": "value1", "key3": "value3"}, "subset of keys"),
(_dict_x3, ["key1", "non_existent"], _dict_x1, "non-existent keys"),
(_dict_x3, [], {}, "empty keys list"),
(_dict_x3, list(_dict_x3.keys()), _dict_x3, "all keys"),
({}, ["any_key"], {}, "empty dict"),
]
],
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_filter_by_keys(dict_type, in_dict, keys, expected, message):
@@ -366,13 +375,14 @@ def test_dict_filter_by_keys(dict_type, in_dict, keys, expected, message):
@pytest.mark.parametrize(
"in_dict, keys, expected, message", [
"in_dict, keys, expected, message",
[
(_dict_x3, ["key1", "key3"], _dict_b, "excluding some keys"),
(_dict_x3, list(_dict_x3.keys()), {}, "excluding all keys"),
(_dict_x3, ["non_existent"], _dict_x3, "excluding non-existent keys"),
(_dict_x3, [], _dict_x3, "empty exclude list"),
({}, ["any_key"], {}, "empty dict"),
]
],
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_exclude_keys(dict_type, in_dict, keys, expected, message):
@@ -400,12 +410,13 @@ def test_dict_get_multiple(dict_type):
@pytest.mark.parametrize(
"in_dict, out_dict, success, message", [
"in_dict, out_dict, success, message",
[
(_dict_x2, {"value1": "key1", "value2": "key2"}, True, "basic inversion"),
({"key1": "value", "key2": "value"}, {"value": "key2"}, True, "duplicated values - last key wins"),
({}, {}, True, "empty dict"),
# TODO: `False` success
]
],
)
@pytest.mark.parametrize("dict_type", _tested_dict_types)
def test_dict_invert(dict_type, in_dict, out_dict, success, message):
+23 -21
View File
@@ -4,38 +4,40 @@ from src.basic_data_handling._dynamic_input import ContainsDynamicDict
def test_contains_dynamic_dict_basic_lookup():
d = ContainsDynamicDict({
'value': ('x', {'_dynamic': 'number'}),
'fixed': 'y',
})
d = ContainsDynamicDict(
{
"value": ("x", {"_dynamic": "number"}),
"fixed": "y",
}
)
# direct behavior
assert 'value' in d
assert d['value'] == ('x', {'_dynamic': 'number'})
assert "value" in d
assert d["value"] == ("x", {"_dynamic": "number"})
# dynamic numeric key lookup
assert 'value1' in d
assert d['value1'] == ('x', {'_dynamic': 'number'})
assert 'value999' in d
assert d['value999'] == ('x', {'_dynamic': 'number'})
assert "value1" in d
assert d["value1"] == ("x", {"_dynamic": "number"})
assert "value999" in d
assert d["value999"] == ("x", {"_dynamic": "number"})
# non-dynamic key and fallback
assert 'fixed' in d
assert d['fixed'] == 'y'
assert "fixed" in d
assert d["fixed"] == "y"
# non-matching key should not be present
assert 'novalue' not in d
assert "novalue" not in d
with pytest.raises(KeyError):
_ = d['novalue']
_ = d["novalue"]
def test_contains_dynamic_dict_partial_prefix_not_numeric():
d = ContainsDynamicDict({'val': ('z', {'_dynamic': 'number'})})
d = ContainsDynamicDict({"val": ("z", {"_dynamic": "number"})})
assert 'val' in d
assert d['val'] == ('z', {'_dynamic': 'number'})
assert 'val1' in d
assert d['val1'] == ('z', {'_dynamic': 'number'})
assert 'valx' not in d
assert "val" in d
assert d["val"] == ("z", {"_dynamic": "number"})
assert "val1" in d
assert d["val1"] == ("z", {"_dynamic": "number"})
assert "valx" not in d
with pytest.raises(KeyError):
_ = d['valx']
_ = d["valx"]
+3 -2
View File
@@ -15,6 +15,7 @@ from src.basic_data_handling.float_nodes import (
FloatFromHex,
)
def test_float_create():
node = FloatCreate()
assert node.create("3.14") == (3.14,)
@@ -58,8 +59,8 @@ def test_float_divide_safe():
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'),)
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)
+1 -1
View File
@@ -261,7 +261,7 @@ def test_list_create_from_string():
def test_list_create_from_json_string():
node = ListCreateFromJSONString()
# Multiline JSON array
assert node.create_from_json('[\n 1,\n 2,\n 3\n]') == ([1, 2, 3],)
assert node.create_from_json("[\n 1,\n 2,\n 3\n]") == ([1, 2, 3],)
# Empty array
assert node.create_from_json("[]") == ([],)
# Mix of JSON types
+27 -16
View File
@@ -3,6 +3,7 @@ import math
from math import sin, cos
from src.basic_data_handling.math_formula_node import MathFormula
def test_basic_formula_evaluation():
"""Test basic formula evaluation with simple operations."""
node = MathFormula()
@@ -24,23 +25,24 @@ def test_basic_formula_evaluation():
assert node.evaluate("-a", a=5)[0] == pytest.approx(-5.0)
assert node.evaluate("a * -b", a=3, b=4)[0] == pytest.approx(-12.0)
def test_function_calls():
"""Test formula evaluation with math functions."""
node = MathFormula()
# Single-argument functions
assert node.evaluate("sin(a)", a=math.pi/6)[0] == pytest.approx(0.5)
assert node.evaluate("sin(a)", a=math.pi / 6)[0] == pytest.approx(0.5)
assert node.evaluate("cos(a)", a=0)[0] == pytest.approx(1.0)
# Unary minus with function calls
assert node.evaluate("sin(-a)", a=math.pi/6)[0] == pytest.approx(-0.5)
assert node.evaluate("-sin(a)", a=math.pi/6)[0] == pytest.approx(-0.5)
assert node.evaluate("-sin(-a)", a=math.pi/6)[0] == pytest.approx(0.5)
assert node.evaluate("sin(-a)", a=math.pi / 6)[0] == pytest.approx(-0.5)
assert node.evaluate("-sin(a)", a=math.pi / 6)[0] == pytest.approx(-0.5)
assert node.evaluate("-sin(-a)", a=math.pi / 6)[0] == pytest.approx(0.5)
# Unary minus with nested function calls
assert node.evaluate("sin(cos(-a))", a=math.pi)[0] == pytest.approx(sin(cos(-math.pi)))
assert node.evaluate("-sin(cos(-a))", a=math.pi)[0] == pytest.approx(-sin(cos(-math.pi)))
assert node.evaluate("tan(a)", a=math.pi/4)[0] == pytest.approx(1.0)
assert node.evaluate("tan(a)", a=math.pi / 4)[0] == pytest.approx(1.0)
assert node.evaluate("sqrt(a)", a=16)[0] == pytest.approx(4.0)
assert node.evaluate("log(a)", a=math.e)[0] == pytest.approx(1.0)
assert node.evaluate("exp(a)", a=2)[0] == pytest.approx(math.exp(2))
@@ -50,6 +52,7 @@ def test_function_calls():
assert node.evaluate("max(a, b)", a=5, b=10)[0] == pytest.approx(10.0)
assert node.evaluate("pow(a, b)", a=2, b=3)[0] == pytest.approx(8.0)
def test_constants():
"""Test formula evaluation with constants."""
node = MathFormula()
@@ -60,15 +63,16 @@ def test_constants():
# Using constants in expressions
assert node.evaluate("2 * pi()")[0] == pytest.approx(2 * math.pi)
assert node.evaluate("e() ** 2")[0] == pytest.approx(math.e ** 2)
assert node.evaluate("e() ** 2")[0] == pytest.approx(math.e**2)
assert node.evaluate("sin(pi() / 2)")[0] == pytest.approx(1.0)
assert node.evaluate("log(e())")[0] == pytest.approx(1.0)
# More complex expressions with constants
assert node.evaluate("a * pi() + b", a=2, b=1)[0] == pytest.approx(2 * math.pi + 1)
assert node.evaluate("e() ** a", a=3)[0] == pytest.approx(math.e ** 3)
assert node.evaluate("e() ** a", a=3)[0] == pytest.approx(math.e**3)
assert node.evaluate("sin(a * pi())", a=0.5)[0] == pytest.approx(1.0, abs=1e-10)
def test_variable_constant_distinction():
"""Test that variables and constants can coexist without conflict."""
node = MathFormula()
@@ -84,6 +88,7 @@ def test_variable_constant_distinction():
assert node.evaluate("e * log(e())", e=2)[0] == pytest.approx(2.0)
assert node.evaluate("a * e() + b * e", a=2, b=3, e=4)[0] == pytest.approx(2 * math.e + 3 * 4)
def test_unary_minus():
"""Test that unary minus (negation) is handled correctly in all contexts."""
node = MathFormula()
@@ -97,7 +102,7 @@ def test_unary_minus():
assert node.evaluate("-e()")[0] == pytest.approx(-math.e)
# Function result negation
assert node.evaluate("-sin(a)", a=math.pi/2)[0] == pytest.approx(-1.0)
assert node.evaluate("-sin(a)", a=math.pi / 2)[0] == pytest.approx(-1.0)
assert node.evaluate("-sqrt(a)", a=4)[0] == pytest.approx(-2.0)
# Nested unary minus
@@ -126,8 +131,9 @@ def test_unary_minus():
assert node.evaluate("(-a - b)", a=5, b=3)[0] == pytest.approx(-8.0)
# Unary minus with constants and functions in complex expressions
assert node.evaluate("-pi() * sin(-a)", a=math.pi/2)[0] == pytest.approx(math.pi)
assert node.evaluate("-e() ** -2")[0] == pytest.approx(1/(math.e**2)) # see documentation about this special case
assert node.evaluate("-pi() * sin(-a)", a=math.pi / 2)[0] == pytest.approx(math.pi)
assert node.evaluate("-e() ** -2")[0] == pytest.approx(1 / (math.e**2)) # see documentation about this special case
def test_error_conditions():
"""Test error conditions in formula evaluation."""
@@ -176,6 +182,7 @@ def test_error_conditions():
with pytest.raises(ValueError):
node.evaluate("asin(x)", x=2) # Out of domain [-1, 1]
def test_complex_expressions():
"""Test evaluation of complex mathematical expressions."""
node = MathFormula()
@@ -183,7 +190,7 @@ def test_complex_expressions():
# Complex expression with variables, functions, and constants
formula = "a * sin(b * pi()) + c * sqrt(d) + e() ** 2"
result = node.evaluate(formula, a=2, b=0.5, c=3, d=9)[0]
expected = 2 * math.sin(0.5 * math.pi) + 3 * math.sqrt(9) + math.e ** 2
expected = 2 * math.sin(0.5 * math.pi) + 3 * math.sqrt(9) + math.e**2
assert result == pytest.approx(expected)
# Expression with nested functions
@@ -193,13 +200,13 @@ def test_complex_expressions():
# Expression with multiple operations and precedence
formula = "a + b * c - d / e + f ** g"
result = node.evaluate(formula, a=1, b=2, c=3, d=8, e=4, f=2, g=3)[0]
expected = 1 + 2 * 3 - 8 / 4 + 2 ** 3
expected = 1 + 2 * 3 - 8 / 4 + 2**3
assert result == pytest.approx(expected)
# Complex expressions with unary minus
formula = "a + -b * c - -(d / e) + -f ** g"
result = node.evaluate(formula, a=1, b=2, c=3, d=8, e=4, f=2, g=3)[0]
expected = 1 + (-2) * 3 - (-(8 / 4)) + (-(2 ** 3))
expected = 1 + (-2) * 3 - (-(8 / 4)) + (-(2**3))
assert result == pytest.approx(expected)
# Expression with multiple unary minuses and parentheses
@@ -208,6 +215,7 @@ def test_complex_expressions():
expected = -2 * (5 + (-3)) / (-4)
assert result == pytest.approx(expected)
def test_formula_with_e_variable():
"""Test formulas with 'e' as a variable to ensure it doesn't clash with the constant."""
node = MathFormula()
@@ -230,6 +238,7 @@ def test_formula_with_e_variable():
expected = 2 * math.sin(math.e * math.pi)
assert result == pytest.approx(expected)
def test_whitespace_handling():
"""Test that the parser correctly handles different whitespace patterns."""
node = MathFormula()
@@ -249,11 +258,12 @@ def test_whitespace_handling():
assert node.evaluate("- (a + b)", a=2, b=3)[0] == pytest.approx(-5.0)
# Mixed spacing around functions
assert node.evaluate("sin( a )+cos(b)", a=math.pi/2, b=0)[0] == pytest.approx(2.0)
assert node.evaluate("sin( a )+cos(b)", a=math.pi / 2, b=0)[0] == pytest.approx(2.0)
# Spacing around constants
assert node.evaluate("2*pi( )", a=1)[0] == pytest.approx(2 * math.pi)
assert node.evaluate("e( )**2")[0] == pytest.approx(math.e ** 2)
assert node.evaluate("e( )**2")[0] == pytest.approx(math.e**2)
def test_tokenizing():
"""Test the tokenization functionality."""
@@ -289,6 +299,7 @@ def test_tokenizing():
tokens = node.tokenize_formula("a + b * (c - d) / e() ** 2")
assert all(token in tokens for token in ["a", "+", "b", "*", "(", "c", "-", "d", ")", "/", "e", "(", ")", "**", "2"])
def test_infix_to_postfix():
"""Test the infix to postfix conversion."""
node = MathFormula()
@@ -305,7 +316,7 @@ def test_infix_to_postfix():
# Test function calls
formula = "sin(a) + cos(b)"
assert node.evaluate(formula, a=math.pi/2, b=0)[0] == pytest.approx(2.0)
assert node.evaluate(formula, a=math.pi / 2, b=0)[0] == pytest.approx(2.0)
# Test constant function calls
formula = "pi() + e()"
+21 -4
View File
@@ -1,9 +1,26 @@
import pytest
import math
from src.basic_data_handling.math_nodes import (
MathSin, MathCos, MathTan, MathAsin, MathAcos, MathAtan, MathAtan2,
MathSqrt, MathExp, MathLog, MathLog10, MathDegrees, MathRadians,
MathFloor, MathCeil, MathAbs, MathPi, MathE, MathMin, MathMax,
MathSin,
MathCos,
MathTan,
MathAsin,
MathAcos,
MathAtan,
MathAtan2,
MathSqrt,
MathExp,
MathLog,
MathLog10,
MathDegrees,
MathRadians,
MathFloor,
MathCeil,
MathAbs,
MathPi,
MathE,
MathMin,
MathMax,
)
@@ -41,7 +58,7 @@ def test_math_tan():
with pytest.raises(ValueError): # Undefined tangent
node.calculate(90, "degrees")
with pytest.raises(ValueError): # Undefined tangent
node.calculate(math.pi/2, "radians")
node.calculate(math.pi / 2, "radians")
def test_math_asin():
+57 -39
View File
@@ -6,12 +6,36 @@ import torch
from PIL import Image
from src.basic_data_handling.path_nodes import (
PathJoin, PathAbspath, PathExists, PathIsFile, PathIsDir, PathGetSize,
PathSplit, PathSplitExt, PathBasename, PathDirname, PathGetExtension,
PathSetExtension, PathNormalize, PathRelative, PathGlob, PathExpandVars, PathGetCwd,
PathListDir, PathIsAbsolute, PathCommonPrefix, PathLoadStringFile, PathSaveStringFile,
PathLoadImageRGB, PathSaveImageRGB, PathLoadImageRGBA, PathSaveImageRGBA,
PathLoadMaskFromAlpha, PathLoadMaskFromGreyscale, PathInputDir, PathOutputDir,
PathJoin,
PathAbspath,
PathExists,
PathIsFile,
PathIsDir,
PathGetSize,
PathSplit,
PathSplitExt,
PathBasename,
PathDirname,
PathGetExtension,
PathSetExtension,
PathNormalize,
PathRelative,
PathGlob,
PathExpandVars,
PathGetCwd,
PathListDir,
PathIsAbsolute,
PathCommonPrefix,
PathLoadStringFile,
PathSaveStringFile,
PathLoadImageRGB,
PathSaveImageRGB,
PathLoadImageRGBA,
PathSaveImageRGBA,
PathLoadMaskFromAlpha,
PathLoadMaskFromGreyscale,
PathInputDir,
PathOutputDir,
)
@@ -37,6 +61,7 @@ def test_path_join():
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_load_save_string_file(tmp_path):
# Test saving a string to a file
save_node = PathSaveStringFile()
@@ -68,6 +93,7 @@ def test_path_load_save_string_file(tmp_path):
# Test error handling
assert load_node.load_text(str(tmp_path / "nonexistent.txt")) == ("", False)
def test_path_save_string_file_append(tmp_path):
# Test appending to an existing file
save_node = PathSaveStringFile()
@@ -130,7 +156,7 @@ def test_path_is_file(tmp_path):
# Test with special paths
assert node.check_is_file(".") == (False,)
# Test with symlink to file if platform supports
if hasattr(os, 'symlink'):
if hasattr(os, "symlink"):
try:
symlink = tmp_path / "symlink.txt"
os.symlink(str(file), str(symlink))
@@ -158,7 +184,7 @@ def test_path_is_dir(tmp_path):
nested_dir.mkdir()
assert node.check_is_dir(str(nested_dir)) == (True,)
# Test with symlink to directory if platform supports
if hasattr(os, 'symlink'):
if hasattr(os, "symlink"):
try:
symlink = tmp_path / "symlink_dir"
os.symlink(str(directory), str(symlink), target_is_directory=True)
@@ -265,6 +291,7 @@ def test_path_get_extension():
# Test with empty string
assert node.get_extension("") == ("",)
def test_path_set_extension():
node = PathSetExtension()
# Test basic extension replacement
@@ -288,12 +315,12 @@ def test_path_load_mask_nodes(tmp_path, monkeypatch):
img_size = (64, 64)
# Image with alpha channel
rgba_img = Image.new('RGBA', img_size, color=(255, 0, 0, 128)) # Semi-transparent red
rgba_img = Image.new("RGBA", img_size, color=(255, 0, 0, 128)) # Semi-transparent red
rgba_path = str(tmp_path / "alpha_mask.png")
rgba_img.save(rgba_path)
# Grayscale image
gray_img = Image.new('L', img_size)
gray_img = Image.new("L", img_size)
# Create a gradient from black to white
for y in range(img_size[1]):
for x in range(img_size[0]):
@@ -308,17 +335,17 @@ def test_path_load_mask_nodes(tmp_path, monkeypatch):
return Image.open(path)
def mock_extract_mask_from_alpha(img):
if 'A' in img.getbands():
alpha = np.array(img.getchannel('A')).astype(np.float32) / 255.0
if "A" in img.getbands():
alpha = np.array(img.getchannel("A")).astype(np.float32) / 255.0
mask_tensor = 1.0 - torch.from_numpy(alpha)
return mask_tensor.unsqueeze(0)
return torch.zeros((1, img.height, img.width), dtype=torch.float32)
def mock_extract_mask_from_greyscale(img):
if img.mode == 'L':
if img.mode == "L":
gray = np.array(img).astype(np.float32) / 255.0
else:
gray = np.array(img.getchannel('R')).astype(np.float32) / 255.0
gray = np.array(img.getchannel("R")).astype(np.float32) / 255.0
mask_tensor = 1.0 - torch.from_numpy(gray)
return mask_tensor.unsqueeze(0)
@@ -358,7 +385,7 @@ def test_path_load_mask_nodes(tmp_path, monkeypatch):
def test_path_load_save_image_rgba(tmp_path, monkeypatch):
# Create a test image with transparency
img_size = (64, 64)
test_img = Image.new('RGBA', img_size, color=(255, 0, 0, 128)) # Semi-transparent red
test_img = Image.new("RGBA", img_size, color=(255, 0, 0, 128)) # Semi-transparent red
img_path = str(tmp_path / "test_rgba.png")
test_img.save(img_path)
@@ -369,8 +396,8 @@ def test_path_load_save_image_rgba(tmp_path, monkeypatch):
return Image.open(path)
def mock_extract_mask_from_alpha(img):
if 'A' in img.getbands():
alpha = np.array(img.getchannel('A')).astype(np.float32) / 255.0
if "A" in img.getbands():
alpha = np.array(img.getchannel("A")).astype(np.float32) / 255.0
mask_tensor = 1.0 - torch.from_numpy(alpha)
return mask_tensor.unsqueeze(0)
return torch.zeros((1, img.height, img.width), dtype=torch.float32)
@@ -386,7 +413,7 @@ def test_path_load_save_image_rgba(tmp_path, monkeypatch):
assert isinstance(loaded_img, torch.Tensor)
assert isinstance(loaded_mask, torch.Tensor)
assert loaded_img.shape == (1, img_size[1], img_size[0], 3) # (batch, height, width, channels)
assert loaded_mask.shape == (1, img_size[1], img_size[0]) # (batch, height, width)
assert loaded_mask.shape == (1, img_size[1], img_size[0]) # (batch, height, width)
# Test saving an image with mask
save_node = PathSaveImageRGBA()
@@ -420,7 +447,7 @@ def test_path_load_save_image_rgba(tmp_path, monkeypatch):
def test_path_load_save_image_rgb(tmp_path, monkeypatch):
# Create a test image
img_size = (64, 64)
test_img = Image.new('RGB', img_size, color='red')
test_img = Image.new("RGB", img_size, color="red")
img_path = str(tmp_path / "test_rgb.png")
test_img.save(img_path)
@@ -485,8 +512,7 @@ def test_path_save_image_rgb_prompt_metadata(tmp_path):
# Both prompt and negative prompt provided
full_path = str(tmp_path / "full")
assert save_node.save_image(red_img, full_path, prompt="a red square",
negative_prompt="blurry, low quality") == (True,)
assert save_node.save_image(red_img, full_path, prompt="a red square", negative_prompt="blurry, low quality") == (True,)
with Image.open(full_path + ".png") as img:
img.load()
assert img.text.get("parameters") == "a red square\nNegative prompt: blurry, low quality"
@@ -507,8 +533,7 @@ def test_path_save_image_rgb_prompt_metadata(tmp_path):
# JPEG embeds the prompt into the EXIF UserComment and ImageDescription fields
jpg_path = str(tmp_path / "jpeg_meta")
assert save_node.save_image(red_img, jpg_path, format="jpg", prompt="a red square",
negative_prompt="blurry, low quality") == (True,)
assert save_node.save_image(red_img, jpg_path, format="jpg", prompt="a red square", negative_prompt="blurry, low quality") == (True,)
assert os.path.exists(jpg_path + ".jpg")
with Image.open(jpg_path + ".jpg") as img:
img.load()
@@ -519,8 +544,7 @@ def test_path_save_image_rgb_prompt_metadata(tmp_path):
# WEBP embeds the prompt into the EXIF UserComment field (no ImageDescription)
webp_path = str(tmp_path / "webp_meta")
assert save_node.save_image(red_img, webp_path, format="webp", prompt="a red square",
negative_prompt="blurry, low quality") == (True,)
assert save_node.save_image(red_img, webp_path, format="webp", prompt="a red square", negative_prompt="blurry, low quality") == (True,)
with Image.open(webp_path + ".webp") as img:
img.load()
exif_bytes = img.info.get("exif", b"")
@@ -548,9 +572,7 @@ def test_path_save_image_rgba_prompt_metadata(tmp_path):
# Both prompt and negative prompt provided
full_path = str(tmp_path / "full")
assert save_node.save_image_with_mask(red_img, mask, full_path,
prompt="a red square",
negative_prompt="blurry, low quality") == (True,)
assert save_node.save_image_with_mask(red_img, mask, full_path, prompt="a red square", negative_prompt="blurry, low quality") == (True,)
with Image.open(full_path + ".png") as img:
img.load()
assert img.text.get("parameters") == "a red square\nNegative prompt: blurry, low quality"
@@ -566,8 +588,7 @@ def test_path_save_image_jxl_prompt_metadata(tmp_path):
prompt = "a red square <lora:x:1.0>"
negative_prompt = "blurry, low quality"
jxl_path = str(tmp_path / "jxl_meta")
assert save_node.save_image(red_img, jxl_path, format="jxl", prompt=prompt,
negative_prompt=negative_prompt) == (True,)
assert save_node.save_image(red_img, jxl_path, format="jxl", prompt=prompt, negative_prompt=negative_prompt) == (True,)
assert os.path.exists(jxl_path + ".jxl")
# JXL embeds the prompt into the EXIF UserComment field
@@ -643,9 +664,7 @@ def test_path_save_image_formats_are_library_derived():
import src.basic_data_handling.path_nodes as pn
# Non-photographic raster ids are never surfaced.
assert not any(
t in pn._NON_PHOTOGRAPHIC_RASTER_IDS for t in map(str.upper, pn._IMAGE_SAVE_FORMATS)
)
assert not any(t in pn._NON_PHOTOGRAPHIC_RASTER_IDS for t in map(str.upper, pn._IMAGE_SAVE_FORMATS))
save_node = PathSaveImageRGB()
img = _batch_rgb(1)
@@ -719,6 +738,7 @@ def test_path_save_image_prefix_mode_date_token(tmp_path, monkeypatch):
"""``%date:yyyy-MM-dd%`` is expanded to the real date folder (not a literal
``%date:...%`` directory) in prefix mode."""
from datetime import datetime
monkeypatch.setattr("src.basic_data_handling.path_nodes.get_output_directory", lambda: str(tmp_path))
save_node = PathSaveImageRGB()
img = _batch_rgb(1)
@@ -734,6 +754,7 @@ def test_path_save_image_prefix_mode_date_token(tmp_path, monkeypatch):
def test_path_save_image_path_mode_date_token(tmp_path):
"""``%date:...%`` templates also work in the plain path mode."""
from datetime import datetime
save_node = PathSaveImageRGB()
img = _batch_rgb(1)
today = datetime.now().strftime("%Y-%m-%d")
@@ -848,11 +869,9 @@ def test_path_glob(tmp_path):
# 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)
])
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
@@ -878,7 +897,6 @@ def test_path_input_output_dir(monkeypatch):
assert PathOutputDir().execute() == ("./",)
def test_path_expand_vars(monkeypatch):
node = PathExpandVars()
monkeypatch.setenv("TEST_VAR", "test_value")
+3 -2
View File
@@ -1,4 +1,4 @@
#import pytest
# import pytest
from src.basic_data_handling.regex_nodes import (
RegexSearchGroupsDataList,
RegexGroupDict,
@@ -8,9 +8,10 @@ from src.basic_data_handling.regex_nodes import (
RegexTest,
RegexFindallList,
RegexSearchGroupsList,
RegexSplitList
RegexSplitList,
)
def test_regex_search_groups():
node = RegexSearchGroupsDataList()
assert node.search_groups(r"(foo)(bar)", "foobar") == (["foo", "bar"],)
+2 -1
View File
@@ -1,4 +1,4 @@
#import pytest
# import pytest
from src.basic_data_handling.set_nodes import (
SetAdd,
SetAll,
@@ -27,6 +27,7 @@ from src.basic_data_handling.set_nodes import (
SetUnion,
)
def test_set_create():
node = SetCreate()
# Testing with kwargs to simulate dynamic inputs
+49 -3
View File
@@ -1,4 +1,4 @@
#import pytest
# import pytest
from src.basic_data_handling.string_nodes import (
StringCapitalize,
@@ -53,18 +53,21 @@ from src.basic_data_handling.string_nodes import (
StringZfill,
)
def test_capitalize():
node = StringCapitalize()
assert node.capitalize("example") == ("Example",)
assert node.capitalize("") == ("",) # Empty string case
assert node.capitalize("HELLO") == ("Hello",) # Already uppercase
def test_casefold():
node = StringCasefold()
assert node.casefold("Germany") == ("germany",)
assert node.casefold("FOO") == ("foo",)
assert node.casefold("ß") == ("ss",) # Special case folding
def test_center():
node = StringCenter()
# Centering with spaces
@@ -78,12 +81,14 @@ def test_center():
# Multiple character fillchar (should use only first character)
assert node.center("test", 10, "ab") == ("aaatestaaa",)
def test_concat():
node = StringConcat()
assert node.concat("hello ", "world") == ("hello world",)
assert node.concat("", "") == ("",) # Empty strings
assert node.concat("123", "456") == ("123456",) # Numbers as strings
def test_count():
node = StringCount()
assert node.count("banana", "a") == (3,)
@@ -93,6 +98,7 @@ def test_count():
assert node.count("banana", "a", 2) == (2,) # Count from index 2 to end
assert node.count("banana", "a", 2, 5) == (1,) # Count from index 2 to 5
def test_decode():
node = StringDecode()
assert node.decode("b'test'", "utf-8") == ("test",)
@@ -100,6 +106,7 @@ def test_decode():
# Invalid bytes string format
assert node.decode("test", "utf-8")[0].startswith("Input is not a bytes string")
def test_encode():
node = StringEncode()
assert node.encode("test", "utf-8") == ("b'test'",) # UTF-8 standard
@@ -108,6 +115,7 @@ def test_encode():
# Error case: Unsupported encoding
assert node.encode("äö", "nonexistent-encoding")[0].startswith("Encoding error")
def test_endswith():
node = StringEndswith()
assert node.endswith("endswith function", "function") == (True,)
@@ -115,12 +123,14 @@ def test_endswith():
assert node.endswith("hello world", "hello", 0, 5) == (True,) # With start and end
assert node.endswith("", "") == (True,) # Empty strings
def test_expandtabs():
node = StringExpandtabs()
assert node.expandtabs("hello\tworld") == ("hello world",) # Default tabsize=8
assert node.expandtabs("hello\tworld", 4) == ("hello world",) # Custom tabsize
assert node.expandtabs("no tabs here") == ("no tabs here",) # No tabs
def test_find():
node = StringFind()
assert node.find("hello world", "world") == (6,)
@@ -128,12 +138,14 @@ def test_find():
assert node.find("hello world", "l", 3, 7) == (3,) # With start and end
assert node.find("hello world", "l", 4) == (9,) # With start
def test_in():
node = StringIn()
assert node.contains("hello world", "world") == (True,)
assert node.contains("hello world", "earth") == (False,)
assert node.contains("", "") == (True,) # Empty strings
def test_format_map():
node = StringFormatMap()
assert node.format_map("Hello, {name}!", {"name": "World"}) == ("Hello, World!",)
@@ -142,30 +154,35 @@ def test_format_map():
result = node.format_map("Hello, {missing}!", {})[0]
assert result.startswith("Key error")
def test_isalnum():
node = StringIsAlnum()
assert node.isalnum("abc123") == (True,)
assert node.isalnum("abc 123") == (False,) # Space makes it not alphanumeric
assert node.isalnum("") == (False,) # Empty string
def test_isalpha():
node = StringIsAlpha()
assert node.isalpha("abc") == (True,)
assert node.isalpha("abc123") == (False,) # Contains digits
assert node.isalpha("") == (False,) # Empty string
def test_isascii():
node = StringIsAscii()
assert node.isascii("hello") == (True,)
assert node.isascii("öäü") == (False,) # Non-ASCII characters
assert node.isascii("") == (True,) # Empty string is ASCII
def test_isdecimal():
node = StringIsDecimal()
assert node.isdecimal("123") == (True,)
assert node.isdecimal("123.45") == (False,) # Decimal point makes it not decimal
assert node.isdecimal("") == (False,) # Empty string
def test_isdigit():
node = StringIsDigit()
assert node.isdigit("12345") == (True,)
@@ -173,12 +190,14 @@ def test_isdigit():
assert node.isdigit("") == (False,) # Empty string is not a digit
assert node.isdigit("²³") == (True,) # Superscript digits are digits
def test_isidentifier():
node = StringIsIdentifier()
assert node.isidentifier("valid_var_name") == (True,)
assert node.isidentifier("1invalid") == (False,) # Cannot start with digit
assert node.isidentifier("") == (False,) # Empty string
def test_islower():
node = StringIsLower()
assert node.islower("lowercase") == (True,)
@@ -186,6 +205,7 @@ def test_islower():
assert node.islower("123") == (False,) # No cased characters
assert node.islower("") == (False,) # Empty string
def test_isnumeric():
node = StringIsNumeric()
assert node.isnumeric("123") == (True,)
@@ -193,30 +213,35 @@ def test_isnumeric():
assert node.isnumeric("123abc") == (False,)
assert node.isnumeric("") == (False,) # Empty string
def test_isprintable():
node = StringIsPrintable()
assert node.isprintable("Hello World") == (True,)
assert node.isprintable("Hello\nWorld") == (False,) # Contains non-printable character
assert node.isprintable("") == (True,) # Empty string is printable
def test_isspace():
node = StringIsSpace()
assert node.isspace(" \t\n") == (True,)
assert node.isspace(" text ") == (False,) # Contains non-whitespace
assert node.isspace("") == (False,) # Empty string
def test_istitle():
node = StringIsTitle()
assert node.istitle("Title Case") == (True,)
assert node.istitle("Not title Case") == (False,)
assert node.istitle("") == (False,) # Empty string
def test_isupper():
node = StringIsUpper()
assert node.isupper("UPPERCASE") == (True,)
assert node.isupper("Mixed") == (False,)
assert node.isupper("") == (False,) # Empty string
def test_join():
node = StringDataListJoin()
# join expects a data list as input, so we need to simulate this
@@ -230,18 +255,21 @@ def test_join():
assert node_list.join("", ["a", "b", "c"]) == ("abc",)
assert node_list.join("-", []) == ("",) # Empty list
def test_ljust():
node = StringLjust()
assert node.ljust("test", 10) == ("test ",)
assert node.ljust("test", 10, "*") == ("test******",)
assert node.ljust("test", 2) == ("test",) # Width smaller than string length
def test_lower():
node = StringLower()
assert node.lower("HELLO") == ("hello",)
assert node.lower("Hello World") == ("hello world",)
assert node.lower("") == ("",) # Empty string
def test_lstrip():
node = StringLstrip()
assert node.lstrip(" hello") == ("hello",)
@@ -249,18 +277,21 @@ def test_lstrip():
assert node.lstrip("hello") == ("hello",) # No leading chars to strip
assert node.lstrip("") == ("",) # Empty string
def test_removeprefix():
node = StringRemoveprefix()
assert node.removeprefix("TestPrefix", "Test") == ("Prefix",)
assert node.removeprefix("Prefix", "Test") == ("Prefix",) # Prefix not present
assert node.removeprefix("", "") == ("",) # Empty strings
def test_removesuffix():
node = StringRemovesuffix()
assert node.removesuffix("PrefixTest", "Test") == ("Prefix",)
assert node.removesuffix("Prefix", "Test") == ("Prefix",) # Suffix not present
assert node.removesuffix("", "") == ("",) # Empty strings
def test_replace():
node = StringReplace()
assert node.replace("hello world", "world", "Python") == ("hello Python",)
@@ -268,6 +299,7 @@ def test_replace():
assert node.replace("banana", "a", "o", 2) == ("bonona",) # Limit replacement to 2 occurrences
assert node.replace("no matches", "x", "y") == ("no matches",) # No matches
def test_rfind():
node = StringRfind()
assert node.rfind("hello world hello", "hello") == (12,)
@@ -275,12 +307,14 @@ def test_rfind():
assert node.rfind("hello world hello", "hello", 1, 11) == (-1,) # Within range
assert node.rfind("", "") == (0,) # Empty strings
def test_rjust():
node = StringRjust()
assert node.rjust("test", 10) == (" test",)
assert node.rjust("test", 10, "*") == ("******test",)
assert node.rjust("test", 2) == ("test",) # Width smaller than string length
def test_rsplit():
node = StringRsplitDataList()
assert node.rsplit("apple,banana,cherry", ",") == (["apple", "banana", "cherry"],)
@@ -293,6 +327,7 @@ def test_rsplit():
assert node_list.rsplit("apple,banana,cherry", ",", 1) == (["apple,banana", "cherry"],)
assert node_list.rsplit(" words with spaces ") == (["words", "with", "spaces"],) # Default splits on whitespace
def test_rstrip():
node = StringRstrip()
assert node.rstrip("hello ") == ("hello",)
@@ -300,6 +335,7 @@ def test_rstrip():
assert node.rstrip("hello") == ("hello",) # No trailing chars to strip
assert node.rstrip("") == ("",) # Empty string
def test_split():
node = StringSplitDataList()
assert node.split("apple,banana,cherry", ",") == (["apple", "banana", "cherry"],)
@@ -312,6 +348,7 @@ def test_split():
assert node_list.split("apple,banana,cherry", ",", 1) == (["apple", "banana,cherry"],)
assert node_list.split(" words with spaces ") == (["words", "with", "spaces"],) # Default splits on whitespace
def test_splitlines():
node = StringSplitlinesDataList()
assert node.splitlines("line1\nline2\nline3") == (["line1", "line2", "line3"],)
@@ -326,6 +363,7 @@ def test_splitlines():
assert node_list.splitlines("line1\nline2\nline3", True) == (["line1\n", "line2\n", "line3"],) # Keep line endings
assert node_list.splitlines("no newlines") == (["no newlines"],)
def test_startswith():
node = StringStartswith()
assert node.startswith("function startswith", "function") == (True,)
@@ -333,6 +371,7 @@ def test_startswith():
assert node.startswith("hello world", "world", 6) == (True,) # With start
assert node.startswith("", "") == (True,) # Empty strings
def test_strip():
node = StringStrip()
assert node.strip(" hello ") == ("hello",)
@@ -340,12 +379,14 @@ def test_strip():
assert node.strip("hello") == ("hello",) # No chars to strip
assert node.strip("") == ("",) # Empty string
def test_swapcase():
node = StringSwapcase()
assert node.swapcase("Hello World") == ("hELLO wORLD",)
assert node.swapcase("") == ("",) # Empty string
assert node.swapcase("123") == ("123",) # No cased characters
def test_title():
node = StringTitle()
assert node.title("hello world") == ("Hello World",)
@@ -353,12 +394,14 @@ def test_title():
assert node.title("hello123 world") == ("Hello123 World",)
assert node.title("") == ("",) # Empty string
def test_upper():
node = StringUpper()
assert node.upper("hello") == ("HELLO",)
assert node.upper("Hello World") == ("HELLO WORLD",)
assert node.upper("") == ("",) # Empty string
def test_zfill():
node = StringZfill()
assert node.zfill("123", 5) == ("00123",)
@@ -366,13 +409,14 @@ def test_zfill():
assert node.zfill("123", 2) == ("123",) # Width smaller than string length
assert node.zfill("", 3) == ("000",) # Empty string
def test_unescape():
node = StringUnescape()
# Test control characters
assert node.unescape(r"Hello\nWorld") == ("Hello\nWorld",) # Newline
assert node.unescape(r"Hello\tWorld") == ("Hello\tWorld",) # Tab
assert node.unescape(r"C:\\Program Files\\App") == (r"C:\Program Files\App",) # Backslash
assert node.unescape(r"Quote: \"Text\"") == ("Quote: \"Text\"",) # Double quote
assert node.unescape(r"Quote: \"Text\"") == ('Quote: "Text"',) # Double quote
assert node.unescape(r"Quote: \'Text\'") == ("Quote: 'Text'",) # Single quote
# Test that normal Unicode characters remain unchanged
@@ -382,13 +426,14 @@ def test_unescape():
# Test complex string with multiple escape sequences
assert node.unescape(r"Path: C:\\folder\\file.txt\nLine1\tLine2") == ("Path: C:\\folder\\file.txt\nLine1\tLine2",)
def test_escape():
node = StringEscape()
# Test control characters
assert node.escape("Hello\nWorld") == (r"Hello\nWorld",) # Newline
assert node.escape("Hello\tWorld") == (r"Hello\tWorld",) # Tab
assert node.escape(r"C:\Program Files\App") == (r"C:\\Program Files\\App",) # Backslash
assert node.escape('Quote: "Text"') == (r'Quote: \"Text\"',) # Double quote
assert node.escape('Quote: "Text"') == (r"Quote: \"Text\"",) # Double quote
assert node.escape("Quote: 'Text'") == (r"Quote: \'Text\'",) # Single quote
# Test that normal Unicode characters remain unchanged
@@ -398,6 +443,7 @@ def test_escape():
# Test complex string with multiple special characters
assert node.escape("Path: C:\\folder\\file.txt\nLine1\tLine2") == (r"Path: C:\\folder\\file.txt\nLine1\tLine2",)
def test_length():
node = StringLength()
assert node.length("hello") == (5,)
+18 -4
View File
@@ -1,9 +1,16 @@
import torch
from src.basic_data_handling.tensor_nodes import (
TensorCreate, TensorBinaryOp, TensorUnaryOp, TensorSlice,
TensorReshape, TensorPermute, TensorJoin, TensorInfo
TensorCreate,
TensorBinaryOp,
TensorUnaryOp,
TensorSlice,
TensorReshape,
TensorPermute,
TensorJoin,
TensorInfo,
)
def test_tensor_create():
node = TensorCreate()
# From list
@@ -20,6 +27,7 @@ def test_tensor_create():
result = node.create(t)
assert result[0] is t
def test_tensor_binary_op():
node = TensorBinaryOp()
a = torch.tensor([10.0, 20.0])
@@ -29,7 +37,8 @@ def test_tensor_binary_op():
assert torch.equal(node.operate(a, b, "subtract")[0], a - b)
assert torch.equal(node.operate(a, b, "multiply")[0], a * b)
assert torch.equal(node.operate(a, b, "divide")[0], a / b)
assert torch.equal(node.operate(a, 2.0, "power")[0], a ** 2.0)
assert torch.equal(node.operate(a, 2.0, "power")[0], a**2.0)
def test_tensor_unary_op():
node = TensorUnaryOp()
@@ -39,9 +48,10 @@ def test_tensor_unary_op():
assert torch.equal(node.operate(t, "neg")[0], torch.tensor([1.0, 0.0, -1.0]))
assert torch.equal(node.operate(torch.tensor([0.0]), "sin")[0], torch.tensor([0.0]))
def test_tensor_slice():
node = TensorSlice()
t = torch.arange(10).reshape(2, 5) # [[0,1,2,3,4], [5,6,7,8,9]]
t = torch.arange(10).reshape(2, 5) # [[0,1,2,3,4], [5,6,7,8,9]]
# Simple slice
result = node.slice_tensor(t, "0, 1:3")
@@ -51,6 +61,7 @@ def test_tensor_slice():
result = node.slice_tensor(t, ":, 2")
assert torch.equal(result[0], t[:, 2])
def test_tensor_reshape():
node = TensorReshape()
t = torch.arange(6)
@@ -61,6 +72,7 @@ def test_tensor_reshape():
result = node.reshape(t, "-1, 2")
assert result[0].shape == (3, 2)
def test_tensor_permute():
node = TensorPermute()
t = torch.randn(2, 3, 4)
@@ -68,6 +80,7 @@ def test_tensor_permute():
result = node.permute(t, "2, 0, 1")
assert result[0].shape == (4, 2, 3)
def test_tensor_join():
node = TensorJoin()
t1 = torch.tensor([1, 2])
@@ -82,6 +95,7 @@ def test_tensor_join():
assert result[0].shape == (2, 2)
assert torch.equal(result[0], torch.tensor([[1, 2], [3, 4]]))
def test_tensor_info():
node = TensorInfo()
t = torch.zeros((2, 3), dtype=torch.float32)
+18 -13
View File
@@ -1,4 +1,4 @@
#import pytest
# import pytest
from datetime import datetime, timedelta
from src.basic_data_handling.time_nodes import (
@@ -17,6 +17,7 @@ from src.basic_data_handling.time_nodes import (
TimeDeltaToMilliseconds,
)
def test_time_now():
node = TimeNow()
result = node.get_now()
@@ -30,6 +31,7 @@ def test_time_now():
assert len(result_with_trigger) == 1
assert isinstance(result_with_trigger[0], datetime)
def test_time_now_utc():
node = TimeNowUTC()
result = node.get_now_utc()
@@ -43,6 +45,7 @@ def test_time_now_utc():
assert len(result_with_trigger) == 1
assert isinstance(result_with_trigger[0], datetime)
def test_time_to_unix():
node = TimeToUnix()
test_datetime = datetime(2023, 1, 1, 12, 0, 0)
@@ -55,6 +58,7 @@ def test_time_to_unix():
expected_timestamp = test_datetime.timestamp()
assert result[0] == expected_timestamp
def test_unix_to_time():
node = UnixToTime()
# Test with a specific timestamp (2023-01-01 12:00:00)
@@ -76,6 +80,7 @@ def test_unix_to_time():
assert returned_dt.minute == 0
assert returned_dt.second == 0
def test_time_format():
node = TimeFormat()
test_datetime = datetime(2023, 1, 1, 12, 30, 45)
@@ -92,6 +97,7 @@ def test_time_format():
result = node.format_time(test_datetime, "%H:%M")
assert result == ("12:30",)
def test_time_parse():
node = TimeParse()
@@ -112,6 +118,7 @@ def test_time_parse():
assert parsed_datetime.hour == 12
assert parsed_datetime.minute == 30
def test_time_delta():
node = TimeDelta()
@@ -127,28 +134,21 @@ def test_time_delta():
delta = result[0]
# Convert to seconds for easier comparison
total_seconds = delta.total_seconds()
expected_seconds = 24*60*60 + 2*60*60 + 30*60 # 1 day, 2 hours, 30 minutes
expected_seconds = 24 * 60 * 60 + 2 * 60 * 60 + 30 * 60 # 1 day, 2 hours, 30 minutes
assert total_seconds == expected_seconds
# Test with all parameters
result = node.create_delta(
days=1,
seconds=30,
microseconds=500,
milliseconds=100,
minutes=5,
hours=2,
weeks=1
)
result = node.create_delta(days=1, seconds=30, microseconds=500, milliseconds=100, minutes=5, hours=2, weeks=1)
delta = result[0]
# A week plus a day
assert delta.days == 8
# Convert the remainder to seconds for comparison
remainder_seconds = delta.seconds
expected_remainder = 2*60*60 + 5*60 + 30 + 0.1 # 2 hours, 5 minutes, 30 seconds, 100 milliseconds
expected_remainder = 2 * 60 * 60 + 5 * 60 + 30 + 0.1 # 2 hours, 5 minutes, 30 seconds, 100 milliseconds
assert abs(remainder_seconds - expected_remainder) < 1
assert delta.microseconds == 100500
def test_time_add_delta():
node = TimeAddDelta()
test_datetime = datetime(2023, 1, 1, 12, 0, 0)
@@ -162,6 +162,7 @@ def test_time_add_delta():
expected_datetime = datetime(2023, 1, 2, 14, 30, 0) # 1 day, 2 hours, 30 minutes later
assert result[0] == expected_datetime
def test_time_subtract_delta():
node = TimeSubtractDelta()
test_datetime = datetime(2023, 1, 2, 14, 30, 0)
@@ -175,6 +176,7 @@ def test_time_subtract_delta():
expected_datetime = datetime(2023, 1, 1, 12, 0, 0) # 1 day, 2 hours, 30 minutes earlier
assert result[0] == expected_datetime
def test_time_difference():
node = TimeDifference()
datetime1 = datetime(2023, 1, 2, 14, 30, 0)
@@ -192,6 +194,7 @@ def test_time_difference():
result = node.difference(datetime2, datetime1)
assert result[0] == -expected_delta
def test_time_extract():
node = TimeExtract()
test_datetime = datetime(2023, 1, 2, 14, 30, 45, 123456)
@@ -212,6 +215,7 @@ def test_time_extract():
# January 2, 2023 was a Monday (weekday 0)
assert weekday == 0
def test_time_delta_to_seconds():
node = TimeDeltaToSeconds()
@@ -224,7 +228,7 @@ def test_time_delta_to_seconds():
# Larger durations
result = node.to_seconds(timedelta(days=1, hours=2, minutes=30))
assert result[0] == 24*60*60 + 2*60*60 + 30*60
assert result[0] == 24 * 60 * 60 + 2 * 60 * 60 + 30 * 60
# Sub-second durations remain fractional
result = node.to_seconds(timedelta(milliseconds=500))
@@ -234,6 +238,7 @@ def test_time_delta_to_seconds():
result = node.to_seconds(timedelta(seconds=-2))
assert result[0] == -2.0
def test_time_delta_to_milliseconds():
node = TimeDeltaToMilliseconds()