Skip to content

Commit de75f50

Browse files
committed
feat: add minLength/maxLength validation for String inputs
1 parent e4b0bb8 commit de75f50

2 files changed

Lines changed: 71 additions & 1 deletion

File tree

comfy_api/latest/_io.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,18 +326,23 @@ class Input(WidgetInput):
326326
'''String input.'''
327327
def __init__(self, id: str, display_name: str=None, optional=False, tooltip: str=None, lazy: bool=None,
328328
multiline=False, placeholder: str=None, default: str=None, dynamic_prompts: bool=None,
329-
socketless: bool=None, force_input: bool=None, extra_dict=None, raw_link: bool=None, advanced: bool=None):
329+
socketless: bool=None, force_input: bool=None, extra_dict=None, raw_link: bool=None, advanced: bool=None,
330+
min_length: int=None, max_length: int=None):
330331
super().__init__(id, display_name, optional, tooltip, lazy, default, socketless, None, force_input, extra_dict, raw_link, advanced)
331332
self.multiline = multiline
332333
self.placeholder = placeholder
333334
self.dynamic_prompts = dynamic_prompts
335+
self.min_length = min_length
336+
self.max_length = max_length
334337
self.default: str
335338

336339
def as_dict(self):
337340
return super().as_dict() | prune_dict({
338341
"multiline": self.multiline,
339342
"placeholder": self.placeholder,
340343
"dynamicPrompts": self.dynamic_prompts,
344+
"minLength": self.min_length,
345+
"maxLength": self.max_length,
341346
})
342347

343348
@comfytype(io_type="COMBO")

execution.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,41 @@ def mark_missing():
215215
v3_data["hidden_inputs"] = hidden_inputs_v3
216216
return input_data_all, missing_keys, v3_data
217217

218+
def validate_resolved_inputs(input_data_all, class_def, inputs):
219+
"""Validate resolved input values against schema constraints.
220+
221+
This is needed because validate_inputs() only sees direct widget values.
222+
Linked inputs aren't resolved during validate_inputs(), so this runs after resolution to catch any violations.
223+
"""
224+
is_v3 = issubclass(class_def, _ComfyNodeInternal)
225+
valid_inputs = class_def.INPUT_TYPES()
226+
if is_v3:
227+
valid_inputs, _, _ = _io.get_finalized_class_inputs(valid_inputs, inputs)
228+
229+
for x, values in input_data_all.items():
230+
input_type, input_category, extra_info = get_input_info(class_def, x, valid_inputs)
231+
if extra_info is None:
232+
continue
233+
if input_type != "STRING":
234+
continue
235+
min_length = extra_info.get("minLength")
236+
max_length = extra_info.get("maxLength")
237+
if min_length is None and max_length is None:
238+
continue
239+
for val in values:
240+
if val is None or not isinstance(val, str):
241+
continue
242+
if min_length is not None and len(val) < min_length:
243+
raise ValueError(
244+
f"Input '{x}': value length {len(val)} is shorter than "
245+
f"minimum length of {min_length}"
246+
)
247+
if max_length is not None and len(val) > max_length:
248+
raise ValueError(
249+
f"Input '{x}': value length {len(val)} is longer than "
250+
f"maximum length of {max_length}"
251+
)
252+
218253
map_node_over_list = None #Don't hook this please
219254

220255
async def resolve_map_node_over_list_results(results):
@@ -498,6 +533,8 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
498533
execution_list.make_input_strong_link(unique_id, i)
499534
return (ExecutionResult.PENDING, None, None)
500535

536+
validate_resolved_inputs(input_data_all, class_def, inputs)
537+
501538
def execution_block_cb(block):
502539
if block.message is not None:
503540
mes = {
@@ -940,6 +977,34 @@ async def validate_inputs(prompt_id, prompt, item, validated):
940977
errors.append(error)
941978
continue
942979

980+
if input_type == "STRING":
981+
if "minLength" in extra_info and len(val) < extra_info["minLength"]:
982+
error = {
983+
"type": "value_shorter_than_min_length",
984+
"message": "Value length {} shorter than min length of {}".format(len(val), extra_info["minLength"]),
985+
"details": f"{x}",
986+
"extra_info": {
987+
"input_name": x,
988+
"input_config": info,
989+
"received_value": val,
990+
}
991+
}
992+
errors.append(error)
993+
continue
994+
if "maxLength" in extra_info and len(val) > extra_info["maxLength"]:
995+
error = {
996+
"type": "value_longer_than_max_length",
997+
"message": "Value length {} longer than max length of {}".format(len(val), extra_info["maxLength"]),
998+
"details": f"{x}",
999+
"extra_info": {
1000+
"input_name": x,
1001+
"input_config": info,
1002+
"received_value": val,
1003+
}
1004+
}
1005+
errors.append(error)
1006+
continue
1007+
9431008
if isinstance(input_type, list) or input_type == io.Combo.io_type:
9441009
if input_type == io.Combo.io_type:
9451010
combo_options = extra_info.get("options", [])

0 commit comments

Comments
 (0)