Add STRING escape and unescape nodes.

Fix test cases for containers and add those for the new string nodes
This commit is contained in:
StableLlama
2025-06-03 17:47:40 +02:00
parent 43b8665a36
commit 597f56cef6
8 changed files with 150 additions and 45 deletions
@@ -811,7 +811,6 @@ class DataListSlice(ComfyNodeABC):
start = kwargs.get('start', [0])[0]
stop = kwargs.get('stop', [INT_MAX])[0]
step = kwargs.get('step', [1])[0]
print(f"start: {start}, stop: {stop}, step: {step}; input_list: {input_list}")
return (input_list[start:stop:step],)
+6 -6
View File
@@ -91,8 +91,8 @@ class ListCreateFromFloat(ComfyNodeABC):
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
values = [float(value) for value in kwargs.values()]
return (values[:-1],)
values = [float(value) for value in list(kwargs.values())[:-1]]
return (values,)
class ListCreateFromInt(ComfyNodeABC):
@@ -117,8 +117,8 @@ class ListCreateFromInt(ComfyNodeABC):
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
values = [int(value) for value in kwargs.values()]
return (values[:-1],)
values = [int(value) for value in list(kwargs.values())[:-1]]
return (values,)
class ListCreateFromString(ComfyNodeABC):
@@ -143,8 +143,8 @@ class ListCreateFromString(ComfyNodeABC):
FUNCTION = "create_list"
def create_list(self, **kwargs: list[Any]) -> tuple[list[Any]]:
values = [str(value) for value in kwargs.values()]
return (values[:-1],)
values = [str(value) for value in list(kwargs.values())[:-1]]
return (values,)
class ListAppend(ComfyNodeABC):
+10 -10
View File
@@ -37,8 +37,8 @@ class SetCreate(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
values = kwargs.values()
return (set(values[:-1]),)
values = list(kwargs.values())[:-1]
return (set(values),)
class SetCreateFromBoolean(ComfyNodeABC):
@@ -63,8 +63,8 @@ class SetCreateFromBoolean(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
values = [bool(value) for value in kwargs.values()]
return (set(values[:-1]),)
values = [bool(value) for value in list(kwargs.values())[:-1]]
return (set(values),)
class SetCreateFromFloat(ComfyNodeABC):
@@ -89,8 +89,8 @@ class SetCreateFromFloat(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
values = [float(value) for value in kwargs.values()]
return (set(values[:-1]),)
values = [float(value) for value in list(kwargs.values())[:-1]]
return (set(values),)
class SetCreateFromInt(ComfyNodeABC):
@@ -115,8 +115,8 @@ class SetCreateFromInt(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
values = [int(value) for value in kwargs.values()]
return (set(values[:-1]),)
values = [int(value) for value in list(kwargs.values())[:-1]]
return (set(values),)
class SetCreateFromString(ComfyNodeABC):
@@ -141,8 +141,8 @@ class SetCreateFromString(ComfyNodeABC):
FUNCTION = "create_set"
def create_set(self, **kwargs: list[Any]) -> tuple[set[Any]]:
values = [str(value) for value in kwargs.values()]
return (set(values[:-1]),)
values = [str(value) for value in list(kwargs.values())[:-1]]
return (set(values),)
class SetAdd(ComfyNodeABC):
+72
View File
@@ -890,6 +890,74 @@ class StringRemovesuffix(ComfyNodeABC):
return (string.removesuffix(suffix),)
class StringUnescape(ComfyNodeABC):
"""
Unescapes a string by converting escape sequences to their actual characters.
This node converts escape sequences like '\n' (two characters) to actual newlines (one character),
'\t' to tabs, '\\' to backslashes, etc. Useful for processing strings where escape sequences
are represented literally rather than interpreted.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"string": (IO.STRING, {"default": ""}),
}
}
RETURN_TYPES = (IO.STRING,)
CATEGORY = "Basic/STRING"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "unescape"
def unescape(self, string):
# Decode escaped sequences only for control characters
result = (
string
.replace(r'\\', '\u0000')
.replace(r'\n', '\n')
.replace(r'\t', '\t')
.replace(r'\"', '"')
.replace(r'\'', "'")
.replace('\u0000', '\\')
)
return (result,)
class StringEscape(ComfyNodeABC):
"""
Escapes a string by converting special characters to escape sequences.
This node converts characters like newlines, tabs, quotes, and backslashes to their escaped
representation (like '\n', '\t', '\"', '\\'). Useful when you need to prepare strings
for formats that require escaped sequences instead of literal special characters.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"string": (IO.STRING, {"default": ""}),
}
}
RETURN_TYPES = (IO.STRING,)
CATEGORY = "Basic/STRING"
DESCRIPTION = cleandoc(__doc__ or "")
FUNCTION = "escape"
def escape(self, string):
result = (
string
.replace('\\', r'\\')
.replace('\n', r'\n')
.replace('\t', r'\t')
.replace('"', r'\"')
.replace("'", r"\'")
)
return (result,)
class StringReplace(ComfyNodeABC):
"""
Replaces occurrences of a substring with another substring.
@@ -1384,6 +1452,7 @@ NODE_CLASS_MAPPINGS = {
"Basic data handling: StringDecode": StringDecode,
"Basic data handling: StringEncode": StringEncode,
"Basic data handling: StringEndswith": StringEndswith,
"Basic data handling: StringEscape": StringEscape,
"Basic data handling: StringExpandtabs": StringExpandtabs,
"Basic data handling: StringFind": StringFind,
"Basic data handling: StringFormatMap": StringFormatMap,
@@ -1422,6 +1491,7 @@ NODE_CLASS_MAPPINGS = {
"Basic data handling: StringStrip": StringStrip,
"Basic data handling: StringSwapcase": StringSwapcase,
"Basic data handling: StringTitle": StringTitle,
"Basic data handling: StringUnescape": StringUnescape,
"Basic data handling: StringUpper": StringUpper,
"Basic data handling: StringZfill": StringZfill,
}
@@ -1435,6 +1505,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: StringDecode": "decode",
"Basic data handling: StringEncode": "encode",
"Basic data handling: StringEndswith": "endswith",
"Basic data handling: StringEscape": "escape",
"Basic data handling: StringExpandtabs": "expandtabs",
"Basic data handling: StringFind": "find",
"Basic data handling: StringFormatMap": "format_map",
@@ -1473,6 +1544,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Basic data handling: StringStrip": "strip",
"Basic data handling: StringSwapcase": "swapcase",
"Basic data handling: StringTitle": "title",
"Basic data handling: StringUnescape": "unescape",
"Basic data handling: StringUpper": "upper",
"Basic data handling: StringZfill": "zfill",
}
+4 -4
View File
@@ -55,7 +55,7 @@ def test_dict_set():
def test_dict_create_from_boolean():
node = DictCreateFromBoolean()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0=True, key_1="key2", value_1=False)
result = node.create(key_0="key1", value_0=True, key_1="key2", value_1=False, key_2="", value_2="")
assert result == ({"key1": True, "key2": False},)
# Test with empty inputs
assert node.create() == ({},)
@@ -64,7 +64,7 @@ def test_dict_create_from_boolean():
def test_dict_create_from_float():
node = DictCreateFromFloat()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0=1.5, key_1="key2", value_1=2.5)
result = node.create(key_0="key1", value_0=1.5, key_1="key2", value_1=2.5, key_2="", value_2="")
assert result == ({"key1": 1.5, "key2": 2.5},)
# Test with empty inputs
assert node.create() == ({},)
@@ -73,7 +73,7 @@ def test_dict_create_from_float():
def test_dict_create_from_int():
node = DictCreateFromInt()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0=1, key_1="key2", value_1=2)
result = node.create(key_0="key1", value_0=1, key_1="key2", value_1=2, key_2="", value_2="")
assert result == ({"key1": 1, "key2": 2},)
# Test with empty inputs
assert node.create() == ({},)
@@ -82,7 +82,7 @@ def test_dict_create_from_int():
def test_dict_create_from_string():
node = DictCreateFromString()
# Test with dynamic inputs
result = node.create(key_0="key1", value_0="value1", key_1="key2", value_1="value2")
result = node.create(key_0="key1", value_0="value1", key_1="key2", value_1="value2", key_2="", value_2="")
assert result == ({"key1": "value1", "key2": "value2"},)
# Test with empty inputs
assert node.create() == ({},)
+10 -10
View File
@@ -176,36 +176,36 @@ def test_list_max():
def test_list_create():
node = ListCreate()
assert node.create_list(item_0=1, item_1=2, item_2=3) == ([1, 2, 3],)
assert node.create_list(item_0=1, item_1=2, item_2=3, item_3="") == ([1, 2, 3],)
assert node.create_list() == ([],) # Empty list
assert node.create_list(item_0="test") == (["test"],)
assert node.create_list(item_0="test", item_1="") == (["test"],)
def test_list_create_from_boolean():
node = ListCreateFromBoolean()
assert node.create_list(item_0=True, item_1=False, item_2=True) == ([True, False, True],)
assert node.create_list(item_0=True) == ([True],)
assert node.create_list(item_0=True, item_1=False, item_2=True, item_3="") == ([True, False, True],)
assert node.create_list(item_0=True, item_1="") == ([True],)
assert node.create_list() == ([],)
def test_list_create_from_float():
node = ListCreateFromFloat()
assert node.create_list(item_0=1.1, item_1=2.2, item_2=3.3) == ([1.1, 2.2, 3.3],)
assert node.create_list(item_0=0.0) == ([0.0],)
assert node.create_list(item_0=1.1, item_1=2.2, item_2=3.3, item_3="") == ([1.1, 2.2, 3.3],)
assert node.create_list(item_0=0.0, item_1="") == ([0.0],)
assert node.create_list() == ([],)
def test_list_create_from_int():
node = ListCreateFromInt()
assert node.create_list(item_0=1, item_1=2, item_2=3) == ([1, 2, 3],)
assert node.create_list(item_0=0) == ([0],)
assert node.create_list(item_0=1, item_1=2, item_2=3, item_3="") == ([1, 2, 3],)
assert node.create_list(item_0=0, item_1="") == ([0],)
assert node.create_list() == ([],)
def test_list_create_from_string():
node = ListCreateFromString()
assert node.create_list(item_0="a", item_1="b", item_2="c") == (["a", "b", "c"],)
assert node.create_list(item_0="test") == (["test"],)
assert node.create_list(item_0="a", item_1="b", item_2="c", item_3="") == (["a", "b", "c"],)
assert node.create_list(item_0="test", item_1="") == (["test"],)
assert node.create_list() == ([],)
+14 -14
View File
@@ -26,29 +26,29 @@ from src.basic_data_handling.set_nodes import (
def test_set_create():
node = SetCreate()
# Testing with kwargs to simulate dynamic inputs
assert node.create_set(item_0=1, item_1=2, item_2=3) == ({1, 2, 3},)
assert node.create_set(item_0="a", item_1="b") == ({"a", "b"},)
assert node.create_set(item_0=1, item_1=2, item_2=3, item_3="") == ({1, 2, 3},)
assert node.create_set(item_0="a", item_1="b", item_2="") == ({"a", "b"},)
assert node.create_set() == (set(),) # Empty set with no arguments
# Mixed types
assert node.create_set(item_0=1, item_1="b", item_2=True) == ({1, "b", True},)
assert node.create_set(item_0=1, item_1="b", item_2=True, item_3="") == ({1, "b", True},)
def test_set_create_from_int():
node = SetCreateFromInt()
assert node.create_set(item_0=1, item_1=2, item_2=3) == ({1, 2, 3},)
assert node.create_set(item_0=5) == ({5},) # Single item set
assert node.create_set(item_0=1, item_1=1) == ({1},) # Duplicate items become single item
assert node.create_set(item_0=1, item_1=2, item_2=3, item_3="") == ({1, 2, 3},)
assert node.create_set(item_0=5, item_1="") == ({5},) # Single item set
assert node.create_set(item_0=1, item_1=1, item_2="") == ({1},) # Duplicate items become single item
assert node.create_set() == (set(),) # Empty set with no arguments
def test_set_create_from_string():
node = SetCreateFromString()
result = node.create_set(item_0="apple", item_1="banana")
result = node.create_set(item_0="apple", item_1="banana", item_2="")
assert isinstance(result[0], set)
assert result[0] == {"apple", "banana"}
# Duplicate strings
result = node.create_set(item_0="apple", item_1="apple")
result = node.create_set(item_0="apple", item_1="apple", item_2="")
assert result[0] == {"apple"}
# Empty set
@@ -57,19 +57,19 @@ def test_set_create_from_string():
def test_set_create_from_float():
node = SetCreateFromFloat()
assert node.create_set(item_0=1.5, item_1=2.5) == ({1.5, 2.5},)
assert node.create_set(item_0=3.14) == ({3.14},) # Single item set
assert node.create_set(item_0=1.0, item_1=1.0) == ({1.0},) # Duplicate items
assert node.create_set(item_0=1.5, item_1=2.5, item_2="") == ({1.5, 2.5},)
assert node.create_set(item_0=3.14, item_1="") == ({3.14},) # Single item set
assert node.create_set(item_0=1.0, item_1=1.0, item_2="") == ({1.0},) # Duplicate items
assert node.create_set() == (set(),) # Empty set with no arguments
def test_set_create_from_boolean():
node = SetCreateFromBoolean()
assert node.create_set(item_0=True, item_1=False) == ({True, False},)
assert node.create_set(item_0=True, item_1=True) == ({True},) # Duplicate booleans
assert node.create_set(item_0=True, item_1=False, item_2="") == ({True, False},)
assert node.create_set(item_0=True, item_1=True, item_2="") == ({True},) # Duplicate booleans
assert node.create_set() == (set(),) # Empty set with no arguments
# Test conversion from non-boolean values
assert node.create_set(item_0=1, item_1=0) == ({True, False},)
assert node.create_set(item_0=1, item_1=0, item_2="") == ({True, False},)
def test_set_add():
+34
View File
@@ -10,6 +10,7 @@ from src.basic_data_handling.string_nodes import (
StringDecode,
StringEncode,
StringEndswith,
StringEscape,
StringExpandtabs,
StringFind,
StringFormatMap,
@@ -47,6 +48,7 @@ from src.basic_data_handling.string_nodes import (
StringStrip,
StringSwapcase,
StringTitle,
StringUnescape,
StringUpper,
StringZfill,
)
@@ -364,6 +366,38 @@ def test_zfill():
assert node.zfill("123", 2) == ("123",) # Width smaller than string length
assert node.zfill("", 3) == ("000",) # Empty string
def test_unescape():
node = StringUnescape()
# Test control characters
assert node.unescape(r"Hello\nWorld") == ("Hello\nWorld",) # Newline
assert node.unescape(r"Hello\tWorld") == ("Hello\tWorld",) # Tab
assert node.unescape(r"C:\\Program Files\\App") == (r"C:\Program Files\App",) # Backslash
assert node.unescape(r"Quote: \"Text\"") == ("Quote: \"Text\"",) # Double quote
assert node.unescape(r"Quote: \'Text\'") == ("Quote: 'Text'",) # Single quote
# Test that normal Unicode characters remain unchanged
assert node.unescape("German: äöüß") == ("German: äöüß",) # Umlauts should be preserved
assert node.unescape("Hello äöü\nWorld") == ("Hello äöü\nWorld",) # Umlauts with control char
# Test complex string with multiple escape sequences
assert node.unescape(r"Path: C:\\folder\\file.txt\nLine1\tLine2") == ("Path: C:\\folder\\file.txt\nLine1\tLine2",)
def test_escape():
node = StringEscape()
# Test control characters
assert node.escape("Hello\nWorld") == (r"Hello\nWorld",) # Newline
assert node.escape("Hello\tWorld") == (r"Hello\tWorld",) # Tab
assert node.escape(r"C:\Program Files\App") == (r"C:\\Program Files\\App",) # Backslash
assert node.escape('Quote: "Text"') == (r'Quote: \"Text\"',) # Double quote
assert node.escape("Quote: 'Text'") == (r"Quote: \'Text\'",) # Single quote
# Test that normal Unicode characters remain unchanged
assert node.escape("German: äöüß") == (r"German: äöüß",) # Umlauts should be preserved
assert node.escape("Hello äöü\nWorld") == (r"Hello äöü\nWorld",) # Umlauts with control char
# Test complex string with multiple special characters
assert node.escape("Path: C:\\folder\\file.txt\nLine1\tLine2") == (r"Path: C:\\folder\\file.txt\nLine1\tLine2",)
def test_length():
node = StringLength()
assert node.length("hello") == (5,)