diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2539924..29974f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/src/basic_data_handling/__init__.py b/src/basic_data_handling/__init__.py index ccdbc3b..909b464 100644 --- a/src/basic_data_handling/__init__.py +++ b/src/basic_data_handling/__init__.py @@ -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) diff --git a/src/basic_data_handling/_dynamic_input.py b/src/basic_data_handling/_dynamic_input.py index fce3bc3..87d33e0 100644 --- a/src/basic_data_handling/_dynamic_input.py +++ b/src/basic_data_handling/_dynamic_input.py @@ -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` 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) diff --git a/src/basic_data_handling/boolean_nodes.py b/src/basic_data_handling/boolean_nodes.py index 17773ba..3f54c02 100644 --- a/src/basic_data_handling/boolean_nodes.py +++ b/src/basic_data_handling/boolean_nodes.py @@ -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,) diff --git a/src/basic_data_handling/casting_nodes.py b/src/basic_data_handling/casting_nodes.py index 3e7134a..60134c2 100644 --- a/src/basic_data_handling/casting_nodes.py +++ b/src/basic_data_handling/casting_nodes.py @@ -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",) diff --git a/src/basic_data_handling/comparison_nodes.py b/src/basic_data_handling/comparison_nodes.py index 1dca02e..a1498b8 100644 --- a/src/basic_data_handling/comparison_nodes.py +++ b/src/basic_data_handling/comparison_nodes.py @@ -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')."}), } } diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index 6793ae6..9cff283 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -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_ 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_ 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 { diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 803d3a1..4e8ec06 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -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 = { diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index 1c48746..572ed98 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -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,) diff --git a/src/basic_data_handling/float_nodes.py b/src/basic_data_handling/float_nodes.py index 7d2c1e2..6a33725 100644 --- a/src/basic_data_handling/float_nodes.py +++ b/src/basic_data_handling/float_nodes.py @@ -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)."}, + ), } } diff --git a/src/basic_data_handling/int_nodes.py b/src/basic_data_handling/int_nodes.py index f6a8c40..073f45f 100644 --- a/src/basic_data_handling/int_nodes.py +++ b/src/basic_data_handling/int_nodes.py @@ -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", } - diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index 1550f26..2aeaab4 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -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,) diff --git a/src/basic_data_handling/math_formula_node.py b/src/basic_data_handling/math_formula_node.py index 7afb11a..2d0eeec 100644 --- a/src/basic_data_handling/math_formula_node.py +++ b/src/basic_data_handling/math_formula_node.py @@ -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, } diff --git a/src/basic_data_handling/math_nodes.py b/src/basic_data_handling/math_nodes.py index 60bd12f..c072b27 100644 --- a/src/basic_data_handling/math_nodes.py +++ b/src/basic_data_handling/math_nodes.py @@ -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 { diff --git a/src/basic_data_handling/path_nodes.py b/src/basic_data_handling/path_nodes.py index 230a7e6..d556129 100644 --- a/src/basic_data_handling/path_nodes.py +++ b/src/basic_data_handling/path_nodes.py @@ -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 = ( '\n' '\n' '\n' '\n' - '' + body + '\n' - '\n' - '\n' - '\n' + '' + body + "\n" + "\n" + "\n" + "\n" '' ) 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 {} diff --git a/src/basic_data_handling/regex_nodes.py b/src/basic_data_handling/regex_nodes.py index 91ef4f6..e1f9313 100644 --- a/src/basic_data_handling/regex_nodes.py +++ b/src/basic_data_handling/regex_nodes.py @@ -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 { diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index 9894ba1..f4278db 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -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 { diff --git a/src/basic_data_handling/string_nodes.py b/src/basic_data_handling/string_nodes.py index 1ac9e81..f7b3714 100644 --- a/src/basic_data_handling/string_nodes.py +++ b/src/basic_data_handling/string_nodes.py @@ -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 { diff --git a/src/basic_data_handling/tensor_nodes.py b/src/basic_data_handling/tensor_nodes.py index 1713724..92fce0a 100644 --- a/src/basic_data_handling/tensor_nodes.py +++ b/src/basic_data_handling/tensor_nodes.py @@ -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, diff --git a/src/basic_data_handling/time_nodes.py b/src/basic_data_handling/time_nodes.py index 951b159..e61b8c0 100644 --- a/src/basic_data_handling/time_nodes.py +++ b/src/basic_data_handling/time_nodes.py @@ -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 { diff --git a/tests/conftest.py b/tests/conftest.py index 76b82ee..4d2bbbb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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") diff --git a/tests/test_boolean_nodes.py b/tests/test_boolean_nodes.py index 5ce15b1..bb7a596 100644 --- a/tests/test_boolean_nodes.py +++ b/tests/test_boolean_nodes.py @@ -1,4 +1,4 @@ -#import pytest +# import pytest from src.basic_data_handling.boolean_nodes import ( BooleanAnd, diff --git a/tests/test_casting_nodes.py b/tests/test_casting_nodes.py index 4ff9174..f240965 100644 --- a/tests/test_casting_nodes.py +++ b/tests/test_casting_nodes.py @@ -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(): diff --git a/tests/test_control_flow_nodes.py b/tests/test_control_flow_nodes.py index 8c199a9..d82838c 100644 --- a/tests/test_control_flow_nodes.py +++ b/tests/test_control_flow_nodes.py @@ -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(): diff --git a/tests/test_data_list_nodes.py b/tests/test_data_list_nodes.py index 8306c2c..b64a351 100644 --- a/tests/test_data_list_nodes.py +++ b/tests/test_data_list_nodes.py @@ -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=[]) == ([],) diff --git a/tests/test_dict_nodes.py b/tests/test_dict_nodes.py index 96a155f..655ca9d 100644 --- a/tests/test_dict_nodes.py +++ b/tests/test_dict_nodes.py @@ -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): diff --git a/tests/test_dynamic_input.py b/tests/test_dynamic_input.py index ba7d90c..8385b03 100644 --- a/tests/test_dynamic_input.py +++ b/tests/test_dynamic_input.py @@ -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"] diff --git a/tests/test_float_nodes.py b/tests/test_float_nodes.py index 19bb9d9..4802a56 100644 --- a/tests/test_float_nodes.py +++ b/tests/test_float_nodes.py @@ -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) diff --git a/tests/test_list_nodes.py b/tests/test_list_nodes.py index 2f5b21b..119d652 100644 --- a/tests/test_list_nodes.py +++ b/tests/test_list_nodes.py @@ -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 diff --git a/tests/test_math_formula_node.py b/tests/test_math_formula_node.py index dd58a9d..75aa6ea 100644 --- a/tests/test_math_formula_node.py +++ b/tests/test_math_formula_node.py @@ -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()" diff --git a/tests/test_math_nodes.py b/tests/test_math_nodes.py index 6831baa..fedf9b5 100644 --- a/tests/test_math_nodes.py +++ b/tests/test_math_nodes.py @@ -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(): diff --git a/tests/test_path_nodes.py b/tests/test_path_nodes.py index 7edebeb..89dcd1c 100644 --- a/tests/test_path_nodes.py +++ b/tests/test_path_nodes.py @@ -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 " 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") diff --git a/tests/test_regex_nodes.py b/tests/test_regex_nodes.py index ff35d42..783155f 100644 --- a/tests/test_regex_nodes.py +++ b/tests/test_regex_nodes.py @@ -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"],) diff --git a/tests/test_set_nodes.py b/tests/test_set_nodes.py index 181e41a..13bbef4 100644 --- a/tests/test_set_nodes.py +++ b/tests/test_set_nodes.py @@ -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 diff --git a/tests/test_string_nodes.py b/tests/test_string_nodes.py index f172ab8..d582516 100644 --- a/tests/test_string_nodes.py +++ b/tests/test_string_nodes.py @@ -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,) diff --git a/tests/test_tensor_nodes.py b/tests/test_tensor_nodes.py index c404277..83702fa 100644 --- a/tests/test_tensor_nodes.py +++ b/tests/test_tensor_nodes.py @@ -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) diff --git a/tests/test_time_nodes.py b/tests/test_time_nodes.py index cb403de..5fa67b1 100644 --- a/tests/test_time_nodes.py +++ b/tests/test_time_nodes.py @@ -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()