mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-07 15:52:32 +08:00
fix(api-io): serialize MultiCombo multi_select as object config (#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>
This commit is contained in:
parent
1ac60da2c9
commit
431fadb520
@ -395,7 +395,6 @@ class Combo(ComfyTypeIO):
|
|||||||
@comfytype(io_type="COMBO")
|
@comfytype(io_type="COMBO")
|
||||||
class MultiCombo(ComfyTypeI):
|
class MultiCombo(ComfyTypeI):
|
||||||
'''Multiselect Combo input (dropdown for selecting potentially more than one value).'''
|
'''Multiselect Combo input (dropdown for selecting potentially more than one value).'''
|
||||||
# TODO: something is wrong with the serialization, frontend does not recognize it as multiselect
|
|
||||||
Type = list[str]
|
Type = list[str]
|
||||||
class Input(Combo.Input):
|
class Input(Combo.Input):
|
||||||
def __init__(self, id: str, options: list[str], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
|
def __init__(self, id: str, options: list[str], display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
|
||||||
@ -408,12 +407,14 @@ class MultiCombo(ComfyTypeI):
|
|||||||
self.default: list[str]
|
self.default: list[str]
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
to_return = super().as_dict() | prune_dict({
|
# Frontend expects `multi_select` to be an object config (not a boolean).
|
||||||
"multi_select": self.multiselect,
|
# Keep top-level `multiselect` from Combo.Input for backwards compatibility.
|
||||||
|
return super().as_dict() | prune_dict({
|
||||||
|
"multi_select": prune_dict({
|
||||||
"placeholder": self.placeholder,
|
"placeholder": self.placeholder,
|
||||||
"chip": self.chip,
|
"chip": self.chip,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
return to_return
|
|
||||||
|
|
||||||
@comfytype(io_type="IMAGE")
|
@comfytype(io_type="IMAGE")
|
||||||
class Image(ComfyTypeIO):
|
class Image(ComfyTypeIO):
|
||||||
|
|||||||
@ -1019,7 +1019,12 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
|
|||||||
combo_options = extra_info.get("options", [])
|
combo_options = extra_info.get("options", [])
|
||||||
else:
|
else:
|
||||||
combo_options = input_type
|
combo_options = input_type
|
||||||
if val not in combo_options:
|
is_multiselect = extra_info.get("multiselect", False)
|
||||||
|
if is_multiselect and isinstance(val, list):
|
||||||
|
invalid_vals = [v for v in val if v not in combo_options]
|
||||||
|
else:
|
||||||
|
invalid_vals = [val] if val not in combo_options else []
|
||||||
|
if invalid_vals:
|
||||||
input_config = info
|
input_config = info
|
||||||
list_info = ""
|
list_info = ""
|
||||||
|
|
||||||
@ -1034,7 +1039,7 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None):
|
|||||||
error = {
|
error = {
|
||||||
"type": "value_not_in_list",
|
"type": "value_not_in_list",
|
||||||
"message": "Value not in list",
|
"message": "Value not in list",
|
||||||
"details": f"{x}: '{val}' not in {list_info}",
|
"details": f"{x}: {', '.join(repr(v) for v in invalid_vals)} not in {list_info}",
|
||||||
"extra_info": {
|
"extra_info": {
|
||||||
"input_name": x,
|
"input_name": x,
|
||||||
"input_config": input_config,
|
"input_config": input_config,
|
||||||
|
|||||||
2
nodes.py
2
nodes.py
@ -2262,7 +2262,7 @@ async def load_custom_node(module_path: str, ignore=set(), module_parent="custom
|
|||||||
logging.warning(f"Error while calling comfy_entrypoint in {module_path}: {e}")
|
logging.warning(f"Error while calling comfy_entrypoint in {module_path}: {e}")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS or NODES_LIST (need one).")
|
logging.warning(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS or comfy_entrypoint (need one).")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(traceback.format_exc())
|
logging.warning(traceback.format_exc())
|
||||||
|
|||||||
78
tests-unit/comfy_api_test/multicombo_serialization_test.py
Normal file
78
tests-unit/comfy_api_test/multicombo_serialization_test.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
from comfy_api.latest._io import Combo, MultiCombo
|
||||||
|
|
||||||
|
|
||||||
|
def test_multicombo_serializes_multi_select_as_object():
|
||||||
|
multi_combo = MultiCombo.Input(
|
||||||
|
id="providers",
|
||||||
|
options=["a", "b", "c"],
|
||||||
|
default=["a"],
|
||||||
|
)
|
||||||
|
|
||||||
|
serialized = multi_combo.as_dict()
|
||||||
|
|
||||||
|
assert serialized["multiselect"] is True
|
||||||
|
assert "multi_select" in serialized
|
||||||
|
assert serialized["multi_select"] == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_multicombo_serializes_multi_select_with_placeholder_and_chip():
|
||||||
|
multi_combo = MultiCombo.Input(
|
||||||
|
id="providers",
|
||||||
|
options=["a", "b", "c"],
|
||||||
|
default=["a"],
|
||||||
|
placeholder="Select providers",
|
||||||
|
chip=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
serialized = multi_combo.as_dict()
|
||||||
|
|
||||||
|
assert serialized["multiselect"] is True
|
||||||
|
assert serialized["multi_select"] == {
|
||||||
|
"placeholder": "Select providers",
|
||||||
|
"chip": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_combo_does_not_serialize_multiselect():
|
||||||
|
"""Regular Combo should not have multiselect in its serialized output."""
|
||||||
|
combo = Combo.Input(
|
||||||
|
id="choice",
|
||||||
|
options=["a", "b", "c"],
|
||||||
|
)
|
||||||
|
|
||||||
|
serialized = combo.as_dict()
|
||||||
|
|
||||||
|
# Combo sets multiselect=False, but prune_dict keeps False (not None),
|
||||||
|
# so it should be present but False
|
||||||
|
assert serialized.get("multiselect") is False
|
||||||
|
assert "multi_select" not in serialized
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_combo_values(val, combo_options, is_multiselect):
|
||||||
|
"""Reproduce the validation logic from execution.py for testing."""
|
||||||
|
if is_multiselect and isinstance(val, list):
|
||||||
|
return [v for v in val if v not in combo_options]
|
||||||
|
else:
|
||||||
|
return [val] if val not in combo_options else []
|
||||||
|
|
||||||
|
|
||||||
|
def test_multicombo_validation_accepts_valid_list():
|
||||||
|
options = ["a", "b", "c"]
|
||||||
|
assert _validate_combo_values(["a", "b"], options, True) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_multicombo_validation_rejects_invalid_values():
|
||||||
|
options = ["a", "b", "c"]
|
||||||
|
assert _validate_combo_values(["a", "x"], options, True) == ["x"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_multicombo_validation_accepts_empty_list():
|
||||||
|
options = ["a", "b", "c"]
|
||||||
|
assert _validate_combo_values([], options, True) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_combo_validation_rejects_list_even_with_valid_items():
|
||||||
|
"""A regular Combo should not accept a list value."""
|
||||||
|
options = ["a", "b", "c"]
|
||||||
|
invalid = _validate_combo_values(["a", "b"], options, False)
|
||||||
|
assert len(invalid) > 0
|
||||||
Loading…
Reference in New Issue
Block a user