Skip to content

Commit 431fadb

Browse files
KosinkadinkNi-zavguill
authored
fix(api-io): serialize MultiCombo multi_select as object config (Comfy-Org#13484)
* fix(api-io): serialize MultiCombo multi_select as object config * fix: remove dead code and redundant top-level keys from MultiCombo serialization * fix: correct skip warning to mention comfy_entrypoint, remove nonexistent NODES_LIST * fix: validate MultiCombo list values against options individually * fix: gate multiselect validation on schema config, improve error message, add tests --------- Co-authored-by: Ni-zav <ni-zav@users.noreply.github.com> Co-authored-by: guill <jacob.e.segal@gmail.com>
1 parent 1ac60da commit 431fadb

4 files changed

Lines changed: 93 additions & 9 deletions

File tree

comfy_api/latest/_io.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,6 @@ def __init__(self, id: str=None, display_name: str=None, options: list[str]=None
395395
@comfytype(io_type="COMBO")
396396
class MultiCombo(ComfyTypeI):
397397
'''Multiselect Combo input (dropdown for selecting potentially more than one value).'''
398-
# TODO: something is wrong with the serialization, frontend does not recognize it as multiselect
399398
Type = list[str]
400399
class Input(Combo.Input):
401400
def __init__(self, id: str, options: list[str], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
@@ -408,12 +407,14 @@ def __init__(self, id: str, options: list[str], display_name: str=None, optional
408407
self.default: list[str]
409408

410409
def as_dict(self):
411-
to_return = super().as_dict() | prune_dict({
412-
"multi_select": self.multiselect,
413-
"placeholder": self.placeholder,
414-
"chip": self.chip,
410+
# Frontend expects `multi_select` to be an object config (not a boolean).
411+
# Keep top-level `multiselect` from Combo.Input for backwards compatibility.
412+
return super().as_dict() | prune_dict({
413+
"multi_select": prune_dict({
414+
"placeholder": self.placeholder,
415+
"chip": self.chip,
416+
}),
415417
})
416-
return to_return
417418

418419
@comfytype(io_type="IMAGE")
419420
class Image(ComfyTypeIO):

execution.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,7 +1019,12 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
10191019
combo_options = extra_info.get("options", [])
10201020
else:
10211021
combo_options = input_type
1022-
if val not in combo_options:
1022+
is_multiselect = extra_info.get("multiselect", False)
1023+
if is_multiselect and isinstance(val, list):
1024+
invalid_vals = [v for v in val if v not in combo_options]
1025+
else:
1026+
invalid_vals = [val] if val not in combo_options else []
1027+
if invalid_vals:
10231028
input_config = info
10241029
list_info = ""
10251030

@@ -1034,7 +1039,7 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
10341039
error = {
10351040
"type": "value_not_in_list",
10361041
"message": "Value not in list",
1037-
"details": f"{x}: '{val}' not in {list_info}",
1042+
"details": f"{x}: {', '.join(repr(v) for v in invalid_vals)} not in {list_info}",
10381043
"extra_info": {
10391044
"input_name": x,
10401045
"input_config": input_config,

nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2262,7 +2262,7 @@ async def load_custom_node(module_path: str, ignore=set(), module_parent="custom
22622262
logging.warning(f"Error while calling comfy_entrypoint in {module_path}: {e}")
22632263
return False
22642264
else:
2265-
logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS or NODES_LIST (need one).")
2265+
logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS or comfy_entrypoint (need one).")
22662266
return False
22672267
except Exception as e:
22682268
logging.warning(traceback.format_exc())
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from comfy_api.latest._io import Combo, MultiCombo
2+
3+
4+
def test_multicombo_serializes_multi_select_as_object():
5+
multi_combo = MultiCombo.Input(
6+
id="providers",
7+
options=["a", "b", "c"],
8+
default=["a"],
9+
)
10+
11+
serialized = multi_combo.as_dict()
12+
13+
assert serialized["multiselect"] is True
14+
assert "multi_select" in serialized
15+
assert serialized["multi_select"] == {}
16+
17+
18+
def test_multicombo_serializes_multi_select_with_placeholder_and_chip():
19+
multi_combo = MultiCombo.Input(
20+
id="providers",
21+
options=["a", "b", "c"],
22+
default=["a"],
23+
placeholder="Select providers",
24+
chip=True,
25+
)
26+
27+
serialized = multi_combo.as_dict()
28+
29+
assert serialized["multiselect"] is True
30+
assert serialized["multi_select"] == {
31+
"placeholder": "Select providers",
32+
"chip": True,
33+
}
34+
35+
36+
def test_combo_does_not_serialize_multiselect():
37+
"""Regular Combo should not have multiselect in its serialized output."""
38+
combo = Combo.Input(
39+
id="choice",
40+
options=["a", "b", "c"],
41+
)
42+
43+
serialized = combo.as_dict()
44+
45+
# Combo sets multiselect=False, but prune_dict keeps False (not None),
46+
# so it should be present but False
47+
assert serialized.get("multiselect") is False
48+
assert "multi_select" not in serialized
49+
50+
51+
def _validate_combo_values(val, combo_options, is_multiselect):
52+
"""Reproduce the validation logic from execution.py for testing."""
53+
if is_multiselect and isinstance(val, list):
54+
return [v for v in val if v not in combo_options]
55+
else:
56+
return [val] if val not in combo_options else []
57+
58+
59+
def test_multicombo_validation_accepts_valid_list():
60+
options = ["a", "b", "c"]
61+
assert _validate_combo_values(["a", "b"], options, True) == []
62+
63+
64+
def test_multicombo_validation_rejects_invalid_values():
65+
options = ["a", "b", "c"]
66+
assert _validate_combo_values(["a", "x"], options, True) == ["x"]
67+
68+
69+
def test_multicombo_validation_accepts_empty_list():
70+
options = ["a", "b", "c"]
71+
assert _validate_combo_values([], options, True) == []
72+
73+
74+
def test_combo_validation_rejects_list_even_with_valid_items():
75+
"""A regular Combo should not accept a list value."""
76+
options = ["a", "b", "c"]
77+
invalid = _validate_combo_values(["a", "b"], options, False)
78+
assert len(invalid) > 0

0 commit comments

Comments
 (0)