feat: reverse FormatString output order for reliability

BREAKING CHANGE: Output order changed to put primary outputs first

- formatted_string now always in output position 0 (was dynamic)
- saved_file_path now always in output position 1 (was dynamic)
- Variable pass-through outputs now in positions 2+ (were 0+)

This change ensures primary outputs are in fixed, predictable positions,
resolving ComfyUI output mapping issues when dynamic outputs are present.

Changes:
- Updated format_string() return statement to reverse output order
- Updated update_widget() to set RETURN_TYPES/RETURN_NAMES in new order
- Updated all 47 tests to expect new output positions
- Updated all docstrings and examples in format_string.py
- Added comprehensive "Output Structure" documentation to README.md
- Added same documentation to docs/index.md for consistency
- All tests passing ✅
This commit is contained in:
darth-veitcher
2025-11-05 15:48:22 +00:00
parent a3fa7b41f1
commit a68b6e79b2
3 changed files with 65 additions and 22 deletions
+19
View File
@@ -7,6 +7,8 @@ A collection of workflow efficiency and quality of life nodes that I've created
## String Formatting
The FormatString node provides flexible string formatting with dynamic input/output configuration.
### Python F-String
A simple python f-string dynamically creates the necessary inputs/outputs for the detected keys.
@@ -19,6 +21,23 @@ Switching to Jinja2 allows you to use more advanced control blocks and other fil
![jinja2](docs/assets/jinja2.png)
### Output Structure
The node's outputs are organized for maximum reliability and flexibility:
1. **`formatted_string`** (Output 0): The formatted result string - always in position 0
2. **`saved_file_path`** (Output 1): Path to saved state file (if save_path provided) - always in position 1
3. **Variable outputs** (Output 2+): Pass-through values for any variables detected in the template, enabling easy chaining
For example, with template `"Hello {name}, you are {age}"`:
* Output 0: The formatted string (e.g., "Hello Alice, you are 30")
* Output 1: The save file path (or empty string)
* Output 2: The value of `name` (e.g., "Alice")
* Output 3: The value of `age` (e.g., "30")
This structure ensures the primary outputs (`formatted_string` and `saved_file_path`) are always in predictable, fixed positions for reliable workflow connections.
## Random Choice
Ability to take arbitrary length and type of inputs to then output a **choice** with a controllable seed.
+19
View File
@@ -7,6 +7,8 @@ A collection of workflow efficiency and quality of life nodes that I've created
## String Formatting
The FormatString node provides flexible string formatting with dynamic input/output configuration.
### Python F-String
A simple python f-string dynamically creates the necessary inputs/outputs for the detected keys.
@@ -19,6 +21,23 @@ Switching to Jinja2 allows you to use more advanced control blocks and other fil
![jinja2](assets/jinja2.png)
### Output Structure
The node's outputs are organized for maximum reliability and flexibility:
1. **`formatted_string`** (Output 0): The formatted result string - always in position 0
2. **`saved_file_path`** (Output 1): Path to saved state file (if save_path provided) - always in position 1
3. **Variable outputs** (Output 2+): Pass-through values for any variables detected in the template, enabling easy chaining
For example, with template `"Hello {name}, you are {age}"`:
* Output 0: The formatted string (e.g., "Hello Alice, you are 30")
* Output 1: The save file path (or empty string)
* Output 2: The value of `name` (e.g., "Alice")
* Output 3: The value of `age` (e.g., "30")
This structure ensures the primary outputs (`formatted_string` and `saved_file_path`) are always in predictable, fixed positions for reliable workflow connections.
## Random Choice
Ability to take arbitrary length and type of inputs to then output a **choice** with a controllable seed.
+27 -22
View File
@@ -50,11 +50,16 @@ class FormatString:
Additional context variables like datetime, random, and math functions are available
in Jinja2 mode.
Outputs:
The node always returns formatted_string and saved_file_path as the first two outputs
(positions 0 and 1), followed by any variable values extracted from the template in
subsequent positions. This ensures the primary outputs are in fixed, predictable positions.
Attributes:
CATEGORY (str): The category of the node in ComfyUI's node menu.
FUNCTION (str): The main function to be called when the node is executed.
RETURN_TYPES (tuple): Types of the returned outputs.
RETURN_NAMES (tuple): Names of the returned outputs.
RETURN_TYPES (tuple): Types of the returned outputs (dynamically updated).
RETURN_NAMES (tuple): Names of the returned outputs (dynamically updated).
node_configs (dict): Storage for configurations of node instances.
jinja_env (SandboxedEnvironment): Sandboxed Jinja2 environment for secure template rendering.
additional_context (dict): Extra context variables available in Jinja2 templates.
@@ -306,8 +311,8 @@ class FormatString:
**kwargs: Variable keyword arguments that provide values for template variables.
Returns:
Tuple[str, ...]: A tuple containing the values of input variables (in order),
followed by the formatted string and the save path.
Tuple[str, ...]: A tuple containing the formatted string, the save path,
followed by the values of input variables (in order).
Example:
```python
@@ -322,7 +327,7 @@ class FormatString:
name="Alice",
age="30"
)
print(result) # Outputs: ('Alice', '30', 'Hello Alice, you are 30 years old', '')
print(result) # Outputs: ('Hello Alice, you are 30 years old', '', 'Alice', '30')
# Jinja2 template example
result = FormatString.format_string(
@@ -332,7 +337,7 @@ class FormatString:
unique_id="124",
name="Bob"
)
print(result) # Outputs: ('Bob', 'Hello Bob, today is Wednesday', '')
print(result) # Outputs: ('Hello Bob, today is Wednesday', '', 'Bob')
```
<!-- Example Test:
@@ -344,10 +349,10 @@ class FormatString:
... name="Alice",
... age="30"
... )
>>> assert result[0] == "Alice"
>>> assert result[1] == "30"
>>> assert result[2] == "Hello Alice, you are 30 years old"
>>> assert result[3] == ""
>>> assert result[0] == "Hello Alice, you are 30 years old"
>>> assert result[1] == ""
>>> assert result[2] == "Alice"
>>> assert result[3] == "30"
>>>
>>> # Test Jinja2 format with datetime (can't test exact output due to time dependency)
>>> result = FormatString.format_string(
@@ -356,9 +361,9 @@ class FormatString:
... save_path="",
... name="Bob"
... )
>>> assert result[0] == "Bob"
>>> assert result[1] == "Name: Bob"
>>> assert result[2] == ""
>>> assert result[0] == "Name: Bob"
>>> assert result[1] == ""
>>> assert result[2] == "Bob"
-->
"""
logger.info(
@@ -511,18 +516,18 @@ class FormatString:
... )
>>> assert "name" in config["inputs"]
>>> assert "age" in config["inputs"]
>>> assert len(config["outputs"]) == 4 # name, age, formatted_string, saved_file_path
>>> assert config["outputs"][0]["name"] == "name"
>>> assert config["outputs"][1]["name"] == "age"
>>> assert config["outputs"][2]["name"] == "formatted_string"
>>> assert config["outputs"][3]["name"] == "saved_file_path"
>>> assert len(config["outputs"]) == 4 # formatted_string, saved_file_path, name, age
>>> assert config["outputs"][0]["name"] == "formatted_string"
>>> assert config["outputs"][1]["name"] == "saved_file_path"
>>> assert config["outputs"][2]["name"] == "name"
>>> assert config["outputs"][3]["name"] == "age"
>>> # Check that RETURN_TYPES and RETURN_NAMES are updated
>>> assert len(FormatString.RETURN_TYPES) == 4
>>> assert len(FormatString.RETURN_NAMES) == 4
>>> assert FormatString.RETURN_NAMES[0] == "name"
>>> assert FormatString.RETURN_NAMES[1] == "age"
>>> assert FormatString.RETURN_NAMES[2] == "formatted_string"
>>> assert FormatString.RETURN_NAMES[3] == "saved_file_path"
>>> assert FormatString.RETURN_NAMES[0] == "formatted_string"
>>> assert FormatString.RETURN_NAMES[1] == "saved_file_path"
>>> assert FormatString.RETURN_NAMES[2] == "name"
>>> assert FormatString.RETURN_NAMES[3] == "age"
>>> # Check that node config is stored
>>> assert "test_node" in FormatString.node_configs
>>> assert FormatString.node_configs["test_node"] == config