diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a01b51..57f28aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.8.1] - 2026-09-06 + +- Add optional additional input for image saving to also save the prompt when provided +- Enhance in line documentation + ## [1.8.0] - 2026-08-31 - Time delta nodes can now be converted to seconds (float) and milliseconds (int) diff --git a/pyproject.toml b/pyproject.toml index 82b8ff5..68552ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "basic_data_handling" -version = "1.8.0" +version = "1.8.1" description = """Basic Python functions for manipulating data that every programmer is used to, lightweight with no additional dependencies. Supported data types: diff --git a/src/basic_data_handling/boolean_nodes.py b/src/basic_data_handling/boolean_nodes.py index 49efffd..17773ba 100644 --- a/src/basic_data_handling/boolean_nodes.py +++ b/src/basic_data_handling/boolean_nodes.py @@ -18,20 +18,23 @@ class IO: class BooleanAnd(ComfyNodeABC): """ - Returns the logical AND result of two boolean values. + Returns the logical AND (conjunction) of two boolean values. - This node takes two boolean inputs and returns their logical AND result. + 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 { "required": { - "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), - "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), + "input1": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "First operand."}), + "input2": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "Second operand."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True only when both inputs are True, otherwise False.",) CATEGORY = "Basic/BOOLEAN" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "and_operation" @@ -42,20 +45,22 @@ def and_operation(self, input1: bool, input2: bool) -> tuple[bool]: class BooleanNand(ComfyNodeABC): """ - Returns the logical NAND result of two boolean values. + Returns the logical NAND of two boolean values. - This node takes two boolean inputs and returns their logical NAND result. + Outputs False only when both inputs are True; it is the negation of AND. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), - "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), + "input1": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "First operand."}), + "input2": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "Second operand."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("False only when both inputs are True, otherwise True.",) CATEGORY = "Basic/BOOLEAN" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "nand_operation" @@ -66,20 +71,22 @@ def nand_operation(self, input1: bool, input2: bool) -> tuple[bool]: class BooleanNor(ComfyNodeABC): """ - Returns the logical NOR result of two boolean values. + Returns the logical NOR of two boolean values. - This node takes two boolean inputs and returns their logical NOR result. + Outputs True only when both inputs are False; it is the negation of OR. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), - "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), + "input1": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "First operand."}), + "input2": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "Second operand."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True only when both inputs are False, otherwise False.",) CATEGORY = "Basic/BOOLEAN" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "nor_operation" @@ -90,19 +97,21 @@ def nor_operation(self, input1: bool, input2: bool) -> tuple[bool]: class BooleanNot(ComfyNodeABC): """ - Returns the logical NOT result of a boolean value. + Returns the logical NOT (negation) of a boolean value. - This node takes one boolean input and returns its logical NOT result. + Outputs True when the input is False and False when the input is True. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "input": (IO.BOOLEAN, {"default": False, "forceInput": True}), + "input": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "The value to negate."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the input is False, False when the input is True.",) CATEGORY = "Basic/BOOLEAN" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "not_operation" @@ -113,20 +122,23 @@ def not_operation(self, input: bool) -> tuple[bool]: class BooleanOr(ComfyNodeABC): """ - Returns the logical OR result of two boolean values. + Returns the logical OR (disjunction) of two boolean values. - This node takes two boolean inputs and returns their logical OR result. + 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 { "required": { - "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), - "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), + "input1": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "First operand."}), + "input2": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "Second operand."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when at least one input is True, otherwise False.",) CATEGORY = "Basic/BOOLEAN" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "or_operation" @@ -137,20 +149,22 @@ def or_operation(self, input1: bool, input2: bool) -> tuple[bool]: class BooleanXor(ComfyNodeABC): """ - Returns the logical XOR result of two boolean values. + Returns the logical XOR (exclusive or) of two boolean values. - This node takes two boolean inputs and returns their logical XOR result. + Outputs True when the inputs differ from one another and False when they are equal. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "input1": (IO.BOOLEAN, {"default": False, "forceInput": True}), - "input2": (IO.BOOLEAN, {"default": False, "forceInput": True}), + "input1": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "First operand."}), + "input2": (IO.BOOLEAN, {"default": False, "forceInput": True, "tooltip": "Second operand."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the two inputs differ, False when they are equal.",) CATEGORY = "Basic/BOOLEAN" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "xor_operation" @@ -161,24 +175,26 @@ def xor_operation(self, input1: bool, input2: bool) -> tuple[bool]: class GenericOr(ComfyNodeABC): """ - Returns the logical N/OR result of one or more values. + Returns the OR of any number of values, evaluated with Python truthiness. - This node takes a dynamic number of inputs and returns their logical N/OR result. - Note that values are evaluated according Python's rules. I.e. an empty string is - `false`, an integer 0 is also `false`, etc. + Connect additional values to the dynamic item input to add more operands. Because + 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}), + "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"}), + "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,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when at least one connected value is truthy (unless inverted).",) CATEGORY = "Basic/BOOLEAN" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "or_operation" @@ -189,24 +205,26 @@ def or_operation(self, invert: bool, **kwargs: list[Any]) -> tuple[bool]: class GenericAnd(ComfyNodeABC): """ - Returns the logical N/AND result of one or more values. + Returns the AND of any number of values, evaluated with Python truthiness. - This node takes a dynamic number of inputs and returns their logical N/AND result. - Note that values are evaluated according Python's rules. I.e. an empty string is - `false`, an integer 0 is also `false`, etc. + Connect additional values to the dynamic item input to add more operands. Because + 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}), + "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"}), + "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,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all connected values are truthy (unless inverted).",) CATEGORY = "Basic/BOOLEAN" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "and_operation" diff --git a/src/basic_data_handling/casting_nodes.py b/src/basic_data_handling/casting_nodes.py index c9d76e7..3e7134a 100644 --- a/src/basic_data_handling/casting_nodes.py +++ b/src/basic_data_handling/casting_nodes.py @@ -15,17 +15,22 @@ class IO: class CastToBoolean(ComfyNodeABC): """ - Converts any input to a BOOLEAN. Follows standard Python truthy/falsy rules. + Converts any value to a BOOLEAN using Python truthiness. + + 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, {}) + "input": (IO.ANY, {"tooltip": "The value to convert to a BOOLEAN."}) } } RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("boolean",) + OUTPUT_TOOLTIPS = ("The input value converted to a BOOLEAN.",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert_to_boolean" @@ -36,17 +41,23 @@ def convert_to_boolean(self, input: Any) -> tuple[bool]: class CastToDict(ComfyNodeABC): """ - Converts compatible inputs to a DICT. Input must be a mapping or a list of key-value pairs. + Converts a value into a DICT. + + The input must already be a mapping, or an iterable of key-value pairs (for + 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, {}) + "input": (IO.ANY, {"tooltip": "The value to convert to a DICT."}) } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The input value converted to a DICT.",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert_to_dict" @@ -60,17 +71,22 @@ def convert_to_dict(self, input: Any) -> tuple[dict]: class CastToFloat(ComfyNodeABC): """ - Converts any numeric input to a FLOAT. Non-numeric or invalid inputs raise a ValueError. + Converts a numeric value to a FLOAT. + + 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, {}) + "input": (IO.ANY, {"tooltip": "The value to convert to a FLOAT."}) } } RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("float",) + OUTPUT_TOOLTIPS = ("The input value converted to a FLOAT.",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert_to_float" @@ -84,17 +100,22 @@ def convert_to_float(self, input: Any) -> tuple[float]: class CastToInt(ComfyNodeABC): """ - Converts any numeric input to an INT. Non-numeric or invalid inputs raise a ValueError. + Converts a numeric value to an INT, truncating toward zero. + + 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, {}) + "input": (IO.ANY, {"tooltip": "The value to convert to an INT."}) } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("int",) + OUTPUT_TOOLTIPS = ("The input value converted to an INT.",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert_to_int" @@ -108,18 +129,22 @@ def convert_to_int(self, input: Any) -> tuple[int]: class CastToList(ComfyNodeABC): """ - Converts any input to a LIST. Non-list inputs are wrapped in a list. If input is a ComfyUI data list, - it converts the individual items into a Python LIST. + Converts a value into a Python LIST. + + 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, {}) + "input": (IO.ANY, {"tooltip": "The value to convert to a LIST."}) } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The input value converted to a LIST.",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert_to_list" @@ -132,18 +157,22 @@ def convert_to_list(self, input: Any) -> tuple[list]: class CastToSet(ComfyNodeABC): """ - Converts any input to a SET. Non-set inputs are converted into a set. If input is a ComfyUI data list, - it casts the individual items into a SET. + Converts a value into a SET (an unordered collection of unique items). + + 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, {}) + "input": (IO.ANY, {"tooltip": "The value to convert to a SET."}) } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("The input value converted to a SET.",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert_to_set" @@ -156,17 +185,22 @@ def convert_to_set(self, input: Any) -> tuple[set]: class CastToString(ComfyNodeABC): """ - Converts any input to a STRING. Non-string values are converted using str(). + Converts any value to a STRING using its textual representation. + + 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, {}) + "input": (IO.ANY, {"tooltip": "The value to convert to a STRING."}) } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("string",) + OUTPUT_TOOLTIPS = ("The input value converted to a STRING.",) CATEGORY = "Basic/cast" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert_to_string" diff --git a/src/basic_data_handling/comparison_nodes.py b/src/basic_data_handling/comparison_nodes.py index 25e1a8b..1dca02e 100644 --- a/src/basic_data_handling/comparison_nodes.py +++ b/src/basic_data_handling/comparison_nodes.py @@ -18,19 +18,21 @@ class Equal(ComfyNodeABC): Checks if two values are equal. This node takes two inputs of any type and returns True if they are equal, - and False otherwise. For complex objects, structural equality is tested. + and False otherwise. For complex objects (lists, dicts, sets), structural + equality is tested. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "value1": (IO.ANY, {}), - "value2": (IO.ANY, {}), + "value1": (IO.ANY, {"tooltip": "First value to compare."}), + "value2": (IO.ANY, {"tooltip": "Second value to compare."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the two values are equal, otherwise False.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" @@ -44,19 +46,21 @@ class NotEqual(ComfyNodeABC): Checks if two values are not equal. This node takes two inputs of any type and returns True if they are not equal, - and False otherwise. For complex objects, structural inequality is tested. + and False otherwise. For complex objects (lists, dicts, sets), structural + inequality is tested. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "value1": (IO.ANY, {}), - "value2": (IO.ANY, {}), + "value1": (IO.ANY, {"tooltip": "First value to compare."}), + "value2": (IO.ANY, {"tooltip": "Second value to compare."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the two values are not equal, otherwise False.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" @@ -74,19 +78,20 @@ class LessThan(ComfyNodeABC): Checks if the first value is less than the second. This node takes two numerical inputs and returns True if the first value - is less than the second value, and False otherwise. + is strictly less than the second value, and False otherwise. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"widgetType": "FLOAT"}), + "value1": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "Left-hand operand of the comparison."}), + "value2": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "Right-hand operand of the comparison."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when value1 is strictly less than value2, otherwise False.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" @@ -106,13 +111,14 @@ class LessThanOrEqual(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"widgetType": "FLOAT"}), + "value1": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "Left-hand operand of the comparison."}), + "value2": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "Right-hand operand of the comparison."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when value1 is less than or equal to value2, otherwise False.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" @@ -126,19 +132,20 @@ class GreaterThan(ComfyNodeABC): Checks if the first value is greater than the second. This node takes two numerical inputs and returns True if the first value - is greater than the second value, and False otherwise. + is strictly greater than the second value, and False otherwise. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"widgetType": "FLOAT"}), + "value1": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "Left-hand operand of the comparison."}), + "value2": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "Right-hand operand of the comparison."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when value1 is strictly greater than value2, otherwise False.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" @@ -158,13 +165,14 @@ class GreaterThanOrEqual(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"widgetType": "FLOAT"}), - "value2": (IO.NUMBER, {"widgetType": "FLOAT"}), + "value1": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "Left-hand operand of the comparison."}), + "value2": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "Right-hand operand of the comparison."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when value1 is greater than or equal to value2, otherwise False.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" @@ -177,19 +185,20 @@ class IsNull(ComfyNodeABC): """ Checks if a value is None/null. - This node takes any input value and returns True if the value is None, - and False otherwise. + 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 { "required": { - "value": (IO.ANY, {}), + "value": (IO.ANY, {"tooltip": "The value to test for null."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("is_null",) + OUTPUT_TOOLTIPS = ("True when the input value is None, otherwise False.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_null" @@ -202,26 +211,27 @@ class NumberInRange(ComfyNodeABC): """ Checks if a number is within a specified range. - This node takes a number and range bounds, and returns True if the number - is within the specified range, and False otherwise. The user can specify - whether the bounds are inclusive or exclusive. + This node takes a number, a minimum and a maximum bound, and returns True if the + 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 { "required": { - "value": (IO.NUMBER, {"widgetType": "FLOAT"}), - "min_value": ("FLOAT", {"default": 0}), - "max_value": ("FLOAT", {"default": 100}), + "value": (IO.NUMBER, {"widgetType": "FLOAT", "tooltip": "The number to test."}), + "min_value": ("FLOAT", {"default": 0, "tooltip": "Lower bound of the range."}), + "max_value": ("FLOAT", {"default": 100, "tooltip": "Upper bound of the range."}), }, "optional": { - "include_min": (IO.BOOLEAN, {"default": "True"}), - "include_max": (IO.BOOLEAN, {"default": "True"}), + "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,) RETURN_NAMES = ("in_range",) + OUTPUT_TOOLTIPS = ("True when the number lies within the configured range.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_range" @@ -236,23 +246,25 @@ def check_range(self, value: float, min_value: float, max_value: float, class CompareLength(ComfyNodeABC): """ - Compares the length of a container (string, list, etc) with a value. + Compares the length of a container (string, list, dict, set, ...) with a value. - This node takes a container and a comparison value, and returns a boolean - result based on the comparison of the container's length with the value. + This node measures ``len(container)`` and compares it with *length* using the + selected *operator*. Returns the boolean result and, as a convenience, the + actual measured length. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "container": (IO.ANY, {}), - "operator": (["==", "!=", ">", "<", ">=", "<="], {"default": "=="}), - "length": (IO.INT, {"default": 0, "min": 0}), + "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."}), + "length": (IO.INT, {"default": 0, "min": 0, "tooltip": "The value the measured length is compared against."}), } } RETURN_TYPES = (IO.BOOLEAN, IO.INT) RETURN_NAMES = ("result", "actual_length") + OUTPUT_TOOLTIPS = ("Result of evaluating length value.", "The measured length of the container (-1 when it has no length).") CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare_length" @@ -284,22 +296,24 @@ class StringComparison(ComfyNodeABC): """ Compares two strings using a selected comparison operator. - This node takes two string inputs and a comparison operator, and returns - a boolean result based on the selected comparison. + This node takes two string inputs and an operator, and returns True when the + 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": ""}), - "string2": ("STRING", {"default": ""}), - "operator": (["==", "!=", ">", "<", ">=", "<="], {"default": "=="}), - "case_sensitive": (IO.BOOLEAN, {"default": True}), + "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."}), + "case_sensitive": (IO.BOOLEAN, {"default": True, "tooltip": "When enabled, letter case is respected (e.g. 'A' != 'a')."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the selected comparison holds between the two strings.",) CATEGORY = "Basic/comparison" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" diff --git a/src/basic_data_handling/control_flow_nodes.py b/src/basic_data_handling/control_flow_nodes.py index cc47f08..f550a75 100644 --- a/src/basic_data_handling/control_flow_nodes.py +++ b/src/basic_data_handling/control_flow_nodes.py @@ -30,14 +30,15 @@ class IfElse(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "condition": (IO.BOOLEAN, {}), - "if_true": (IO.ANY, {"lazy": True}), - "if_false": (IO.ANY, {"lazy": True}), + "condition": (IO.BOOLEAN, {"tooltip": "Controls which branch is returned."}), + "if_true": (IO.ANY, {"lazy": True, "tooltip": "Value returned when the condition is True."}), + "if_false": (IO.ANY, {"lazy": True, "tooltip": "Value returned when the condition is False."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The value of the branch selected by the condition.",) CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -68,18 +69,19 @@ class IfElifElse(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "if": (IO.BOOLEAN, {"forceInput": True}), - "then": (IO.ANY, {"lazy": True}), + "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}), - "then_0": (IO.ANY, {"lazy": True, "_dynamic": "number", "_dynamicGroup": 0}), - "else": (IO.ANY, {"lazy": True}), + "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,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The value of the first branch whose condition is True (or the else value).",) CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -157,16 +159,17 @@ class SwitchCase(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": ContainsDynamicDict({ - "select": (IO.INT, {"default": 0, "min": 0}), - "case_0": (IO.ANY, {"lazy": True, "_dynamic": "number"}), + "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}), + "default": (IO.ANY, {"lazy": True, "tooltip": "Value returned when the selected index is out of range."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The selected case value, or the default when the index is out of range.",) CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -225,16 +228,17 @@ class ContinueFlow(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.ANY, {}), - "select": (IO.BOOLEAN, {"default": True}), + "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."}), }, "optional": { - "message": (IO.STRING, {"default": ""}), + "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,) RETURN_NAMES = ("value",) + OUTPUT_TOOLTIPS = ("The input value, or an execution blocker when the flow is disabled.",) CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -259,13 +263,14 @@ class FlowSelect(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.ANY, {}), - "select": (IO.BOOLEAN, {}), + "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."}), } } 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).") CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "select" @@ -292,12 +297,13 @@ class ForceCalculation(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.ANY, {}), + "value": (IO.ANY, {"tooltip": "Any value; passed through unchanged while forcing recalculation."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("value",) + OUTPUT_TOOLTIPS = ("The unchanged input value.",) CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -326,13 +332,14 @@ class ExecutionOrder(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": { - "E/O": ("E/O", {}), - "any node output": (IO.ANY, {}), + "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."}), } } RETURN_TYPES = ("E/O", IO.ANY) RETURN_NAMES = ("E/O", "passthrough") + OUTPUT_TOOLTIPS = ("Chain to the next execution-order node.", "The 'any node output' value passed through unchanged.") FUNCTION = "execution_order" CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") @@ -354,12 +361,13 @@ class IsConnected(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": { - "input": (IO.ANY, {}), + "input": (IO.ANY, {"tooltip": "The input to test whether it is connected."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("is_connected",) + OUTPUT_TOOLTIPS = ("True when the input is connected to another node's output, otherwise False.",) CATEGORY = "Basic/flow control" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" diff --git a/src/basic_data_handling/data_list_nodes.py b/src/basic_data_handling/data_list_nodes.py index 04e855e..e045317 100644 --- a/src/basic_data_handling/data_list_nodes.py +++ b/src/basic_data_handling/data_list_nodes.py @@ -29,12 +29,13 @@ class DataListCreate(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING"}), + "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,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The items as a ComfyUI data list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -58,12 +59,13 @@ class DataListListCreate(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING"}), + "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,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The items as a ComfyUI data list of lists.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -86,12 +88,13 @@ class DataListCreateFromBoolean(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING"}), + "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,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The boolean items as a ComfyUI data list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -113,12 +116,13 @@ class DataListCreateFromFloat(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING"}), + "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,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The float items as a ComfyUI data list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -140,12 +144,13 @@ class DataListCreateFromInt(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING"}), + "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,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The integer items as a ComfyUI data list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -167,12 +172,13 @@ class DataListCreateFromString(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.STRING, {"_dynamic": "number", "widgetType": "STRING"}), + "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,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The string items as a ComfyUI data list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -193,12 +199,13 @@ class DataListAll(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), + "list": (IO.ANY, {"tooltip": "The Data List to evaluate."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when every element is truthy (or the list is empty).",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_all" @@ -218,12 +225,13 @@ class DataListAny(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), + "list": (IO.ANY, {"tooltip": "The Data List to evaluate."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when at least one element is truthy (False for an empty list).",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_any" @@ -244,13 +252,14 @@ class DataListAppend(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": { - "list":(IO.ANY,{}), - "item":(IO.ANY,{}), + "list":(IO.ANY,{"tooltip": "The Data List to append to."}), + "item":(IO.ANY,{"tooltip": "The item to append at the end."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new Data List with the item appended.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "append" @@ -276,13 +285,14 @@ class DataListContains(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), - "value": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to search."}), + "value": (IO.ANY, {"tooltip": "The value to look for."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("contains",) + OUTPUT_TOOLTIPS = ("True when the value is present in the list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "contains" @@ -306,13 +316,14 @@ class DataListCount(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), - "value": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to count in."}), + "value": (IO.ANY, {"tooltip": "The value whose occurrences are counted."}), } } RETURN_TYPES = (IO.INT,) RETURN_NAMES = ("count",) + OUTPUT_TOOLTIPS = ("The number of times the value occurs in the list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "count" @@ -333,15 +344,16 @@ class DataListEnumerate(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), + "list": (IO.ANY, {"tooltip": "The Data List to enumerate."}), }, "optional": { - "start": (IO.INT, {"default": 0}), + "start": (IO.INT, {"default": 0, "tooltip": "Index assigned to the first element."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A Data List of [index, value] pairs.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "enumerate_list" @@ -365,13 +377,14 @@ class DataListExtend(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": { - "list_a": (IO.ANY,{}), - "list_b": (IO.ANY,{}), + "list_a":(IO.ANY,{"tooltip": "First Data List (kept as-is)."}), + "list_b":(IO.ANY,{"tooltip": "Second Data List whose elements are appended."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new Data List with all elements of both inputs.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "extend" @@ -397,13 +410,14 @@ class DataListFilter(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.ANY, {}), - "filter": (IO.BOOLEAN, {"forceInput": True}), + "value": (IO.ANY, {"tooltip": "The Data List of values to filter."}), + "filter": (IO.BOOLEAN, {"forceInput": True, "tooltip": "Data List of booleans; items whose filter is False are kept."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("filtered_list",) + OUTPUT_TOOLTIPS = ("The values kept where the filter was False.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "filter_data" @@ -435,13 +449,14 @@ class DataListFilterSelect(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.ANY, {}), - "select": (IO.BOOLEAN, {}), + "value": (IO.ANY, {"tooltip": "The Data List of values to split."}), + "select": (IO.BOOLEAN, {"tooltip": "Data List of booleans routing each value to the 'true' or 'false' output."}), } } RETURN_TYPES = (IO.ANY, IO.ANY) RETURN_NAMES = ("true", "false") + OUTPUT_TOOLTIPS = ("Values whose selector was True.", "Values whose selector was False.") CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "select" @@ -471,12 +486,13 @@ class DataListFirst(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), + "list": (IO.ANY, {"tooltip": "The Data List to read from."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("first_element",) + OUTPUT_TOOLTIPS = ("The first element of the list (None when empty).",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_first_element" @@ -499,13 +515,14 @@ class DataListGetItem(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), - "index": (IO.INT, {"default": 0}), + "list": (IO.ANY, {"tooltip": "The Data List to read from."}), + "index": (IO.INT, {"default": 0, "tooltip": "Position of the item; negative counts from the end."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("item",) + OUTPUT_TOOLTIPS = ("The item at the index (None when out of range).",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_item" @@ -531,17 +548,18 @@ class DataListIndex(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), - "value": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to search."}), + "value": (IO.ANY, {"tooltip": "The value whose first occurrence is located."}), }, "optional": { - "start": (IO.INT, {"default": 0}), - "end": (IO.INT, {"default": -1}), + "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,) RETURN_NAMES = ("index",) + OUTPUT_TOOLTIPS = ("Index of the first occurrence (-1 when the value is absent).",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "list_index" @@ -572,14 +590,15 @@ class DataListInsert(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), - "index": (IO.INT, {"default": 0}), - "item": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to insert into."}), + "index": (IO.INT, {"default": 0, "tooltip": "Position at which to insert the item."}), + "item": (IO.ANY, {"tooltip": "The item to insert."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new Data List with the item inserted at the index.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "insert" @@ -603,12 +622,13 @@ class DataListLast(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), + "list": (IO.ANY, {"tooltip": "The Data List to read from."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("last_element",) + OUTPUT_TOOLTIPS = ("The last element of the list (None when empty).",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_last_element" @@ -629,12 +649,13 @@ class DataListLength(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to measure."}), } } RETURN_TYPES = (IO.INT,) RETURN_NAMES = ("length",) + OUTPUT_TOOLTIPS = ("The number of items in the list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "length" @@ -656,12 +677,13 @@ class DataListMax(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.NUMBER, {}), + "list": (IO.NUMBER, {"tooltip": "The Data List of numbers."}), } } RETURN_TYPES = (IO.NUMBER,) RETURN_NAMES = ("max",) + OUTPUT_TOOLTIPS = ("The maximum value (None when empty or not comparable).",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "find_max" @@ -692,12 +714,13 @@ class DataListMin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.NUMBER, {}), + "list": (IO.NUMBER, {"tooltip": "The Data List of numbers."}), } } RETURN_TYPES = (IO.NUMBER,) RETURN_NAMES = ("min",) + OUTPUT_TOOLTIPS = ("The minimum value (None when empty or not comparable).",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "find_min" @@ -729,15 +752,16 @@ class DataListPop(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to pop from."}), }, "optional": { - "index": (IO.INT, {"default": -1}), + "index": (IO.INT, {"default": -1, "tooltip": "Position of the item to remove (-1 = last item)."}), } } RETURN_TYPES = (IO.ANY, IO.ANY) RETURN_NAMES = ("list", "item") + OUTPUT_TOOLTIPS = ("The list with the item removed.", "The removed item (None when empty or the index is invalid).") CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop" @@ -766,15 +790,16 @@ class DataListPopRandom(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), + "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}), + "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."}), }, } RETURN_TYPES = (IO.ANY, IO.ANY) RETURN_NAMES = ("list", "item") + OUTPUT_TOOLTIPS = ("The list with a random element removed.", "The removed element (None when the list is empty).") CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop_random_element" @@ -816,16 +841,18 @@ class DataListRange(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "start": ("INT", {"default": 0}), - "stop": ("INT", {"default": 10}), + "start": ("INT", {"default": 0, "tooltip": "First number of the sequence (inclusive)."}), + "stop": ("INT", {"default": 10, "tooltip": "Stop value of the sequence (exclusive)."}), }, "optional": { - "step": ("INT", {"default": 1}), + "step": ("INT", {"default": 1, "tooltip": "Step between numbers; must not be 0."}), } } RETURN_TYPES = ("INT",) FUNCTION = "create_range" + RETURN_NAMES = ("range",) + OUTPUT_TOOLTIPS = ("The generated numbers as a Data List.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") OUTPUT_IS_LIST = (True,) @@ -847,13 +874,14 @@ class DataListRemove(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), - "value": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to remove from."}), + "value": (IO.ANY, {"tooltip": "The value whose first occurrence is removed."}), } } 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" @@ -880,12 +908,13 @@ class DataListReverse(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to reverse."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new Data List with the items in reversed order.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "reverse" @@ -909,14 +938,15 @@ class DataListSetItem(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), - "index": (IO.INT, {"default": 0}), - "value": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to modify."}), + "index": (IO.INT, {"default": 0, "tooltip": "Position of the item to replace."}), + "value": (IO.ANY, {"tooltip": "The new value."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new Data List with the item at the index replaced.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "set_item" @@ -945,13 +975,14 @@ class DataListShuffle(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), - "seed": (IO.INT, {"default": 0}), + "list": (IO.ANY, {"tooltip": "The Data List to shuffle."}), + "seed": (IO.INT, {"default": 0, "tooltip": "Seed for reproducible shuffling."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new Data List with the items shuffled.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "shuffle_list" @@ -979,17 +1010,18 @@ class DataListSlice(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to slice."}), }, "optional": { - "start": (IO.INT, {"default": 0}), - "stop": (IO.INT, {"default": INT_MAX}), - "step": (IO.INT, {"default": 1}), + "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,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The requested slice of the list.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "slice" @@ -1016,15 +1048,16 @@ class DataListSort(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY,), + "list": (IO.ANY, {"tooltip": "The Data List to sort."}), }, "optional": { - "reverse": (["False", "True"], {"default": "False"}), + "reverse": (["False", "True"], {"default": "False", "tooltip": "Sort in descending order when True."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new sorted Data List.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "sort" @@ -1049,15 +1082,16 @@ class DataListSum(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.NUMBER, {}), + "list": (IO.NUMBER, {"tooltip": "The Data List of numbers to sum."}), }, "optional": { - "start": (IO.INT, {"default": 0}), + "start": (IO.INT, {"default": 0, "tooltip": "Initial value added to the 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 "") FUNCTION = "sum_list" @@ -1082,17 +1116,18 @@ class DataListZip(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list1": (IO.ANY,), - "list2": (IO.ANY,), + "list1": (IO.ANY, {"tooltip": "First Data List."}), + "list2": (IO.ANY, {"tooltip": "Second Data List."}), }, "optional": { - "list3": (IO.ANY,), - "list4": (IO.ANY,), + "list3": (IO.ANY, {"tooltip": "Optional additional Data List."}), + "list4": (IO.ANY, {"tooltip": "Optional additional Data List."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("Elements combined element-wise; length matches the shortest input.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "zip_lists" @@ -1124,11 +1159,13 @@ class DataListToList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), + "list": (IO.ANY, {"tooltip": "The Data List to convert."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The Data List's items as a single Python LIST value.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert" @@ -1149,11 +1186,13 @@ class DataListToSet(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": (IO.ANY, {}), + "list": (IO.ANY, {"tooltip": "The Data List to convert."}), } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("A SET of the Data List's unique items.",) CATEGORY = "Basic/Data List" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert" diff --git a/src/basic_data_handling/dict_nodes.py b/src/basic_data_handling/dict_nodes.py index b0084d9..61fb9f5 100644 --- a/src/basic_data_handling/dict_nodes.py +++ b/src/basic_data_handling/dict_nodes.py @@ -65,12 +65,14 @@ class DictCreate(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), - "value_0": (IO.ANY, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The created DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -96,12 +98,14 @@ class DictCreateFromBoolean(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), - "value_0": (IO.BOOLEAN, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The created DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -127,12 +131,14 @@ class DictCreateFromFloat(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), - "value_0": (IO.FLOAT, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The created DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -158,12 +164,14 @@ class DictCreateFromInt(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), - "value_0": (IO.INT, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The created DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -189,12 +197,14 @@ class DictCreateFromString(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "key_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), - "value_0": (IO.STRING, {"_dynamic": "number", "_dynamicGroup": 0, "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The created DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -220,11 +230,13 @@ class DictCreateFromItemsDataList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "item": (IO.ANY, {}), + "item": (IO.ANY, {"tooltip": "A data list of (key, value) pairs to build the DICT from."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The created DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_from_items" @@ -253,11 +265,13 @@ class DictCreateFromItemsList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "items": ("LIST", {}), + "items": ("LIST", {"tooltip": "A LIST of (key, value) pairs to build the DICT from."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The created DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_from_items" @@ -286,12 +300,14 @@ class DictCreateFromLists(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "keys": ("LIST", {}), - "values": ("LIST", {}), + "keys": ("LIST", {"tooltip": "The keys of the DICT."}), + "values": ("LIST", {"tooltip": "The values paired with the keys by position."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The DICT created by pairing the two lists (up to the shorter length).",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_from_lists" @@ -314,13 +330,14 @@ class DictCompare(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "dict1": ("DICT", {}), - "dict2": ("DICT", {}), + "dict1": ("DICT", {"tooltip": "First DICT to compare."}), + "dict2": ("DICT", {"tooltip": "Second DICT to compare."}), } } 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.") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "compare" @@ -353,12 +370,14 @@ class DictContainsKey(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), + "input_dict": ("DICT", {"tooltip": "The DICT to search."}), + "key": (IO.STRING, {"default": "", "tooltip": "The key to look for."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("contains",) + OUTPUT_TOOLTIPS = ("True when the key exists in the DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "contains_key" @@ -378,12 +397,14 @@ class DictExcludeKeys(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "keys_to_exclude": ("LIST", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to copy from."}), + "keys_to_exclude": ("LIST", {"tooltip": "Keys to drop from the result."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("A new DICT without the excluded keys.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "exclude_keys" @@ -427,12 +448,14 @@ class DictFilterByKeys(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "keys": ("LIST", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to filter."}), + "keys": ("LIST", {"tooltip": "Keys to keep in the result."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("A new DICT containing only the selected keys.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "filter_by_keys" @@ -479,14 +502,16 @@ class DictFromKeys(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "keys": ("LIST", {}), + "keys": ("LIST", {"tooltip": "The keys of the new DICT."}), }, "optional": { - "value": (IO.ANY, {}), + "value": (IO.ANY, {"tooltip": "Default value assigned to every key (None when not provided)."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("A DICT mapping each key to the given value.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "from_keys" @@ -507,16 +532,17 @@ class DictGet(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), + "input_dict": ("DICT", {"tooltip": "The DICT to read from."}), + "key": (IO.STRING, {"default": "", "tooltip": "The key whose value is retrieved."}), }, "optional": { - "default": (IO.ANY, {}), + "default": (IO.ANY, {"tooltip": "Value returned when the key is missing (None when not provided)."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("value",) + OUTPUT_TOOLTIPS = ("The value for the key, or the default when the key is absent.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get" @@ -536,12 +562,13 @@ class DictGetKeysValues(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to decompose."}), } } RETURN_TYPES = ("LIST", "LIST") RETURN_NAMES = ("keys", "values") + OUTPUT_TOOLTIPS = ("All keys of the DICT.", "All values of the DICT, in the same order as the keys.") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_keys_values" @@ -564,16 +591,17 @@ class DictGetMultiple(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "keys": ("LIST", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to read from."}), + "keys": ("LIST", {"tooltip": "The keys whose values are retrieved."}), }, "optional": { - "default": (IO.ANY, {}), + "default": (IO.ANY, {"tooltip": "Value used for keys that are missing (None when not provided)."}), } } RETURN_TYPES = ("LIST",) RETURN_NAMES = ("values",) + OUTPUT_TOOLTIPS = ("The value for each requested key, using the default for missing keys.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_multiple" @@ -598,12 +626,13 @@ class DictInvert(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), + "input_dict": ("DICT", {"tooltip": "The DICT whose keys and values are swapped."}), } } RETURN_TYPES = ("DICT", IO.BOOLEAN) RETURN_NAMES = ("inverted_dict", "success") + OUTPUT_TOOLTIPS = ("The DICT with keys and values swapped.", "True when the inversion succeeded (values were hashable).") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "invert" @@ -630,11 +659,13 @@ class DictItems(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to read."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("items",) + OUTPUT_TOOLTIPS = ("A LIST of (key, value) tuples, one per entry.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "items" @@ -653,11 +684,13 @@ class DictKeys(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to read."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("keys",) + OUTPUT_TOOLTIPS = ("A LIST of all keys in the DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "keys" @@ -676,12 +709,13 @@ class DictLength(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to measure."}), } } RETURN_TYPES = ("INT",) RETURN_NAMES = ("length",) + OUTPUT_TOOLTIPS = ("The number of key-value pairs in the DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "length" @@ -701,16 +735,18 @@ class DictMerge(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "dict1": ("DICT", {}), + "dict1": ("DICT", {"tooltip": "First DICT to merge."}), }, "optional": { - "dict2": ("DICT", {}), - "dict3": ("DICT", {}), - "dict4": ("DICT", {}), + "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",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The merged DICT; later inputs take precedence on duplicate keys.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "merge" @@ -750,16 +786,17 @@ class DictPop(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), + "input_dict": ("DICT", {"tooltip": "The DICT to pop from."}), + "key": (IO.STRING, {"default": "", "tooltip": "The key whose entry is removed."}), }, "optional": { - "default_value": (IO.ANY, {}), + "default_value": (IO.ANY, {"tooltip": "Value returned when the key is absent (None when not provided)."}), } } RETURN_TYPES = ("DICT", IO.ANY) RETURN_NAMES = ("dict", "value") + OUTPUT_TOOLTIPS = ("The DICT with the key removed.", "The removed value (or the default when the key was absent).") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop" @@ -794,12 +831,13 @@ class DictPopItem(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to pop an entry from."}), } } 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.") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "popitem" @@ -832,15 +870,16 @@ class DictPopRandom(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), + "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}), + "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.") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop_random" @@ -880,13 +919,14 @@ class DictRemove(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), + "input_dict": ("DICT", {"tooltip": "The DICT to remove from."}), + "key": (IO.STRING, {"default": "", "tooltip": "The key to remove."}), } } RETURN_TYPES = ("DICT", IO.BOOLEAN) RETURN_NAMES = ("dict", "key_removed") + OUTPUT_TOOLTIPS = ("The DICT with the key removed (unchanged when it was absent).", "True when the key existed and was removed.") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "remove" @@ -913,13 +953,15 @@ class DictSet(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), - "value": (IO.ANY, {}), + "input_dict": ("DICT", {"tooltip": "The DICT to modify."}), + "key": (IO.STRING, {"default": "", "tooltip": "The key to add or update."}), + "value": (IO.ANY, {"tooltip": "The value to store under the key."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("A new DICT with the key set to the given value.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "set" @@ -944,14 +986,15 @@ class DictSetDefault(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), - "key": (IO.STRING, {"default": ""}), - "default_value": (IO.ANY, {}), + "input_dict": ("DICT", {"tooltip": "The DICT to read or modify."}), + "key": (IO.STRING, {"default": "", "tooltip": "The key to look up."}), + "default_value": (IO.ANY, {"tooltip": "Value inserted and returned when the key is missing."}), } } 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).") CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "setdefault" @@ -976,12 +1019,14 @@ class DictUpdate(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "dict1": ("DICT", {}), - "dict2": ("DICT", {}), + "dict1": ("DICT", {"tooltip": "Base DICT to update."}), + "dict2": ("DICT", {"tooltip": "DICT whose entries are added; it wins on duplicate keys."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("dict",) + OUTPUT_TOOLTIPS = ("The merged DICT with dict2's entries applied to dict1.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "update" @@ -1007,11 +1052,13 @@ class DictValues(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input_dict": ("DICT", {}), + "input_dict": ("DICT", {"tooltip": "The DICT to read."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("values",) + OUTPUT_TOOLTIPS = ("A LIST of all values in the DICT.",) CATEGORY = "Basic/DICT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "values" diff --git a/src/basic_data_handling/float_nodes.py b/src/basic_data_handling/float_nodes.py index 0529883..7d2c1e2 100644 --- a/src/basic_data_handling/float_nodes.py +++ b/src/basic_data_handling/float_nodes.py @@ -24,11 +24,13 @@ class FloatCreate(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.ANY, {"default": "0.0", "widgetType": "STRING"}), + "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."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("float",) + OUTPUT_TOOLTIPS = ("The parsed FLOAT value.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -47,12 +49,14 @@ class FloatAdd(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float1": (IO.FLOAT, {"default": 0.0}), - "float2": (IO.FLOAT, {"default": 0.0}), + "float1": (IO.FLOAT, {"default": 0.0, "tooltip": "First addend."}), + "float2": (IO.FLOAT, {"default": 0.0, "tooltip": "Second addend."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The sum of the two floats.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "add" @@ -72,12 +76,14 @@ class FloatSubtract(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float1": (IO.FLOAT, {"default": 0.0}), - "float2": (IO.FLOAT, {"default": 0.0}), + "float1": (IO.FLOAT, {"default": 0.0, "tooltip": "Minuend (the value being subtracted from)."}), + "float2": (IO.FLOAT, {"default": 0.0, "tooltip": "Subtrahend (the value to subtract)."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The difference float1 - float2.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "subtract" @@ -96,12 +102,14 @@ class FloatMultiply(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float1": (IO.FLOAT, {"default": 1.0}), - "float2": (IO.FLOAT, {"default": 1.0}), + "float1": (IO.FLOAT, {"default": 1.0, "tooltip": "First factor."}), + "float2": (IO.FLOAT, {"default": 1.0, "tooltip": "Second factor."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The product of the two floats.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "multiply" @@ -121,12 +129,14 @@ class FloatDivide(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float1": (IO.FLOAT, {"default": 1.0}), - "float2": (IO.FLOAT, {"default": 1.0}), + "float1": (IO.FLOAT, {"default": 1.0, "tooltip": "Dividend (numerator)."}), + "float2": (IO.FLOAT, {"default": 1.0, "tooltip": "Divisor (denominator); must not be 0."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The quotient float1 / float2.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "divide" @@ -148,12 +158,14 @@ class FloatDivideSafe(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float1": (IO.FLOAT, {"default": 1.0}), - "float2": (IO.FLOAT, {"default": 1.0}), + "float1": (IO.FLOAT, {"default": 1.0, "tooltip": "Dividend (numerator)."}), + "float2": (IO.FLOAT, {"default": 1.0, "tooltip": "Divisor; a value of 0 yields +/-infinity instead of an error."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The quotient float1 / float2, or +/-infinity (NaN for 0/0) when the divisor is 0.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "divide" @@ -177,12 +189,13 @@ class FloatAsIntegerRatio(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float_value": (IO.FLOAT, {"default": 0.0}), + "float_value": (IO.FLOAT, {"default": 0.0, "tooltip": "The float to decompose into an exact integer ratio."}), } } RETURN_TYPES = (IO.INT, IO.INT) RETURN_NAMES = ("numerator", "denominator") + OUTPUT_TOOLTIPS = ("Numerator of the exact ratio.", "Denominator of the exact ratio.") CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "as_integer_ratio" @@ -203,11 +216,13 @@ class FloatFromHex(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "hex_value": (IO.STRING, {"default": "0x0.0p+0"}), + "hex_value": (IO.STRING, {"default": "0x0.0p+0", "tooltip": "Hexadecimal float string as produced by the \"to hex\" node, e.g. 0x1.8p+1."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("float",) + OUTPUT_TOOLTIPS = ("The decoded FLOAT value.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "from_hex" @@ -226,11 +241,13 @@ class FloatHex(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float_value": (IO.FLOAT, {"default": 0.0}), + "float_value": (IO.FLOAT, {"default": 0.0, "tooltip": "The float to format as a hexadecimal string."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("hex_string",) + OUTPUT_TOOLTIPS = ("Hexadecimal representation of the float, e.g. '0x1.0000000000000p+0'.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "to_hex" @@ -250,11 +267,13 @@ class FloatIsInteger(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float_value": (IO.FLOAT, {"default": 0.0}), + "float_value": (IO.FLOAT, {"default": 0.0, "tooltip": "The float to test."}), } } RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("is_integer",) + OUTPUT_TOOLTIPS = ("True when the float has no fractional part (e.g. 3.0), otherwise False.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "is_integer" @@ -274,12 +293,14 @@ class FloatPower(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "base": (IO.FLOAT, {"default": 1.0}), - "exponent": (IO.FLOAT, {"default": 1.0}), + "base": (IO.FLOAT, {"default": 1.0, "tooltip": "The base of the power."}), + "exponent": (IO.FLOAT, {"default": 1.0, "tooltip": "The exponent to raise the base to."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The result of base ** exponent.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "power" @@ -299,12 +320,14 @@ class FloatRound(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "float_value": (IO.FLOAT, {"default": 0.0}), - "decimal_places": (IO.INT, {"default": 2, "min": 0}), + "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)."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The float rounded to the requested number of decimal places.",) CATEGORY = "Basic/FLOAT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "round" diff --git a/src/basic_data_handling/int_nodes.py b/src/basic_data_handling/int_nodes.py index 88e4413..f6a8c40 100644 --- a/src/basic_data_handling/int_nodes.py +++ b/src/basic_data_handling/int_nodes.py @@ -30,11 +30,13 @@ class IntCreate(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.ANY, {"default": "0", "widgetType": "STRING"}), + "value": (IO.ANY, {"default": "0", "widgetType": "STRING", "tooltip": "Textual form of the integer to parse. Prefixes 0b/0o/0x select binary/octal/hexadecimal."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("int",) + OUTPUT_TOOLTIPS = ("The parsed INT value.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -54,12 +56,14 @@ class IntCreateWithBase(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.STRING, {"default": "0"}), - "base": (IO.INT, {"default": "10", "min": 2}), + "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."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("int",) + OUTPUT_TOOLTIPS = ("The parsed INT value.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -78,12 +82,14 @@ class IntAdd(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int1": (IO.INT, {"default": 0}), - "int2": (IO.INT, {"default": 0}), + "int1": (IO.INT, {"default": 0, "tooltip": "First addend."}), + "int2": (IO.INT, {"default": 0, "tooltip": "Second addend."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The sum of the two integers.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "add" @@ -103,12 +109,14 @@ class IntSubtract(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int1": (IO.INT, {"default": 0}), - "int2": (IO.INT, {"default": 0}), + "int1": (IO.INT, {"default": 0, "tooltip": "Minuend (the value being subtracted from)."}), + "int2": (IO.INT, {"default": 0, "tooltip": "Subtrahend (the value to subtract)."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The difference int1 - int2.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "subtract" @@ -127,12 +135,14 @@ class IntMultiply(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int1": (IO.INT, {"default": 1}), - "int2": (IO.INT, {"default": 1}), + "int1": (IO.INT, {"default": 1, "tooltip": "First factor."}), + "int2": (IO.INT, {"default": 1, "tooltip": "Second factor."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The product of the two integers.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "multiply" @@ -152,12 +162,14 @@ class IntDivide(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int1": (IO.INT, {"default": 1}), - "int2": (IO.INT, {"default": 1}), + "int1": (IO.INT, {"default": 1, "tooltip": "Dividend (numerator)."}), + "int2": (IO.INT, {"default": 1, "tooltip": "Divisor (denominator); must not be 0."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The integer quotient int1 // int2 (fractional part discarded).",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "divide" @@ -179,13 +191,15 @@ class IntDivideSafe(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int1": (IO.INT, {"default": 1}), - "int2": (IO.INT, {"default": 1}), - "infinity": (IO.INT, {"default": 9223372036854775807}), # 2**63 - 1 + "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 } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The integer quotient int1 // int2, or the +/-infinity sentinel when the divisor is 0.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "divide" @@ -206,11 +220,13 @@ class IntBitCount(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int_value": (IO.INT, {"default": 0}), + "int_value": (IO.INT, {"default": 0, "tooltip": "The integer to examine."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("Number of 1 bits in the binary (two's complement) representation.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "bit_count" @@ -231,11 +247,13 @@ class IntBitLength(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int_value": (IO.INT, {"default": 0}), + "int_value": (IO.INT, {"default": 0, "tooltip": "The integer to examine."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("Number of bits required to represent the value (excluding the sign and leading zeros).",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "bit_length" @@ -255,13 +273,15 @@ class IntFromBytes(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "bytes_value": ("BYTES", {}), - "byteorder": (["big", "little"], {"default": "big"}), - "signed": (["True", "False"], {"default": "False"}), + "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."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("int",) + OUTPUT_TOOLTIPS = ("The decoded INT value.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "from_bytes" @@ -282,12 +302,14 @@ class IntModulus(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int1": (IO.INT, {"default": 0}), - "int2": (IO.INT, {"default": 1}), + "int1": (IO.INT, {"default": 0, "tooltip": "Dividend."}), + "int2": (IO.INT, {"default": 1, "tooltip": "Divisor; must not be 0."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The remainder of int1 divided by int2 (same sign as the divisor).",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "modulus" @@ -309,12 +331,14 @@ class IntPower(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "base": (IO.INT, {"default": 1}), - "exponent": (IO.INT, {"default": 0}), + "base": (IO.INT, {"default": 1, "tooltip": "The base of the power."}), + "exponent": (IO.INT, {"default": 0, "tooltip": "The exponent to raise the base to."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The result of base ** exponent.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "power" @@ -334,14 +358,16 @@ class IntToBytes(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "int_value": (IO.INT, {"default": 0}), - "length": (IO.INT, {"default": 4, "min": 1}), - "byteorder": (["big", "little"], {"default": "big"}), - "signed": (["True", "False"], {"default": "False"}), + "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."}), } } RETURN_TYPES = ("BYTES",) + RETURN_NAMES = ("bytes",) + OUTPUT_TOOLTIPS = ("The bytes object representation of the integer.",) CATEGORY = "Basic/INT" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "to_bytes" diff --git a/src/basic_data_handling/list_nodes.py b/src/basic_data_handling/list_nodes.py index ef96439..39828f7 100644 --- a/src/basic_data_handling/list_nodes.py +++ b/src/basic_data_handling/list_nodes.py @@ -29,11 +29,13 @@ class ListCreate(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The LIST containing the provided items.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -54,11 +56,13 @@ class ListCreateFromBoolean(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The LIST containing the provided boolean items.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -79,11 +83,13 @@ class ListCreateFromFloat(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The LIST containing the provided float items.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -104,11 +110,13 @@ class ListCreateFromInt(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The LIST containing the provided integer items.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -129,11 +137,13 @@ class ListCreateFromString(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.STRING, {"_dynamic": "number"}), + "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",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The LIST containing the provided string items.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_list" @@ -153,11 +163,13 @@ class ListAll: def INPUT_TYPES(cls) -> dict: return { "required": { - "list": ("LIST",), + "list": ("LIST", {"tooltip": "The LIST to evaluate."}), } } RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("all_true",) + OUTPUT_TOOLTIPS = ("True when every element is truthy (or the LIST is empty).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_all" @@ -176,11 +188,13 @@ class ListAny: def INPUT_TYPES(cls) -> dict: return { "required": { - "list": ("LIST",), + "list": ("LIST", {"tooltip": "The LIST to evaluate."}), } } RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("any_true",) + OUTPUT_TOOLTIPS = ("True when at least one element is truthy (False for an empty LIST).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_any" @@ -200,12 +214,14 @@ class ListAppend(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "item": (IO.ANY, {}), + "list": ("LIST", {"tooltip": "The LIST to append to."}), + "item": (IO.ANY, {"tooltip": "The item to append at the end."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new LIST with the item appended.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "append" @@ -227,13 +243,14 @@ class ListContains(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "value": (IO.ANY, {}), + "list": ("LIST", {"tooltip": "The LIST to search."}), + "value": (IO.ANY, {"tooltip": "The value to look for."}), } } RETURN_TYPES = ("BOOLEAN",) RETURN_NAMES = ("contains",) + OUTPUT_TOOLTIPS = ("True when the value is present in the LIST.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "contains" @@ -253,13 +270,14 @@ class ListCount(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "value": (IO.ANY, {}), + "list": ("LIST", {"tooltip": "The LIST to count in."}), + "value": (IO.ANY, {"tooltip": "The value whose occurrences are counted."}), } } RETURN_TYPES = ("INT",) RETURN_NAMES = ("count",) + OUTPUT_TOOLTIPS = ("The number of times the value occurs in the LIST.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "count" @@ -278,14 +296,16 @@ class ListEnumerate: def INPUT_TYPES(cls) -> dict: return { "required": { - "list": ("LIST",), + "list": ("LIST", {"tooltip": "The LIST to enumerate."}), }, "optional": { - "start": ("INT", {"default": 0}), + "start": ("INT", {"default": 0, "tooltip": "Index assigned to the first element."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("enumerated",) + OUTPUT_TOOLTIPS = ("A LIST of [index, value] pairs.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "enumerate_list" @@ -305,12 +325,14 @@ class ListExtend(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list1": ("LIST", {}), - "list2": ("LIST", {}), + "list1": ("LIST", {"tooltip": "First LIST (kept as-is)."}), + "list2": ("LIST", {"tooltip": "Second LIST whose elements are appended."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new LIST with all elements of both lists.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "extend" @@ -332,12 +354,13 @@ class ListFirst(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to read from."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("first_element",) + OUTPUT_TOOLTIPS = ("The first element of the LIST (None when empty).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_first_element" @@ -358,13 +381,14 @@ class ListGetItem(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "index": ("INT", {"default": 0}), + "list": ("LIST", {"tooltip": "The LIST to read from."}), + "index": ("INT", {"default": 0, "tooltip": "Position of the item; negative counts from the end."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("item",) + OUTPUT_TOOLTIPS = ("The item at the index (None when out of range).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_item" @@ -388,17 +412,18 @@ class ListIndex(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "value": (IO.ANY, {}), + "list": ("LIST", {"tooltip": "The LIST to search."}), + "value": (IO.ANY, {"tooltip": "The value whose first occurrence is located."}), }, "optional": { - "start": ("INT", {"default": 0}), - "end": ("INT", {"default": -1}), + "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",) RETURN_NAMES = ("index",) + OUTPUT_TOOLTIPS = ("Index of the first occurrence (-1 when the value is absent).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "index" @@ -424,13 +449,15 @@ class ListInsert(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "index": ("INT", {"default": 0}), - "item": (IO.ANY, {}), + "list": ("LIST", {"tooltip": "The LIST to insert into."}), + "index": ("INT", {"default": 0, "tooltip": "Position at which to insert the item."}), + "item": (IO.ANY, {"tooltip": "The item to insert."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new LIST with the item inserted at the index.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "insert" @@ -452,12 +479,13 @@ class ListLast(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to read from."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("last_element",) + OUTPUT_TOOLTIPS = ("The last element of the LIST (None when empty).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_last_element" @@ -476,12 +504,13 @@ class ListLength(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to measure."}), } } RETURN_TYPES = ("INT",) RETURN_NAMES = ("length",) + OUTPUT_TOOLTIPS = ("The number of items in the LIST.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "length" @@ -501,12 +530,13 @@ class ListMax(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST of comparable items."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("max_value",) + OUTPUT_TOOLTIPS = ("The maximum value (None for an empty or non-comparable LIST).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "find_max" @@ -533,12 +563,13 @@ class ListMin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST of comparable items."}), } } RETURN_TYPES = (IO.ANY,) RETURN_NAMES = ("min_value",) + OUTPUT_TOOLTIPS = ("The minimum value (None for an empty or non-comparable LIST).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "find_min" @@ -567,15 +598,16 @@ class ListPop(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to pop from."}), }, "optional": { - "index": ("INT", {"default": -1}), + "index": ("INT", {"default": -1, "tooltip": "Position of the item to remove (-1 = last item)."}), } } RETURN_TYPES = ("LIST", IO.ANY) RETURN_NAMES = ("list", "item") + OUTPUT_TOOLTIPS = ("The LIST with the item removed.", "The removed item (None when the LIST is empty or the index is invalid).") CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop" @@ -601,15 +633,16 @@ class ListPopRandom(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to pop a random element from."}), }, "optional": { - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}), + "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 = ("LIST", IO.ANY) RETURN_NAMES = ("list", "item") + OUTPUT_TOOLTIPS = ("The LIST with a random element removed.", "The removed element (None when the LIST is empty).") CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop_random_element" @@ -643,15 +676,17 @@ class ListRange(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "start": ("INT", {"default": 0}), - "stop": ("INT", {"default": 10}), + "start": ("INT", {"default": 0, "tooltip": "First number of the sequence (inclusive)."}), + "stop": ("INT", {"default": 10, "tooltip": "Stop value of the sequence (exclusive)."}), }, "optional": { - "step": ("INT", {"default": 1}), + "step": ("INT", {"default": 1, "tooltip": "Step between numbers; must not be 0."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("range",) + OUTPUT_TOOLTIPS = ("The generated LIST of numbers.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_range" @@ -674,13 +709,14 @@ class ListRemove(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "value": (IO.ANY, {}), + "list": ("LIST", {"tooltip": "The LIST to remove from."}), + "value": (IO.ANY, {"tooltip": "The value whose first occurrence is removed."}), } } RETURN_TYPES = ("LIST", "BOOLEAN") RETURN_NAMES = ("list", "success") + OUTPUT_TOOLTIPS = ("The LIST with the first occurrence removed.", "True when the value was present and removed.") CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "remove" @@ -704,11 +740,13 @@ class ListReverse(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to reverse."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new LIST with the items in reversed order.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "reverse" @@ -730,13 +768,15 @@ class ListSetItem(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "index": ("INT", {"default": 0}), - "value": (IO.ANY, {}), + "list": ("LIST", {"tooltip": "The LIST to modify."}), + "index": ("INT", {"default": 0, "tooltip": "Position of the item to replace."}), + "value": (IO.ANY, {"tooltip": "The new value."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new LIST with the item at the index replaced.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "set_item" @@ -760,12 +800,14 @@ class ListShuffle(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), - "seed": ("INT", {"default": 0}), + "list": ("LIST", {"tooltip": "The LIST to shuffle."}), + "seed": ("INT", {"default": 0, "tooltip": "Seed for reproducible shuffling."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new LIST with the items shuffled.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "shuffle_list" @@ -789,16 +831,18 @@ class ListSlice(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to slice."}), }, "optional": { - "start": ("INT", {"default": 0}), - "stop": ("INT", {"default": INT_MAX}), - "step": ("INT", {"default": 1}), + "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",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The requested slice of the LIST.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "slice" @@ -818,14 +862,16 @@ class ListSort(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to sort."}), }, "optional": { - "reverse": (["False", "True"], {"default": "False"}), + "reverse": (["False", "True"], {"default": "False", "tooltip": "Sort in descending order when True."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("A new sorted LIST (the original is returned when items are not comparable).",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "sort" @@ -853,14 +899,16 @@ class ListSum: def INPUT_TYPES(cls) -> dict: return { "required": { - "list": ("LIST",), + "list": ("LIST", {"tooltip": "The LIST of numbers to sum."}), }, "optional": { - "start": ("INT", {"default": 0}), + "start": ("INT", {"default": 0, "tooltip": "Initial value added to the sum."}), } } 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 "") FUNCTION = "sum_list" @@ -882,11 +930,13 @@ class ListToDataList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to convert."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("items",) + OUTPUT_TOOLTIPS = ("The LIST's items as a ComfyUI data list.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert" @@ -907,11 +957,13 @@ class ListToSet(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "list": ("LIST", {}), + "list": ("LIST", {"tooltip": "The LIST to convert."}), } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("A SET of the LIST's unique items.",) CATEGORY = "Basic/LIST" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert" diff --git a/src/basic_data_handling/math_formula_node.py b/src/basic_data_handling/math_formula_node.py index f9d66a1..7afb11a 100644 --- a/src/basic_data_handling/math_formula_node.py +++ b/src/basic_data_handling/math_formula_node.py @@ -44,14 +44,16 @@ class MathFormula(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "formula": (IO.STRING, {"default": "-pi() ** 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"}), + "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,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The numerical result of evaluating the formula.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "evaluate" diff --git a/src/basic_data_handling/math_nodes.py b/src/basic_data_handling/math_nodes.py index 35ea0b6..60bd12f 100644 --- a/src/basic_data_handling/math_nodes.py +++ b/src/basic_data_handling/math_nodes.py @@ -24,11 +24,13 @@ class MathAbs(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The number to take the absolute value of."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The absolute (non-negative) value of the input.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -48,12 +50,14 @@ class MathAcos(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), + "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."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The arc cosine angle in the chosen unit.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -76,12 +80,14 @@ class MathAsin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), + "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."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The arc sine angle in the chosen unit.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -104,12 +110,14 @@ class MathAtan(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The value whose arc tangent is computed."}), + "unit": (["radians", "degrees"], {"default": "degrees", "tooltip": "Unit of the returned angle."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The arc tangent angle in the chosen unit.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -133,13 +141,15 @@ class MathAtan2(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "y": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "x": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), + "y": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "Y coordinate (numerator)."}), + "x": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "X coordinate (denominator)."}), + "unit": (["radians", "degrees"], {"default": "degrees", "tooltip": "Unit of the returned angle."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The angle of the point (x, y) in the chosen unit.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -161,11 +171,13 @@ class MathCeil(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The number to round up."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The smallest integer greater than or equal to the value.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -186,12 +198,14 @@ class MathCos(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The angle to take the cosine of."}), + "unit": (["radians", "degrees"], {"default": "degrees", "tooltip": "Unit of the input angle."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The cosine of the angle.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -213,11 +227,13 @@ class MathDegrees(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "radians": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "radians": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The angle in radians to convert."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The equivalent angle in degrees.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -237,6 +253,8 @@ def INPUT_TYPES(cls): return {"required": {}} RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("Euler's number e, approximately 2.71828.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -255,11 +273,13 @@ class MathExp(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The exponent x in e^x."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("e raised to the power of the input value.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -278,11 +298,13 @@ class MathFloor(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "value": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The number to round down."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The largest integer less than or equal to the value.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -302,14 +324,16 @@ class MathLog(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING"}), + "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"}), + "base": (IO.NUMBER, {"default": math.e, "min": 0.0000001, "widgetType": "STRING", "tooltip": "Logarithm base; e gives the natural logarithm."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The logarithm of the value in the chosen base.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -328,11 +352,13 @@ class MathLog10(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING"}), + "value": (IO.NUMBER, {"default": 1.0, "min": 0.0000001, "widgetType": "STRING", "tooltip": "The positive number to take the base-10 logarithm of."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The base-10 logarithm of the value.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -352,12 +378,14 @@ class MathMax(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "First value."}), + "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "Second value."}), } } RETURN_TYPES = ("*",) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The larger of the two input values.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -377,12 +405,14 @@ class MathMin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "value1": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "First value."}), + "value2": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "Second value."}), } } RETURN_TYPES = ("*",) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The smaller of the two input values.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -402,6 +432,8 @@ def INPUT_TYPES(cls): return {"required": {}} RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The mathematical constant pi, approximately 3.14159.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -420,11 +452,13 @@ class MathRadians(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "degrees": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), + "degrees": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The angle in degrees to convert."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The equivalent angle in radians.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -445,12 +479,14 @@ class MathSin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The angle to take the sine of."}), + "unit": (["radians", "degrees"], {"default": "degrees", "tooltip": "Unit of the input angle."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The sine of the angle.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -472,11 +508,13 @@ class MathSqrt(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "STRING"}), + "value": (IO.NUMBER, {"default": 0.0, "min": 0.0, "widgetType": "STRING", "tooltip": "The non-negative number to take the square root of."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The square root of the value.",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" @@ -497,12 +535,14 @@ class MathTan(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING"}), - "unit": (["radians", "degrees"], {"default": "degrees"}), + "angle": (IO.NUMBER, {"default": 0.0, "widgetType": "STRING", "tooltip": "The angle to take the tangent of."}), + "unit": (["radians", "degrees"], {"default": "degrees", "tooltip": "Unit of the input angle."}), } } RETURN_TYPES = (IO.FLOAT,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The tangent of the angle (raises an error where the tangent is undefined).",) CATEGORY = "Basic/maths" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "calculate" diff --git a/src/basic_data_handling/path_nodes.py b/src/basic_data_handling/path_nodes.py index 078b33a..6badb60 100644 --- a/src/basic_data_handling/path_nodes.py +++ b/src/basic_data_handling/path_nodes.py @@ -138,12 +138,13 @@ class PathAbspath(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to resolve to an absolute path."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("absolute path",) + OUTPUT_TOOLTIPS = ("The absolute path with relative components and symlinks resolved.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_abspath" @@ -163,12 +164,13 @@ class PathBasename(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path whose final filename component is returned."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("basename",) + OUTPUT_TOOLTIPS = ("The final filename component of the path.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_basename" @@ -187,15 +189,16 @@ class PathCommonPrefix(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path1": (IO.STRING, {"default": ""}), + "path1": (IO.STRING, {"default": "", "tooltip": "First path."}), }, "optional": { - "path2": (IO.STRING, {"default": ""}), + "path2": (IO.STRING, {"default": "", "tooltip": "Second path."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("common prefix",) + OUTPUT_TOOLTIPS = ("The longest common leading component of the given paths.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_common_prefix" @@ -216,12 +219,13 @@ class PathDirname(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path whose directory component is returned."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("dirname",) + OUTPUT_TOOLTIPS = ("The directory (parent) component of the path.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_dirname" @@ -241,12 +245,13 @@ class PathExists(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to check for existence."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("exists",) + OUTPUT_TOOLTIPS = ("True when the path exists as a file or directory.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_exists" @@ -266,12 +271,13 @@ class PathExpandVars(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path that may contain environment variables (e.g. $HOME, %USERPROFILE%)."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("expanded path",) + OUTPUT_TOOLTIPS = ("The path with environment variables expanded.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "expand_vars" @@ -292,6 +298,7 @@ def INPUT_TYPES(cls): RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("current directory",) + OUTPUT_TOOLTIPS = ("The current working directory as an absolute path.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_cwd" @@ -311,12 +318,13 @@ class PathGetExtension(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path whose file extension is extracted."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("extension",) + OUTPUT_TOOLTIPS = ("The extension including the dot (e.g. '.txt'); empty when there is none.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_extension" @@ -336,12 +344,13 @@ class PathGetSize(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "File whose size in bytes is returned."}), } } RETURN_TYPES = (IO.INT,) RETURN_NAMES = ("size (bytes)",) + OUTPUT_TOOLTIPS = ("The file size in bytes.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_size" @@ -369,15 +378,16 @@ class PathGlob(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "pattern": (IO.STRING, {"default": "*.txt"}), + "pattern": (IO.STRING, {"default": "*.txt", "tooltip": "Shell-style pattern to match, e.g. '*.txt'."}), }, "optional": { - "recursive": (IO.BOOLEAN, {"default": False}), + "recursive": (IO.BOOLEAN, {"default": False, "tooltip": "When True, '**' also matches inside subdirectories."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("matching paths",) + OUTPUT_TOOLTIPS = ("All paths matching the pattern, as a data list.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "glob_paths" @@ -427,12 +437,13 @@ class PathIsAbsolute(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("is absolute",) + OUTPUT_TOOLTIPS = ("True when the path is absolute (starts at the root).",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_is_absolute" @@ -452,12 +463,13 @@ class PathIsDir(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("is dir",) + OUTPUT_TOOLTIPS = ("True when the path exists and is a directory.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_is_dir" @@ -477,12 +489,13 @@ class PathIsFile(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("is file",) + OUTPUT_TOOLTIPS = ("True when the path exists and is a regular file.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_is_file" @@ -503,15 +516,16 @@ class PathJoin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path1": (IO.STRING, {"default": ""}), + "path1": (IO.STRING, {"default": "", "tooltip": "First path component."}), }, "optional": { - "path2": (IO.STRING, {"default": ""}), + "path2": (IO.STRING, {"default": "", "tooltip": "Second path component."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("path",) + OUTPUT_TOOLTIPS = ("The components joined into a single path.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "join_paths" @@ -534,16 +548,17 @@ class PathListDir(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Directory to list."}), }, "optional": { - "files_only": (IO.BOOLEAN, {"default": False}), - "dirs_only": (IO.BOOLEAN, {"default": False}), + "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,) RETURN_NAMES = ("entries",) + OUTPUT_TOOLTIPS = ("The names of the directory entries, as a data list.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "list_directory" @@ -580,12 +595,13 @@ class PathNormalize(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to normalize."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("normalized path",) + OUTPUT_TOOLTIPS = ("The path with redundant separators and up-level references collapsed.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "normalize_path" @@ -605,13 +621,14 @@ class PathSetExtension(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {}), - "extension": (IO.STRING, {"default": ".txt"}), + "path": (IO.STRING, {"default": "", "tooltip": "Path whose extension is replaced."}), + "extension": (IO.STRING, {"default": ".txt", "tooltip": "The new extension; a leading dot is added if missing."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("path",) + OUTPUT_TOOLTIPS = ("The path with its extension replaced.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "set_extension" @@ -636,15 +653,16 @@ class PathRelative(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to express relative to start."}), }, "optional": { - "start": (IO.STRING, {"default": ""}), + "start": (IO.STRING, {"default": "", "tooltip": "Base path; the current working directory is used when empty."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("relative path",) + OUTPUT_TOOLTIPS = ("The path expressed relative to start.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_relative_path" @@ -666,12 +684,13 @@ class PathSplit(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to split into directory and filename."}), } } RETURN_TYPES = (IO.STRING, IO.STRING) RETURN_NAMES = ("directory", "filename") + OUTPUT_TOOLTIPS = ("The directory (head) component.", "The filename (tail) component.") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "split_path" @@ -691,12 +710,13 @@ class PathSplitExt(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path to split into name and extension."}), } } RETURN_TYPES = (IO.STRING, IO.STRING) RETURN_NAMES = ("path without ext", "extension") + OUTPUT_TOOLTIPS = ("The path without its extension.", "The extension including the dot (empty when none).") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "split_ext" @@ -714,12 +734,13 @@ class PathLoadStringFile(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path of the UTF-8 text file to read."}), }, } RETURN_TYPES = (IO.STRING, IO.BOOLEAN) RETURN_NAMES = ("text", "exists") + OUTPUT_TOOLTIPS = ("The file content (empty when the file is missing or unreadable).", "True when the file exists and was read.") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_text" @@ -758,12 +779,13 @@ class PathLoadImageRGB(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path of the image file to load."}), }, } RETURN_TYPES = (IO.IMAGE, IO.BOOLEAN) RETURN_NAMES = ("image", "exists") + OUTPUT_TOOLTIPS = ("The RGB image as a tensor (a blank 1x1 image when the file is missing).", "True when the image was loaded.") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_image_rgb" @@ -810,12 +832,13 @@ class PathLoadImageRGBA(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path of the image file to load."}), }, } 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.") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_image_rgba" @@ -866,12 +889,13 @@ class PathLoadMaskFromAlpha(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path of the image whose alpha channel is used."}), }, } RETURN_TYPES = (IO.MASK, IO.BOOLEAN) RETURN_NAMES = ("mask", "exists") + OUTPUT_TOOLTIPS = ("The alpha channel as a mask (blank when the image has none or is missing).", "True when the image was loaded.") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_mask_from_alpha" @@ -911,15 +935,16 @@ class PathLoadMaskFromGreyscale(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Path of the image to build the mask from."}), }, "optional": { - "invert": (IO.BOOLEAN, {"default": False}), + "invert": (IO.BOOLEAN, {"default": False, "tooltip": "Invert the mask (1.0 - mask) after extraction."}), }, } RETURN_TYPES = (IO.MASK, IO.BOOLEAN) RETURN_NAMES = ("mask", "exists") + OUTPUT_TOOLTIPS = ("The mask derived from the greyscale/red channel.", "True when the image was loaded.") CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "load_mask_from_greyscale" @@ -965,18 +990,19 @@ class PathSaveStringFile(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "text": (IO.STRING, {"default": ""}), - "path": (IO.STRING, {"default": ""}), + "text": (IO.STRING, {"default": "", "tooltip": "The text to write."}), + "path": (IO.STRING, {"default": "", "tooltip": "Destination file path."}), }, "optional": { - "create_dirs": (IO.BOOLEAN, {"default": True}), - "append": (IO.BOOLEAN, {"default": False}), - "encoding": (IO.STRING, {"default": "utf-8"}), + "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,) RETURN_NAMES = ("success",) + OUTPUT_TOOLTIPS = ("True when the file was written successfully.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "save_text" @@ -1005,35 +1031,162 @@ def save_text(self, text: str, path: str, create_dirs: bool = True, append: bool return (False,) +def compose_prompt_text(prompt: str, negative_prompt: str) -> str: + """ + Build the generation-parameter text embedded into saved images. + + Follows the Stable Diffusion WebUI convention: the (optional) positive + prompt is written first, followed by an optional ``Negative prompt:`` + line:: + + + Negative prompt: + + Returns an empty string when neither value is provided, in which case no + metadata is embedded into the file. + """ + lines = [] + if prompt.strip(): + lines.append(prompt.strip()) + if negative_prompt.strip(): + lines.append(f"Negative prompt: {negative_prompt.strip()}") + return "\n".join(lines) + + +def build_png_info(metadata_text: str): + """ + Wrap ``metadata_text`` in a Pillow ``PngInfo`` container under the standard + ``parameters`` text-chunk key so it can be embedded in a PNG file. + + Returns ``None`` when there is no text to embed (or Pillow's PNG metadata + support is unavailable), in which case the image should be saved without + extra metadata. + """ + if not metadata_text: + return None + try: + from PIL import PngImagePlugin + except ModuleNotFoundError: + return None + pnginfo = PngImagePlugin.PngInfo() + pnginfo.add_text("parameters", metadata_text) + return pnginfo + + +def build_image_exif(metadata_text: str, include_description: bool = True): + """ + Build an EXIF block that stores ``metadata_text`` for formats without a + native text chunk (JPEG, WEBP, JXL). + + The payload is written into the EXIF ``UserComment`` field (tag 0x9286) of + the Exif IFD as ``UNICODE\0`` + UTF-16-BE, which matches what Stable + Diffusion WebUI / piexif based readers expect. When ``include_description`` + is true, the payload is also written as UTF-8 into the EXIF + ``ImageDescription`` field (tag 0x010E) of IFD0. + + Returns the EXIF bytes (starting with the ``Exif\0\0`` marker), or ``None`` + when there is no text to embed. + """ + if not metadata_text: + return None + try: + from PIL import ExifTags + except ModuleNotFoundError: + return None + Image, _ = _require_pillow() + exif = Image.Exif() + if include_description: + exif[0x010E] = metadata_text.encode("utf-8") + exif.get_ifd(ExifTags.IFD.Exif)[0x9286] = b"UNICODE\x00" + metadata_text.encode("utf-16-be") + return exif.tobytes() + + +def build_xmp_packet(metadata_text: str) -> bytes: + """ + Build an XMP packet storing ``metadata_text`` in the Dublin Core + ``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' + '' + ) + return packet.encode("utf-8") + + +def metadata_save_kwargs(metadata_text: str, fmt: str) -> dict: + """ + Return the extra keyword arguments that embed ``metadata_text`` when saving + an image in the (lower-case) format ``fmt``. + + Returns an empty dict when there is no text to embed or when the format + cannot carry text metadata. + """ + if not metadata_text: + return {} + if fmt == "png": + return {"pnginfo": build_png_info(metadata_text)} + if fmt in ("jpg", "jpeg"): + exif = build_image_exif(metadata_text, include_description=True) + return {"exif": exif} if exif is not None else {} + if fmt in ("webp", "jxl"): + exif = build_image_exif(metadata_text, include_description=False) + kwargs = {"exif": exif} if exif is not None else {} + if fmt == "jxl": + # EXIF and XMP boxes are only available in the JXL container format + kwargs["use_container"] = True + kwargs["xmp"] = build_xmp_packet(metadata_text) + return kwargs + return {} + + class PathSaveImageRGB(ComfyNodeABC): """ Saves an image to a file. This node takes an image tensor and saves it to the specified path. Supports various image formats like PNG, JPG, WEBP, JXL (if pillow-jxl is installed), etc. + + When ``prompt`` and/or ``negative_prompt`` are provided, they are embedded + into the saved image as ``parameters`` metadata: in the PNG text chunk, in + the EXIF ``UserComment`` (and ``ImageDescription`` for JPEG) fields, and in + 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": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Destination file path (an extension is added from the format when missing)."}), }, "optional": { - "format": (IO.STRING, {"default": "png"}), - "quality": (IO.INT, {"default": 95, "min": 1, "max": 100}), - "create_dirs": (IO.BOOLEAN, {"default": True}), + "format": (IO.STRING, {"default": "png", "tooltip": "Image format: png, jpg, webp or jxl (jxl needs pillow-jxl installed)."}), + "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."}), + "prompt": (IO.STRING, {"default": "", "tooltip": "Optional positive prompt embedded as parameters metadata."}), + "negative_prompt": (IO.STRING, {"default": "", "tooltip": "Optional negative prompt embedded as parameters metadata."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("success",) + OUTPUT_TOOLTIPS = ("True when the image was saved successfully.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "save_image" OUTPUT_NODE = True - def save_image(self, images, path: str, format: str = "png", quality: int = 95, create_dirs: bool = True): + def save_image(self, images, path: str, 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,) @@ -1074,15 +1227,23 @@ def save_image(self, images, path: str, format: str = "png", quality: int = 95, # Create PIL image pil_img = Image.fromarray(img_np) - # Save the image - if format.lower() == "jpg" or format.lower() == "jpeg": - pil_img.save(path, format="JPEG", quality=quality) - elif format.lower() == "webp": - pil_img.save(path, format="WEBP", quality=quality) - elif format.lower() == "jxl" and has_jxl_support: + # Compose the prompt metadata to embed into the saved file + metadata_text = compose_prompt_text(prompt, negative_prompt) + fmt = format.lower() + + # Save the image, embedding prompt metadata where the format supports it + if fmt == "jpg" or fmt == "jpeg": + pil_img.save(path, format="JPEG", quality=quality, **metadata_save_kwargs(metadata_text, fmt)) + elif fmt == "webp": + pil_img.save(path, format="WEBP", quality=quality, **metadata_save_kwargs(metadata_text, fmt)) + elif fmt == "jxl" and has_jxl_support: # JPEG XL specific options - pil_img.save(path, format="JXL", quality=quality) + pil_img.save(path, format="JXL", quality=quality, **metadata_save_kwargs(metadata_text, fmt)) + elif fmt == "png": + pil_img.save(path, format="PNG", **metadata_save_kwargs(metadata_text, fmt)) else: + if metadata_text: + print("Basic data handling: Prompt metadata is not supported for this format; skipping it.") pil_img.save(path, format=format.upper()) print(f"Basic data handling: Successfully saved image to {path}") @@ -1099,6 +1260,12 @@ class PathSaveImageRGBA(ComfyNodeABC): This node takes an image tensor and a mask tensor and saves them to the specified path as an image with transparency, where the mask defines the alpha channel. + + When ``prompt`` and/or ``negative_prompt`` are provided, they are embedded + into the saved image as ``parameters`` metadata: in the PNG text chunk, in + the EXIF ``UserComment`` (and ``ImageDescription`` for JPEG) fields, and in + the EXIF + XMP boxes for JPEG XL. Formats that cannot carry text metadata + ignore the prompts. """ @classmethod def INPUT_TYPES(cls): @@ -1106,26 +1273,30 @@ def INPUT_TYPES(cls): "required": { "images": (IO.IMAGE,), "mask": (IO.MASK,), - "path": (IO.STRING, {"default": ""}), + "path": (IO.STRING, {"default": "", "tooltip": "Destination file path (an extension is added from the format when missing)."}), }, "optional": { - "format": (IO.STRING, {"default": "png"}), - "quality": (IO.INT, {"default": 95, "min": 1, "max": 100}), - "invert_mask": (IO.BOOLEAN, {"default": False}), - "create_dirs": (IO.BOOLEAN, {"default": True}), + "format": (IO.STRING, {"default": "png", "tooltip": "Image format supporting alpha: png, webp or jxl; jpg is coerced to png."}), + "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."}), + "prompt": (IO.STRING, {"default": "", "tooltip": "Optional positive prompt embedded as parameters metadata."}), + "negative_prompt": (IO.STRING, {"default": "", "tooltip": "Optional negative prompt embedded as parameters metadata."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("success",) + OUTPUT_TOOLTIPS = ("True when the image with alpha was saved successfully.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "save_image_with_mask" OUTPUT_NODE = True def save_image_with_mask(self, images, mask, path: str, format: str = "png", - quality: int = 95, invert_mask: bool = False, - create_dirs: bool = True): + 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,) @@ -1186,13 +1357,21 @@ def save_image_with_mask(self, images, mask, path: str, format: str = "png", pil_img_rgba = pil_img.convert("RGBA") pil_img_rgba.putalpha(alpha_img) - # Save the image - if format.lower() == "webp": - pil_img_rgba.save(path, format="WEBP", quality=quality) - elif format.lower() == "jxl" and has_jxl_support: + # Compose the prompt metadata to embed into the saved file + metadata_text = compose_prompt_text(prompt, negative_prompt) + fmt = format.lower() + + # Save the image, embedding prompt metadata where the format supports it + if fmt == "webp": + pil_img_rgba.save(path, format="WEBP", quality=quality, **metadata_save_kwargs(metadata_text, fmt)) + elif fmt == "jxl" and has_jxl_support: # JPEG XL supports alpha channel - pil_img_rgba.save(path, format="JXL", quality=quality) + pil_img_rgba.save(path, format="JXL", quality=quality, **metadata_save_kwargs(metadata_text, fmt)) + elif fmt == "png": + pil_img_rgba.save(path, format="PNG", **metadata_save_kwargs(metadata_text, fmt)) else: + if metadata_text: + print("Basic data handling: Prompt metadata is not supported for this format; skipping it.") pil_img_rgba.save(path, format=format.upper()) print(f"Basic data handling: Successfully saved image with mask to {path}") @@ -1214,6 +1393,7 @@ def INPUT_TYPES(cls): RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("input_path",) + OUTPUT_TOOLTIPS = ("Absolute path of ComfyUI's input directory.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" @@ -1235,6 +1415,7 @@ def INPUT_TYPES(cls): RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("output_path",) + OUTPUT_TOOLTIPS = ("Absolute path of ComfyUI's output directory.",) CATEGORY = "Basic/Path" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "execute" diff --git a/src/basic_data_handling/regex_nodes.py b/src/basic_data_handling/regex_nodes.py index a6cef22..91ef4f6 100644 --- a/src/basic_data_handling/regex_nodes.py +++ b/src/basic_data_handling/regex_nodes.py @@ -15,18 +15,20 @@ class IO: class RegexFindallDataList(ComfyNodeABC): """ - Returns all non-overlapping matches of a pattern in the string as a list of strings. + Returns all non-overlapping matches of a pattern in the string as a data list of strings. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), + "string": (IO.STRING, {"tooltip": "The text to search."}), + "pattern": (IO.STRING, {"tooltip": "Regular expression pattern to find."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("matches",) + OUTPUT_TOOLTIPS = ("Each non-overlapping match of the pattern, as a data list of strings.",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "findall" @@ -38,18 +40,20 @@ def findall(self, pattern: str, string: str) -> tuple[list[str]]: class RegexFindallList(ComfyNodeABC): """ - Returns all non-overlapping matches of a pattern in the string as a list of strings. + Returns all non-overlapping matches of a pattern in the string as a Python LIST. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), + "string": (IO.STRING, {"tooltip": "The text to search."}), + "pattern": (IO.STRING, {"tooltip": "Regular expression pattern to find."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("matches",) + OUTPUT_TOOLTIPS = ("Each non-overlapping match of the pattern, as a Python LIST of strings.",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "findall" @@ -67,12 +71,14 @@ class RegexGroupDict(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), + "string": (IO.STRING, {"tooltip": "The text to search."}), + "pattern": (IO.STRING, {"tooltip": "Regular expression pattern containing named groups, e.g. (?P...)."}), } } RETURN_TYPES = ("DICT",) + RETURN_NAMES = ("groups",) + OUTPUT_TOOLTIPS = ("Named capture groups mapped to their matched text (empty DICT when there is no match).",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "groupdict" @@ -86,19 +92,21 @@ def groupdict(self, pattern: str, string: str) -> tuple[dict]: class RegexSearchGroupsDataList(ComfyNodeABC): """ - Searches the string for a match to the pattern and returns a LIST of match groups. - If no match is found, it returns an empty LIST. + 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 { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), + "string": (IO.STRING, {"tooltip": "The text to search."}), + "pattern": (IO.STRING, {"tooltip": "Regular expression pattern with capturing groups, e.g. (\\d+)-(\\d+)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("groups",) + OUTPUT_TOOLTIPS = ("The captured groups of the first match, as a data list (empty when there is no match).",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "search_groups" @@ -113,19 +121,21 @@ def search_groups(self, pattern: str, string: str) -> tuple[list[str]]: class RegexSearchGroupsList(ComfyNodeABC): """ - Searches the string for a match to the pattern and returns a data list of match groups. - If no match is found, it returns an empty data list. + 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 { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), + "string": (IO.STRING, {"tooltip": "The text to search."}), + "pattern": (IO.STRING, {"tooltip": "Regular expression pattern with capturing groups, e.g. (\\d+)-(\\d+)."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("groups",) + OUTPUT_TOOLTIPS = ("The captured groups of the first match, as a Python LIST (empty when there is no match).",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "search_groups" @@ -139,18 +149,20 @@ def search_groups(self, pattern: str, string: str) -> tuple[list[str]]: class RegexSplitDataList(ComfyNodeABC): """ - Splits the string at each match of the pattern and returns a list of substrings. + Splits the string at each match of the pattern and returns a data list of substrings. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), + "string": (IO.STRING, {"tooltip": "The text to split."}), + "pattern": (IO.STRING, {"tooltip": "Regular expression pattern used as the delimiter."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("parts",) + OUTPUT_TOOLTIPS = ("The substrings between matches, as a data list.",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "split" @@ -162,18 +174,20 @@ def split(self, pattern: str, string: str) -> tuple[list[str]]: class RegexSplitList(ComfyNodeABC): """ - Splits the string at each match of the pattern and returns a list of substrings. + Splits the string at each match of the pattern and returns a Python LIST of substrings. """ @classmethod def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), + "string": (IO.STRING, {"tooltip": "The text to split."}), + "pattern": (IO.STRING, {"tooltip": "Regular expression pattern used as the delimiter."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("parts",) + OUTPUT_TOOLTIPS = ("The substrings between matches, as a Python LIST.",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "split" @@ -190,14 +204,16 @@ class RegexSub(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), - "repl": (IO.STRING, {}), - "count": ("INT", {"default": 0}), # 0 means replace all occurrences + "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 } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The string with the matched parts replaced.",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "sub" @@ -215,12 +231,14 @@ class RegexTest(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {}), - "pattern": (IO.STRING, {}), + "string": (IO.STRING, {"tooltip": "The text to test."}), + "pattern": (IO.STRING, {"tooltip": "Regular expression pattern to search for."}), } } RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("is_match",) + OUTPUT_TOOLTIPS = ("True when the pattern matches any part of the string, otherwise False.",) CATEGORY = "Basic/STRING/regex" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "test" diff --git a/src/basic_data_handling/set_nodes.py b/src/basic_data_handling/set_nodes.py index f704cea..3d317c8 100644 --- a/src/basic_data_handling/set_nodes.py +++ b/src/basic_data_handling/set_nodes.py @@ -27,11 +27,13 @@ class SetCreate(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.ANY, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("The new SET containing the provided items (duplicates removed).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_set" @@ -52,11 +54,13 @@ class SetCreateFromBoolean(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.BOOLEAN, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("The new SET containing the provided boolean items (duplicates removed).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_set" @@ -77,11 +81,13 @@ class SetCreateFromFloat(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.FLOAT, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("The new SET containing the provided float items (duplicates removed).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_set" @@ -102,11 +108,13 @@ class SetCreateFromInt(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.INT, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("The new SET containing the provided integer items (duplicates removed).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_set" @@ -127,11 +135,13 @@ class SetCreateFromString(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": ContainsDynamicDict({ - "item_0": (IO.STRING, {"_dynamic": "number", "widgetType": "STRING"}), + "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",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("The new SET containing the provided string items (duplicates removed).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_set" @@ -152,12 +162,14 @@ class SetAdd(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), - "item": (IO.ANY, {}), + "set": ("SET", {"tooltip": "The SET to add to."}), + "item": (IO.ANY, {"tooltip": "The item to add."}), } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("The SET with the item added (unchanged when the item was already present).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "add" @@ -179,12 +191,13 @@ class SetAll(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET to evaluate."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("all_true",) + OUTPUT_TOOLTIPS = ("True when every element is truthy (or the SET is empty).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_all" @@ -204,12 +217,13 @@ class SetAny(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET to evaluate."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("any_true",) + OUTPUT_TOOLTIPS = ("True when at least one element is truthy (False for an empty SET).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "check_any" @@ -229,13 +243,14 @@ class SetContains(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), - "value": (IO.ANY, {}), + "set": ("SET", {"tooltip": "The SET to search."}), + "value": (IO.ANY, {"tooltip": "The value to look for."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("contains",) + OUTPUT_TOOLTIPS = ("True when the value is present in the SET.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "contains" @@ -255,12 +270,14 @@ class SetDifference(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), + "set1": ("SET", {"tooltip": "The SET to subtract from."}), + "set2": ("SET", {"tooltip": "The SET of elements to remove."}), } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("Elements in set1 that are not in set2.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "difference" @@ -282,12 +299,14 @@ class SetDiscard(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), - "item": (IO.ANY, {}), + "set": ("SET", {"tooltip": "The SET to remove from."}), + "item": (IO.ANY, {"tooltip": "The item to remove if present."}), } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("The SET with the item removed (unchanged when it was absent).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "discard" @@ -313,14 +332,16 @@ class SetEnumerate(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET to enumerate."}), }, "optional": { - "start": ("INT", {"default": 0}), + "start": ("INT", {"default": 0, "tooltip": "Index value assigned to the first element."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("enumerated",) + OUTPUT_TOOLTIPS = ("List of (index, value) pairs. Order is arbitrary but stable within one operation.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "enumerate_set" @@ -340,16 +361,18 @@ class SetIntersection(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), + "set1": ("SET", {"tooltip": "First SET."}), + "set2": ("SET", {"tooltip": "Second SET."}), }, "optional": { - "set3": ("SET", {}), - "set4": ("SET", {}), + "set3": ("SET", {"tooltip": "Optional additional SET."}), + "set4": ("SET", {"tooltip": "Optional additional SET."}), } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("Elements present in all of the input SETs.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "intersection" @@ -377,13 +400,14 @@ class SetIsDisjoint(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), + "set1": ("SET", {"tooltip": "First SET."}), + "set2": ("SET", {"tooltip": "Second SET."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("is_disjoint",) + OUTPUT_TOOLTIPS = ("True when the two SETs share no elements.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "is_disjoint" @@ -403,13 +427,14 @@ class SetIsSubset(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), + "set1": ("SET", {"tooltip": "Candidate subset."}), + "set2": ("SET", {"tooltip": "SET that may contain all of set1's elements."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("is_subset",) + OUTPUT_TOOLTIPS = ("True when every element of set1 is also in set2.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "is_subset" @@ -429,13 +454,14 @@ class SetIsSuperset(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), + "set1": ("SET", {"tooltip": "Candidate superset."}), + "set2": ("SET", {"tooltip": "SET whose elements must all be in set1."}), } } RETURN_TYPES = (IO.BOOLEAN,) RETURN_NAMES = ("is_superset",) + OUTPUT_TOOLTIPS = ("True when set1 contains every element of set2.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "is_superset" @@ -454,12 +480,13 @@ class SetLength(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET to measure."}), } } RETURN_TYPES = (IO.INT,) RETURN_NAMES = ("length",) + OUTPUT_TOOLTIPS = ("The number of elements in the SET.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "length" @@ -480,12 +507,13 @@ class SetPop(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET to pop an item from."}), } } RETURN_TYPES = ("SET", IO.ANY) RETURN_NAMES = ("set", "item") + OUTPUT_TOOLTIPS = ("The SET with an arbitrary item removed.", "The removed item (None when the SET was empty).") CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop" @@ -511,15 +539,16 @@ class SetPopRandom(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET to pop a random element from."}), }, "optional": { - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}), + "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 = ("SET", IO.ANY) RETURN_NAMES = ("set", "item") + OUTPUT_TOOLTIPS = ("The SET with a random element removed.", "The removed element (None when the SET was empty).") CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "pop_random_element" @@ -554,13 +583,14 @@ class SetRemove(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), - "item": (IO.ANY, {}), + "set": ("SET", {"tooltip": "The SET to remove from."}), + "item": (IO.ANY, {"tooltip": "The item to remove."}), } } RETURN_TYPES = ("SET", IO.BOOLEAN) RETURN_NAMES = ("set", "success") + OUTPUT_TOOLTIPS = ("The SET with the item removed.", "True when the item was present and removed.") CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "remove" @@ -589,15 +619,16 @@ class SetSum(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET of numbers to sum."}), }, "optional": { - "start": ("INT", {"default": 0}), + "start": ("INT", {"default": 0, "tooltip": "Initial value added to the sum."}), } } 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 "") FUNCTION = "sum_set" @@ -618,12 +649,14 @@ class SetSymmetricDifference(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), + "set1": ("SET", {"tooltip": "First SET."}), + "set2": ("SET", {"tooltip": "Second SET."}), } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("Elements present in exactly one of the two SETs.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "symmetric_difference" @@ -645,16 +678,18 @@ class SetUnion(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set1": ("SET", {}), - "set2": ("SET", {}), + "set1": ("SET", {"tooltip": "First SET."}), + "set2": ("SET", {"tooltip": "Second SET."}), }, "optional": { - "set3": ("SET", {}), - "set4": ("SET", {}), + "set3": ("SET", {"tooltip": "Optional additional SET."}), + "set4": ("SET", {"tooltip": "Optional additional SET."}), } } RETURN_TYPES = ("SET",) + RETURN_NAMES = ("set",) + OUTPUT_TOOLTIPS = ("All elements from every input SET (duplicates removed).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "union" @@ -684,11 +719,13 @@ class SetToDataList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET to convert."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("items",) + OUTPUT_TOOLTIPS = ("The SET's elements as a ComfyUI data list.",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert" @@ -710,11 +747,13 @@ class SetToList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "set": ("SET", {}), + "set": ("SET", {"tooltip": "The SET to convert."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("list",) + OUTPUT_TOOLTIPS = ("The SET's elements as a Python LIST (order is arbitrary).",) CATEGORY = "Basic/SET" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "convert" diff --git a/src/basic_data_handling/string_nodes.py b/src/basic_data_handling/string_nodes.py index 02dc638..c147c54 100644 --- a/src/basic_data_handling/string_nodes.py +++ b/src/basic_data_handling/string_nodes.py @@ -18,11 +18,13 @@ class StringCapitalize(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to capitalize."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with its first character uppercased and the rest lowercased.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "capitalize" @@ -43,11 +45,13 @@ class StringCasefold(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to casefold."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text casefolded for case-insensitive comparisons.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "casefold" @@ -68,15 +72,17 @@ class StringCenter(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "width": (IO.INT, {"default": 20, "min": 0, "max": 1000}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to center."}), + "width": (IO.INT, {"default": 20, "min": 0, "max": 1000, "tooltip": "Total width of the resulting field."}), }, "optional": { - "fillchar": (IO.STRING, {"default": " "}), + "fillchar": (IO.STRING, {"default": " ", "tooltip": "Padding character (only its first character is used)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text centered within the field width.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "center" @@ -97,12 +103,14 @@ class StringConcat(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string_a": (IO.STRING, {"default": ""}), - "string_b": (IO.STRING, {"default": ""}), + "string_a": (IO.STRING, {"default": "", "tooltip": "First text (left side)."}), + "string_b": (IO.STRING, {"default": "", "tooltip": "Second text (right side)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The two texts joined end-to-end.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "concat" @@ -122,16 +130,18 @@ class StringCount(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "substring": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to search in."}), + "substring": (IO.STRING, {"default": "", "tooltip": "The text whose occurrences are counted."}), }, "optional": { - "start": (IO.INT, {"default": 0, "min": 0}), - "end": (IO.INT, {"default": 0, "min": 0}), + "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,) + RETURN_NAMES = ("count",) + OUTPUT_TOOLTIPS = ("The number of non-overlapping occurrences of the substring.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "count" @@ -156,13 +166,15 @@ class StringDecode(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "bytes_string": (IO.STRING, {"default": "b''"}), - "encoding": (["utf-8", "ascii", "latin-1", "utf-16", "utf-32", "cp1252"],), - "errors": (["strict", "ignore", "replace", "backslashreplace"],), + "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."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The decoded text (or an error message when decoding fails).",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "decode" @@ -205,13 +217,15 @@ class StringEncode(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "encoding": (["utf-8", "ascii", "latin-1", "utf-16", "utf-32", "cp1252"],), - "errors": (["strict", "ignore", "replace", "xmlcharrefreplace", "backslashreplace"],), + "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."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The bytes as a string representation (or an error message on failure).",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "encode" @@ -241,16 +255,18 @@ class StringEndswith(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "suffix": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to test."}), + "suffix": (IO.STRING, {"default": "", "tooltip": "The suffix to look for."}), }, "optional": { - "start": (IO.INT, {"default": 0, "min": 0}), - "end": (IO.INT, {"default": 0, "min": 0}), + "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,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the text ends with the suffix.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "endswith" @@ -275,14 +291,16 @@ class StringExpandtabs(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to expand."}), }, "optional": { - "tabsize": (IO.INT, {"default": 8, "min": 1, "max": 100}), + "tabsize": (IO.INT, {"default": 8, "min": 1, "max": 100, "tooltip": "Number of spaces each tab expands to."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with tab characters replaced by spaces.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "expandtabs" @@ -303,16 +321,18 @@ class StringFind(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "substring": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to search in."}), + "substring": (IO.STRING, {"default": "", "tooltip": "The text to search for."}), }, "optional": { - "start": (IO.INT, {"default": 0, "min": 0}), - "end": (IO.INT, {"default": 0, "min": 0}), + "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,) + RETURN_NAMES = ("index",) + OUTPUT_TOOLTIPS = ("Index of the first occurrence, or -1 when not found.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "find" @@ -338,12 +358,14 @@ class StringFormatMap(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "template": (IO.STRING, {"default": "Hello, {key}"}), - "mapping": ("DICT", {"default": {}}), + "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."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The formatted string (or an error message when a key is missing).",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "format_map" @@ -370,12 +392,14 @@ class StringIn(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "substring": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to search in."}), + "substring": (IO.STRING, {"default": "", "tooltip": "The text to look for."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("contains",) + OUTPUT_TOOLTIPS = ("True when the substring occurs in the text.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "contains" @@ -395,11 +419,13 @@ class StringIsAlnum(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all characters are alphanumeric and the string is non-empty.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isalnum" @@ -419,11 +445,13 @@ class StringIsAlpha(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all characters are alphabetic and the string is non-empty.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isalpha" @@ -443,11 +471,13 @@ class StringIsAscii(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when every character is in the ASCII character set.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isascii" @@ -469,11 +499,13 @@ class StringIsDecimal(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all characters are decimal digits and the string is non-empty.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isdecimal" @@ -493,11 +525,13 @@ class StringIsDigit(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all characters are digits and the string is non-empty.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isdigit" @@ -518,11 +552,13 @@ class StringIsIdentifier(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the string is a valid Python identifier.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isidentifier" @@ -542,11 +578,13 @@ class StringIsLower(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all cased characters are lowercase and at least one exists.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "islower" @@ -567,11 +605,13 @@ class StringIsNumeric(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all characters are numeric and the string is non-empty.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isnumeric" @@ -591,11 +631,13 @@ class StringIsPrintable(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all characters are printable.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isprintable" @@ -615,11 +657,13 @@ class StringIsSpace(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all characters are whitespace and the string is non-empty.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isspace" @@ -640,11 +684,13 @@ class StringIsTitle(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the string is titlecased.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "istitle" @@ -664,11 +710,13 @@ class StringIsUpper(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The string to test."}), } } RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when all cased characters are uppercase and at least one exists.",) CATEGORY = "Basic/STRING/is" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "isupper" @@ -688,11 +736,13 @@ class StringLength(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to measure."}), } } RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("length",) + OUTPUT_TOOLTIPS = ("The number of characters in the text.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "length" @@ -713,12 +763,14 @@ class StringDataListJoin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "strings": (IO.STRING, {"forceInput": True}), - "sep": (IO.STRING, {"default": " "}), + "strings": (IO.STRING, {"forceInput": True, "tooltip": "Data list of strings to join."}), + "sep": (IO.STRING, {"default": " ", "tooltip": "Separator placed between each pair of strings."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The strings joined with the separator.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "join" @@ -741,12 +793,14 @@ class StringListJoin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "strings": ("LIST", {"forceInput": True}), - "sep": (IO.STRING, {"default": " "}), + "strings": ("LIST", {"forceInput": True, "tooltip": "LIST of strings to join."}), + "sep": (IO.STRING, {"default": " ", "tooltip": "Separator placed between each pair of strings."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The strings joined with the separator.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "join" @@ -766,15 +820,17 @@ class StringLjust(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "width": (IO.INT, {"default": 10, "min": 0}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to align."}), + "width": (IO.INT, {"default": 10, "min": 0, "tooltip": "Total width of the resulting field."}), }, "optional": { - "fillchar": (IO.STRING, {"default": " "}), + "fillchar": (IO.STRING, {"default": " ", "tooltip": "Padding character (only its first character is used)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text left-aligned within the field width.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "ljust" @@ -796,11 +852,13 @@ class StringLower(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to convert."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text converted to lowercase.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "lower" @@ -821,14 +879,16 @@ class StringLstrip(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to strip."}), }, "optional": { - "chars": (IO.STRING, {"default": ""}), + "chars": (IO.STRING, {"default": "", "tooltip": "Set of characters to remove from the start (whitespace when empty)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with leading characters removed.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "lstrip" @@ -850,12 +910,14 @@ class StringRemoveprefix(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "prefix": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to modify."}), + "prefix": (IO.STRING, {"default": "", "tooltip": "The prefix to remove when present."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text without the prefix (unchanged when the prefix is absent).",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "removeprefix" @@ -875,12 +937,14 @@ class StringRemovesuffix(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "suffix": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to modify."}), + "suffix": (IO.STRING, {"default": "", "tooltip": "The suffix to remove when present."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text without the suffix (unchanged when the suffix is absent).",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "removesuffix" @@ -901,11 +965,13 @@ class StringUnescape(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to unescape."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with escape sequences converted to their actual characters.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "unescape" @@ -936,11 +1002,13 @@ class StringEscape(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to escape."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with special characters turned into escape sequences.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "escape" @@ -969,16 +1037,18 @@ class StringReplace(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "old": (IO.STRING, {"default": ""}), - "new": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to modify."}), + "old": (IO.STRING, {"default": "", "tooltip": "The substring to replace."}), + "new": (IO.STRING, {"default": "", "tooltip": "The replacement text."}), }, "optional": { - "count": (IO.INT, {"default": -1, "min": -1}), + "count": (IO.INT, {"default": -1, "min": -1, "tooltip": "Maximum number of replacements (-1 replaces all)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with the occurrences replaced.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "replace" @@ -999,16 +1069,18 @@ class StringRfind(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "substring": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to search in."}), + "substring": (IO.STRING, {"default": "", "tooltip": "The text to search for."}), }, "optional": { - "start": (IO.INT, {"default": 0, "min": 0}), - "end": (IO.INT, {"default": 0, "min": 0}), + "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,) + RETURN_NAMES = ("index",) + OUTPUT_TOOLTIPS = ("Index of the last occurrence, or -1 when not found.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "rfind" @@ -1032,15 +1104,17 @@ class StringRjust(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "width": (IO.INT, {"default": 10, "min": 0}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to align."}), + "width": (IO.INT, {"default": 10, "min": 0, "tooltip": "Total width of the resulting field."}), }, "optional": { - "fillchar": (IO.STRING, {"default": " "}), + "fillchar": (IO.STRING, {"default": " ", "tooltip": "Padding character (only its first character is used)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text right-aligned within the field width.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "rjust" @@ -1065,15 +1139,17 @@ class StringRsplitDataList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to split."}), }, "optional": { - "sep": (IO.STRING, {"default": ""}), - "maxsplit": (IO.INT, {"default": -1, "min": -1}), + "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,) + RETURN_NAMES = ("parts",) + OUTPUT_TOOLTIPS = ("The substrings as a data list.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "rsplit" @@ -1098,15 +1174,17 @@ class StringRsplitList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to split."}), }, "optional": { - "sep": (IO.STRING, {"default": ""}), - "maxsplit": (IO.INT, {"default": -1, "min": -1}), + "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",) + RETURN_NAMES = ("parts",) + OUTPUT_TOOLTIPS = ("The substrings as a Python LIST.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "rsplit" @@ -1129,14 +1207,16 @@ class StringRstrip(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to strip."}), }, "optional": { - "chars": (IO.STRING, {"default": ""}), + "chars": (IO.STRING, {"default": "", "tooltip": "Set of characters to remove from the end (whitespace when empty)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with trailing characters removed.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "rstrip" @@ -1162,18 +1242,20 @@ class StringSplitDataList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to split."}), }, "optional": { - "sep": (IO.STRING, {"default": ""}), - "maxsplit": (IO.INT, {"default": -1, "min": -1}), + "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,) - FUNCTION = "split" + RETURN_NAMES = ("parts",) + OUTPUT_TOOLTIPS = ("The substrings as a data list.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "split" OUTPUT_IS_LIST = (True,) # This indicates that the output is a data list def split(self, string, sep=None, maxsplit=-1): @@ -1200,18 +1282,20 @@ class StringSplitList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to split."}), }, "optional": { - "sep": (IO.STRING, {"default": ""}), - "maxsplit": (IO.INT, {"default": -1, "min": -1}), + "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",) - FUNCTION = "split" + RETURN_NAMES = ("parts",) + OUTPUT_TOOLTIPS = ("The substrings as a Python LIST.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") + FUNCTION = "split" def split(self, string, sep=None, maxsplit=-1): if sep == "": @@ -1234,14 +1318,16 @@ class StringSplitlinesDataList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to split into lines."}), }, "optional": { - "keepends": (IO.BOOLEAN, {"default": False}), + "keepends": (IO.BOOLEAN, {"default": False, "tooltip": "Keep the line break characters in the resulting lines."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("lines",) + OUTPUT_TOOLTIPS = ("The lines as a data list.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "splitlines" @@ -1264,14 +1350,16 @@ class StringSplitlinesList(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to split into lines."}), }, "optional": { - "keepends": (IO.BOOLEAN, {"default": False}), + "keepends": (IO.BOOLEAN, {"default": False, "tooltip": "Keep the line break characters in the resulting lines."}), } } RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("lines",) + OUTPUT_TOOLTIPS = ("The lines as a Python LIST.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "splitlines" @@ -1293,16 +1381,18 @@ class StringStartswith(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "prefix": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to test."}), + "prefix": (IO.STRING, {"default": "", "tooltip": "The prefix to look for."}), }, "optional": { - "start": (IO.INT, {"default": 0, "min": 0}), - "end": (IO.INT, {"default": 0, "min": 0}), + "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,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("True when the text starts with the prefix.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "startswith" @@ -1327,14 +1417,16 @@ class StringStrip(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to strip."}), }, "optional": { - "chars": (IO.STRING, {"default": ""}), + "chars": (IO.STRING, {"default": "", "tooltip": "Set of characters to remove from both ends (whitespace when empty)."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with leading and trailing characters removed.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "strip" @@ -1356,11 +1448,13 @@ class StringSwapcase(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text whose letter case is swapped."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text with each character's case inverted.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "swapcase" @@ -1380,11 +1474,13 @@ class StringTitle(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to titlecase."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text in titlecase (each word starts with an uppercase letter).",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "title" @@ -1403,11 +1499,13 @@ class StringUpper(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to convert."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text converted to uppercase.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "upper" @@ -1428,12 +1526,14 @@ class StringZfill(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "string": (IO.STRING, {"default": ""}), - "width": (IO.INT, {"default": 10, "min": 0}), + "string": (IO.STRING, {"default": "", "tooltip": "The text to pad with zeros."}), + "width": (IO.INT, {"default": 10, "min": 0, "tooltip": "Total width of the resulting field."}), } } RETURN_TYPES = (IO.STRING,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The text zero-padded on the left to the given width.",) CATEGORY = "Basic/STRING" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "zfill" diff --git a/src/basic_data_handling/tensor_nodes.py b/src/basic_data_handling/tensor_nodes.py index 52b4683..1713724 100644 --- a/src/basic_data_handling/tensor_nodes.py +++ b/src/basic_data_handling/tensor_nodes.py @@ -30,11 +30,13 @@ class TensorCreate(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input": (IO.ANY, {}), + "input": (IO.ANY, {"tooltip": "A number, list/tuple, or existing tensor to convert."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("tensor",) + OUTPUT_TOOLTIPS = ("The resulting PyTorch tensor.",) CATEGORY = "Basic/tensor" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create" @@ -55,13 +57,15 @@ class TensorBinaryOp(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "a": (IO.ANY, {}), - "b": (IO.ANY, {}), - "operation": (["add", "subtract", "multiply", "divide", "power", "remainder", "floor_divide"], {"default": "add"}), + "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."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("tensor",) + OUTPUT_TOOLTIPS = ("The tensor resulting from applying the operation element-wise.",) CATEGORY = "Basic/tensor" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "operate" @@ -95,12 +99,14 @@ class TensorUnaryOp(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "input": (IO.ANY, {}), - "operation": (["abs", "neg", "exp", "log", "sin", "cos", "sqrt", "sigmoid", "relu"], {"default": "abs"}), + "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."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("tensor",) + OUTPUT_TOOLTIPS = ("The tensor resulting from applying the operation element-wise.",) CATEGORY = "Basic/tensor" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "operate" @@ -137,12 +143,14 @@ class TensorSlice(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "tensor": (IO.ANY, {}), - "slice_str": (IO.STRING, {"default": ":"}), + "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'."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("tensor",) + OUTPUT_TOOLTIPS = ("The sliced tensor.",) CATEGORY = "Basic/tensor" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "slice_tensor" @@ -179,12 +187,14 @@ class TensorReshape(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "tensor": (IO.ANY, {}), - "shape": (IO.STRING, {"default": "-1"}), + "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'."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("tensor",) + OUTPUT_TOOLTIPS = ("The reshaped tensor.",) CATEGORY = "Basic/tensor" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "reshape" @@ -207,12 +217,14 @@ class TensorPermute(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "tensor": (IO.ANY, {}), - "dims": (IO.STRING, {"default": "0, 1"}), + "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'."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("tensor",) + OUTPUT_TOOLTIPS = ("The tensor with its dimensions permuted.",) CATEGORY = "Basic/tensor" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "permute" @@ -235,14 +247,16 @@ class TensorJoin(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "tensor1": (IO.ANY, {}), - "tensor2": (IO.ANY, {}), - "dim": (IO.INT, {"default": 0}), - "mode": (["concatenate", "stack"], {"default": "concatenate"}), + "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."}), } } RETURN_TYPES = (IO.ANY,) + RETURN_NAMES = ("tensor",) + OUTPUT_TOOLTIPS = ("The joined tensor.",) CATEGORY = "Basic/tensor" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "join" @@ -264,12 +278,13 @@ class TensorInfo(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "tensor": (IO.ANY, {}), + "tensor": (IO.ANY, {"tooltip": "The tensor to inspect."}), } } 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.") CATEGORY = "Basic/tensor" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_info" diff --git a/src/basic_data_handling/time_nodes.py b/src/basic_data_handling/time_nodes.py index 6b7f20b..951b159 100644 --- a/src/basic_data_handling/time_nodes.py +++ b/src/basic_data_handling/time_nodes.py @@ -29,12 +29,13 @@ class TimeNow(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": { - "trigger": (IO.ANY, {"description": "Optional input to trigger execution"}) + "trigger": (IO.ANY, {"tooltip": "Optional input to trigger a fresh evaluation."}) } } RETURN_TYPES = (IO.DATETIME,) RETURN_NAMES = ("now",) + OUTPUT_TOOLTIPS = ("The current local date and time; a new value every run.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_now" @@ -61,12 +62,13 @@ class TimeNowUTC(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": { - "trigger": (IO.ANY, {"description": "Optional input to trigger execution"}) + "trigger": (IO.ANY, {"tooltip": "Optional input to trigger a fresh evaluation."}) } } RETURN_TYPES = (IO.DATETIME,) RETURN_NAMES = ("now",) + OUTPUT_TOOLTIPS = ("The current UTC date and time (timezone-aware); a new value every run.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "get_now_utc" @@ -92,12 +94,13 @@ class TimeToUnix(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "datetime": (IO.DATETIME, {}), + "datetime": (IO.DATETIME, {"tooltip": "The DATETIME to convert."}), } } RETURN_TYPES = (IO.FLOAT,) RETURN_NAMES = ("unix_timestamp",) + OUTPUT_TOOLTIPS = ("Seconds since the Unix epoch (1970-01-01 UTC).",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "to_unix" @@ -117,12 +120,13 @@ class UnixToTime(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "unix_timestamp": (IO.NUMBER, {"default": 0.0}), + "unix_timestamp": (IO.NUMBER, {"default": 0.0, "tooltip": "Seconds since the Unix epoch (float or int)."}), } } RETURN_TYPES = (IO.DATETIME,) RETURN_NAMES = ("datetime",) + OUTPUT_TOOLTIPS = ("The DATETIME corresponding to the timestamp, in the local timezone.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "from_unix" @@ -143,13 +147,14 @@ class TimeFormat(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "datetime": (IO.DATETIME, {}), - "format_string": (IO.STRING, {"default": "%Y-%m-%d %H:%M:%S"}), + "datetime": (IO.DATETIME, {"tooltip": "The DATETIME to format."}), + "format_string": (IO.STRING, {"default": "%Y-%m-%d %H:%M:%S", "tooltip": "strftime format code, e.g. %Y-%m-%d %H:%M:%S."}), } } RETURN_TYPES = (IO.STRING,) RETURN_NAMES = ("formatted_string",) + OUTPUT_TOOLTIPS = ("The DATETIME rendered as a string using the format code.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "format_time" @@ -169,13 +174,14 @@ class TimeParse(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "time_string": (IO.STRING, {}), - "format_string": (IO.STRING, {"default": "%Y-%m-%d %H:%M:%S"}), + "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."}), } } RETURN_TYPES = (IO.DATETIME,) RETURN_NAMES = ("datetime",) + OUTPUT_TOOLTIPS = ("The parsed DATETIME.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "parse_time" @@ -195,17 +201,19 @@ class TimeDelta(ComfyNodeABC): def INPUT_TYPES(cls): return { "optional": { - "days": (IO.FLOAT, {"default": 0}), - "seconds": (IO.FLOAT, {"default": 0}), - "microseconds": (IO.FLOAT, {"default": 0}), - "milliseconds": (IO.FLOAT, {"default": 0}), - "minutes": (IO.FLOAT, {"default": 0}), - "hours": (IO.FLOAT, {"default": 0}), - "weeks": (IO.FLOAT, {"default": 0}), + "days": (IO.FLOAT, {"default": 0, "tooltip": "Number of days."}), + "seconds": (IO.FLOAT, {"default": 0, "tooltip": "Number of seconds."}), + "microseconds": (IO.FLOAT, {"default": 0, "tooltip": "Number of microseconds."}), + "milliseconds": (IO.FLOAT, {"default": 0, "tooltip": "Number of milliseconds."}), + "minutes": (IO.FLOAT, {"default": 0, "tooltip": "Number of minutes."}), + "hours": (IO.FLOAT, {"default": 0, "tooltip": "Number of hours."}), + "weeks": (IO.FLOAT, {"default": 0, "tooltip": "Number of weeks."}), } } RETURN_TYPES = (IO.TIMEDELTA,) + RETURN_NAMES = ("delta",) + OUTPUT_TOOLTIPS = ("A TIMEDELTA representing the combined duration.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "create_delta" @@ -225,12 +233,14 @@ class TimeAddDelta(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "datetime": (IO.DATETIME, {}), - "delta": (IO.TIMEDELTA, {}), + "datetime": (IO.DATETIME, {"tooltip": "The DATETIME to add the duration to."}), + "delta": (IO.TIMEDELTA, {"tooltip": "The duration (TIMEDELTA) to add."}), } } RETURN_TYPES = (IO.DATETIME,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The DATETIME advanced by the given duration.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "add" @@ -250,12 +260,14 @@ class TimeSubtractDelta(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "datetime": (IO.DATETIME, {}), - "delta": (IO.TIMEDELTA, {}), + "datetime": (IO.DATETIME, {"tooltip": "The DATETIME to subtract the duration from."}), + "delta": (IO.TIMEDELTA, {"tooltip": "The duration (TIMEDELTA) to subtract."}), } } RETURN_TYPES = (IO.DATETIME,) + RETURN_NAMES = ("result",) + OUTPUT_TOOLTIPS = ("The DATETIME moved back by the given duration.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "subtract" @@ -275,12 +287,14 @@ class TimeDifference(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "datetime1": (IO.DATETIME, {}), - "datetime2": (IO.DATETIME, {}), + "datetime1": (IO.DATETIME, {"tooltip": "First date/time (the minuend)."}), + "datetime2": (IO.DATETIME, {"tooltip": "Second date/time, subtracted from the first."}), } } RETURN_TYPES = (IO.TIMEDELTA,) + RETURN_NAMES = ("delta",) + OUTPUT_TOOLTIPS = ("The duration datetime1 - datetime2 (negative when datetime1 is earlier).",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "difference" @@ -300,11 +314,12 @@ class TimeExtract(ComfyNodeABC): @classmethod def INPUT_TYPES(cls): return { - "required": { "datetime": (IO.DATETIME, {}) } + "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).") CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "extract" @@ -326,12 +341,13 @@ class TimeDeltaToSeconds(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "delta": (IO.TIMEDELTA, {}), + "delta": (IO.TIMEDELTA, {"tooltip": "The duration to convert."}), } } RETURN_TYPES = (IO.FLOAT,) RETURN_NAMES = ("seconds",) + OUTPUT_TOOLTIPS = ("The duration expressed as a float in seconds.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "to_seconds" @@ -352,12 +368,13 @@ class TimeDeltaToMilliseconds(ComfyNodeABC): def INPUT_TYPES(cls): return { "required": { - "delta": (IO.TIMEDELTA, {}), + "delta": (IO.TIMEDELTA, {"tooltip": "The duration to convert."}), } } RETURN_TYPES = (IO.INT,) RETURN_NAMES = ("milliseconds",) + OUTPUT_TOOLTIPS = ("The duration expressed as an integer number of milliseconds, rounded to the nearest millisecond.",) CATEGORY = "Basic/time" DESCRIPTION = cleandoc(__doc__ or "") FUNCTION = "to_milliseconds" diff --git a/tests/test_path_nodes.py b/tests/test_path_nodes.py index 65b447d..3ed6fb6 100644 --- a/tests/test_path_nodes.py +++ b/tests/test_path_nodes.py @@ -15,6 +15,17 @@ ) +def decode_user_comment(value): + """Decode an EXIF UserComment value like piexif/A1111 readers do.""" + if isinstance(value, bytes): + if value.startswith(b"ASCII\x00\x00\x00"): + return value[8:].decode("utf-8", errors="ignore") + if value.startswith(b"UNICODE\x00"): + return value[8:].decode("utf-16-be", errors="ignore") + return value.decode("utf-8", errors="ignore") + return value + + def test_path_join(): node = PathJoin() assert node.join_paths("folder", "file.txt") == (os.path.join("folder", "file.txt"),) @@ -459,6 +470,124 @@ def mock_load_image_helper(path): load_node.load_image_rgb(str(tmp_path / "nonexistent.png")) +def test_path_save_image_rgb_prompt_metadata(tmp_path): + save_node = PathSaveImageRGB() + img_size = (16, 16) + red_img = torch.zeros(1, img_size[1], img_size[0], 3) + red_img[0, :, :, 0] = 1.0 # Red channel set to 1 + + # No metadata when neither prompt nor negative prompt is provided + plain_path = str(tmp_path / "plain") + assert save_node.save_image(red_img, plain_path) == (True,) + with Image.open(plain_path + ".png") as img: + img.load() + assert img.text.get("parameters") is None + + # 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,) + with Image.open(full_path + ".png") as img: + img.load() + assert img.text.get("parameters") == "a red square\nNegative prompt: blurry, low quality" + + # Only the prompt is provided + pos_path = str(tmp_path / "pos_only") + assert save_node.save_image(red_img, pos_path, prompt="only positive") == (True,) + with Image.open(pos_path + ".png") as img: + img.load() + assert img.text.get("parameters") == "only positive" + + # Only the negative prompt is provided + neg_path = str(tmp_path / "neg_only") + assert save_node.save_image(red_img, neg_path, negative_prompt="only negative") == (True,) + with Image.open(neg_path + ".png") as img: + img.load() + assert img.text.get("parameters") == "Negative prompt: only negative" + + # 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 os.path.exists(jpg_path + ".jpg") + with Image.open(jpg_path + ".jpg") as img: + img.load() + exif = img.getexif() + expected = "a red square\nNegative prompt: blurry, low quality" + assert decode_user_comment(exif.get_ifd(0x8769).get(0x9286)) == expected + assert exif.get(0x010E) == expected + + # 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,) + with Image.open(webp_path + ".webp") as img: + img.load() + exif_bytes = img.info.get("exif", b"") + if exif_bytes.startswith(b"Exif\x00\x00"): + exif_bytes = exif_bytes[6:] + exif = Image.Exif() + exif.load(exif_bytes) + assert decode_user_comment(exif.get_ifd(0x8769).get(0x9286)) == expected + assert 0x010E not in exif + + +def test_path_save_image_rgba_prompt_metadata(tmp_path): + save_node = PathSaveImageRGBA() + img_size = (16, 16) + red_img = torch.zeros(1, img_size[1], img_size[0], 3) + red_img[0, :, :, 0] = 1.0 # Red channel set to 1 + mask = torch.zeros(1, img_size[1], img_size[0]) + + # No metadata when neither prompt nor negative prompt is provided + plain_path = str(tmp_path / "plain") + assert save_node.save_image_with_mask(red_img, mask, plain_path) == (True,) + with Image.open(plain_path + ".png") as img: + img.load() + assert img.text.get("parameters") is None + + # 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,) + with Image.open(full_path + ".png") as img: + img.load() + assert img.text.get("parameters") == "a red square\nNegative prompt: blurry, low quality" + + +def test_path_save_image_jxl_prompt_metadata(tmp_path): + pytest.importorskip("pillow_jxl") + save_node = PathSaveImageRGB() + img_size = (16, 16) + red_img = torch.zeros(1, img_size[1], img_size[0], 3) + red_img[0, :, :, 0] = 1.0 # Red channel set to 1 + + 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 os.path.exists(jxl_path + ".jxl") + + # JXL embeds the prompt into the EXIF UserComment field + with Image.open(jxl_path + ".jxl") as img: + img.load() + exif_bytes = img.info.get("exif", b"") + if exif_bytes.startswith(b"Exif\x00\x00"): + exif_bytes = exif_bytes[6:] + exif = Image.Exif() + exif.load(exif_bytes) + expected = "a red square \nNegative prompt: blurry, low quality" + assert decode_user_comment(exif.get_ifd(0x8769).get(0x9286)) == expected + + # JXL also embeds the prompt (XML-escaped) in an XMP xml box + raw = (tmp_path / "jxl_meta.jxl").read_bytes() + assert b"xml " in raw + assert b"" in raw + escaped = "a red square <lora:x:1.0>\nNegative prompt: blurry, low quality" + assert escaped.encode("utf-8") in raw + def test_path_normalize(): node = PathNormalize()