update code formatting (ruff)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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",)
|
||||
|
||||
@@ -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')."}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,)
|
||||
|
||||
|
||||
|
||||
@@ -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)."},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
@@ -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,)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,4 +1,4 @@
|
||||
#import pytest
|
||||
# import pytest
|
||||
|
||||
from src.basic_data_handling.boolean_nodes import (
|
||||
BooleanAnd,
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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"],)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user