Skip to content

Commit a0a64c6

Browse files
authored
Add Number Convert node (Comfy-Org#13041)
* Add Number Convert node for unified numeric type conversion Consolidates fragmented IntToFloat/FloatToInt nodes (previously only available via third-party packs like ComfyMath, FillNodes, etc.) into a single core node. - Single input accepting INT, FLOAT, STRING, and BOOL types - Two outputs: FLOAT and INT - Conversion: bool→0/1, string→parsed number, float↔int standard cast - Follows Math Expression node patterns (comfy_api, io.Schema, etc.) Refs: COM-16925 * Register nodes_number_convert.py in extras_files list Without this entry in nodes.py, the Number Convert node file would not be discovered and loaded at startup. * Add isfinite guard, exception chaining, and unit tests for Number Convert node - Add math.isfinite() check to prevent int() crash on inf/nan string inputs - Use 'from None' for cleaner exception chaining on string parse failure - Add 21 unit tests covering all input types and error paths
1 parent 8e73678 commit a0a64c6

File tree

3 files changed

+203
-0
lines changed

3 files changed

+203
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Number Convert node for unified numeric type conversion.
2+
3+
Provides a single node that converts INT, FLOAT, STRING, and BOOL
4+
inputs into FLOAT and INT outputs.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import math
10+
11+
from typing_extensions import override
12+
13+
from comfy_api.latest import ComfyExtension, io
14+
15+
16+
class NumberConvertNode(io.ComfyNode):
17+
"""Converts various types to numeric FLOAT and INT outputs."""
18+
19+
@classmethod
20+
def define_schema(cls) -> io.Schema:
21+
return io.Schema(
22+
node_id="ComfyNumberConvert",
23+
display_name="Number Convert",
24+
category="math",
25+
search_aliases=[
26+
"int to float", "float to int", "number convert",
27+
"int2float", "float2int", "cast", "parse number",
28+
"string to number", "bool to int",
29+
],
30+
inputs=[
31+
io.MultiType.Input(
32+
"value",
33+
[io.Int, io.Float, io.String, io.Boolean],
34+
display_name="value",
35+
),
36+
],
37+
outputs=[
38+
io.Float.Output(display_name="FLOAT"),
39+
io.Int.Output(display_name="INT"),
40+
],
41+
)
42+
43+
@classmethod
44+
def execute(cls, value) -> io.NodeOutput:
45+
if isinstance(value, bool):
46+
float_val = 1.0 if value else 0.0
47+
elif isinstance(value, (int, float)):
48+
float_val = float(value)
49+
elif isinstance(value, str):
50+
text = value.strip()
51+
if not text:
52+
raise ValueError("Cannot convert empty string to number.")
53+
try:
54+
float_val = float(text)
55+
except ValueError:
56+
raise ValueError(
57+
f"Cannot convert string to number: {value!r}"
58+
) from None
59+
else:
60+
raise TypeError(
61+
f"Unsupported input type: {type(value).__name__}"
62+
)
63+
64+
if not math.isfinite(float_val):
65+
raise ValueError(
66+
f"Cannot convert non-finite value to number: {float_val}"
67+
)
68+
69+
return io.NodeOutput(float_val, int(float_val))
70+
71+
72+
class NumberConvertExtension(ComfyExtension):
73+
@override
74+
async def get_node_list(self) -> list[type[io.ComfyNode]]:
75+
return [NumberConvertNode]
76+
77+
78+
async def comfy_entrypoint() -> NumberConvertExtension:
79+
return NumberConvertExtension()

nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2454,6 +2454,7 @@ async def init_builtin_extra_nodes():
24542454
"nodes_nag.py",
24552455
"nodes_sdpose.py",
24562456
"nodes_math.py",
2457+
"nodes_number_convert.py",
24572458
"nodes_painter.py",
24582459
"nodes_curve.py",
24592460
]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import pytest
2+
from unittest.mock import patch, MagicMock
3+
4+
mock_nodes = MagicMock()
5+
mock_nodes.MAX_RESOLUTION = 16384
6+
mock_server = MagicMock()
7+
8+
with patch.dict("sys.modules", {"nodes": mock_nodes, "server": mock_server}):
9+
from comfy_extras.nodes_number_convert import NumberConvertNode
10+
11+
12+
class TestNumberConvertExecute:
13+
@staticmethod
14+
def _exec(value) -> object:
15+
return NumberConvertNode.execute(value)
16+
17+
# --- INT input ---
18+
19+
def test_int_input(self):
20+
result = self._exec(42)
21+
assert result[0] == 42.0
22+
assert result[1] == 42
23+
24+
def test_int_zero(self):
25+
result = self._exec(0)
26+
assert result[0] == 0.0
27+
assert result[1] == 0
28+
29+
def test_int_negative(self):
30+
result = self._exec(-7)
31+
assert result[0] == -7.0
32+
assert result[1] == -7
33+
34+
# --- FLOAT input ---
35+
36+
def test_float_input(self):
37+
result = self._exec(3.14)
38+
assert result[0] == 3.14
39+
assert result[1] == 3
40+
41+
def test_float_truncation_toward_zero(self):
42+
result = self._exec(-2.9)
43+
assert result[0] == -2.9
44+
assert result[1] == -2 # int() truncates toward zero, not floor
45+
46+
def test_float_output_type(self):
47+
result = self._exec(5)
48+
assert isinstance(result[0], float)
49+
50+
def test_int_output_type(self):
51+
result = self._exec(5.7)
52+
assert isinstance(result[1], int)
53+
54+
# --- BOOL input ---
55+
56+
def test_bool_true(self):
57+
result = self._exec(True)
58+
assert result[0] == 1.0
59+
assert result[1] == 1
60+
61+
def test_bool_false(self):
62+
result = self._exec(False)
63+
assert result[0] == 0.0
64+
assert result[1] == 0
65+
66+
# --- STRING input ---
67+
68+
def test_string_integer(self):
69+
result = self._exec("42")
70+
assert result[0] == 42.0
71+
assert result[1] == 42
72+
73+
def test_string_float(self):
74+
result = self._exec("3.14")
75+
assert result[0] == 3.14
76+
assert result[1] == 3
77+
78+
def test_string_negative(self):
79+
result = self._exec("-5.5")
80+
assert result[0] == -5.5
81+
assert result[1] == -5
82+
83+
def test_string_with_whitespace(self):
84+
result = self._exec(" 7.0 ")
85+
assert result[0] == 7.0
86+
assert result[1] == 7
87+
88+
def test_string_scientific_notation(self):
89+
result = self._exec("1e3")
90+
assert result[0] == 1000.0
91+
assert result[1] == 1000
92+
93+
# --- STRING error paths ---
94+
95+
def test_empty_string_raises(self):
96+
with pytest.raises(ValueError, match="Cannot convert empty string"):
97+
self._exec("")
98+
99+
def test_whitespace_only_string_raises(self):
100+
with pytest.raises(ValueError, match="Cannot convert empty string"):
101+
self._exec(" ")
102+
103+
def test_non_numeric_string_raises(self):
104+
with pytest.raises(ValueError, match="Cannot convert string to number"):
105+
self._exec("abc")
106+
107+
def test_string_inf_raises(self):
108+
with pytest.raises(ValueError, match="non-finite"):
109+
self._exec("inf")
110+
111+
def test_string_nan_raises(self):
112+
with pytest.raises(ValueError, match="non-finite"):
113+
self._exec("nan")
114+
115+
def test_string_negative_inf_raises(self):
116+
with pytest.raises(ValueError, match="non-finite"):
117+
self._exec("-inf")
118+
119+
# --- Unsupported type ---
120+
121+
def test_unsupported_type_raises(self):
122+
with pytest.raises(TypeError, match="Unsupported input type"):
123+
self._exec([1, 2, 3])

0 commit comments

Comments
 (0)