From 75cc2194ff01761720946fdd5d924ac899835a04 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Mon, 17 Nov 2025 18:49:24 -0800 Subject: [PATCH] Add nesting of inputs on DynamicCombo during execution --- comfy_api/latest/_io.py | 75 ++++++++++++++++++++++++++++++++----- comfy_extras/nodes_logic.py | 24 ++++++++---- execution.py | 5 ++- 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 2ec109ddb..a5f53917c 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -2,6 +2,7 @@ from __future__ import annotations import copy import inspect +import re from abc import ABC, abstractmethod from collections import Counter from collections.abc import Iterable @@ -881,6 +882,14 @@ class AutogrowDynamic(ComfyTypeI): curr_count += 1 return new_inputs +def add_dynamic_id_mapping(d: dict[str], inputs: list[Input], curr_prefix: str, self: DynamicInput=None): + dynamic = d.setdefault("dynamic_data", {}) + if self is not None: + dynamic[self.id] = f"{curr_prefix}{self.id}" + for i in inputs: + if not isinstance(i, DynamicInput): + dynamic[i.id] = f"{curr_prefix}{i.id}" + @comfytype(io_type="COMFY_DYNAMICCOMBO_V3") class DynamicCombo(ComfyTypeI): class Option: @@ -900,8 +909,9 @@ class DynamicCombo(ComfyTypeI): super().__init__(id, display_name, optional, tooltip, lazy, extra_dict) self.options = options - def add_to_dict_live_inputs(self, d: dict[str], live_inputs: dict[str]): + def add_to_dict_live_inputs(self, d: dict[str], live_inputs: dict[str], curr_prefix=''): # check if dynamic input's id is in live_inputs + curr_prefix = f"{curr_prefix}{self.id}." if self.id in live_inputs: key = live_inputs[self.id] selected_option = None @@ -910,8 +920,8 @@ class DynamicCombo(ComfyTypeI): selected_option = option break if selected_option is not None: - add_to_input_dict_v1(d, selected_option.inputs, live_inputs) - add_dynamic_to_dict_v1(d, self, selected_option.inputs) + add_to_input_dict_v1(d, selected_option.inputs, live_inputs, curr_prefix) + add_dynamic_id_mapping(d, selected_option.inputs, curr_prefix, self) def get_dynamic(self) -> list[Input]: return [input for option in self.options for input in option.inputs] @@ -1259,12 +1269,12 @@ def create_input_dict_v1(inputs: list[Input], live_inputs: dict[str]=None) -> di add_to_input_dict_v1(input, inputs, live_inputs) return input -def add_to_input_dict_v1(d: dict[str], inputs: list[Input], live_inputs: dict[str]=None): +def add_to_input_dict_v1(d: dict[str], inputs: list[Input], live_inputs: dict[str]=None, curr_prefix=''): for i in inputs: if isinstance(i, DynamicInput): add_to_dict_v1(i, d) if live_inputs is not None: - i.add_to_dict_live_inputs(d, live_inputs) + i.add_to_dict_live_inputs(d, live_inputs, curr_prefix) else: add_to_dict_v1(i, d) @@ -1279,14 +1289,59 @@ def add_to_dict_v1(i: Input, d: dict, dynamic_dict: dict=None): value = (i.get_io_type(), as_dict, dynamic_dict) d.setdefault(key, {})[i.id] = value -def add_dynamic_to_dict_v1(d: dict[str], parent: DynamicInput, inputs: list[Input]): - dynamic = d.setdefault("dynamic_data", {}) - ids = [i.id for i in inputs] - dynamic[parent.id] = {"ids": ids} - def add_to_dict_v3(io: Input | Output, d: dict): d[io.id] = (io.get_io_type(), io.as_dict()) +def build_nested_inputs(values: dict[str], paths: dict[str]): + # NOTE: This was initially AI generated + # Tries to account for arrays as well, will likely be changed once that's in + values = values.copy() + result = {} + + index_pattern = re.compile(r"^(?P[A-Za-z0-9_]+)\[(?P\d+)\]$") + + for key, path in paths.items(): + parts = path.split(".") + current = result + + for i, p in enumerate(parts): + is_last = (i == len(parts) - 1) + + match = index_pattern.match(p) + if match: + list_key = match.group("key") + index = int(match.group("index")) + + # Ensure list exists + if list_key not in current or not isinstance(current[list_key], list): + current[list_key] = [] + + lst = current[list_key] + + # Expand list to the needed index + while len(lst) <= index: + lst.append(None) + + # Last element → assign the value directly + if is_last: + lst[index] = values.pop(key) + break + + # Non-last element → ensure dict + if lst[index] is None or not isinstance(lst[index], dict): + lst[index] = {} + + current = lst[index] + continue + + # Normal dict key + if is_last: + current[p] = values.pop(key) + else: + current = current.setdefault(p, {}) + + values.update(result) + return values class _ComfyNodeBaseInternal(_ComfyNodeInternal): diff --git a/comfy_extras/nodes_logic.py b/comfy_extras/nodes_logic.py index ab095f8d3..879dc2b2d 100644 --- a/comfy_extras/nodes_logic.py +++ b/comfy_extras/nodes_logic.py @@ -1,3 +1,4 @@ +from typing import TypedDict from typing_extensions import override from comfy_api.latest import ComfyExtension, io @@ -35,6 +36,12 @@ class SwitchNode(io.ComfyNode): class DCTestNode(io.ComfyNode): + class DCValues(TypedDict): + combo: str + string: str + integer: int + image: io.Image.Type + @classmethod def define_schema(cls): return io.Schema( @@ -52,15 +59,16 @@ class DCTestNode(io.ComfyNode): ) @classmethod - def execute(cls, combo, **kwargs) -> io.NodeOutput: - if combo == "option1": - return io.NodeOutput(kwargs["string"]) - elif combo == "option2": - return io.NodeOutput(kwargs["integer"]) - elif combo == "option3": - return io.NodeOutput(kwargs["image"]) + def execute(cls, combo: DCValues) -> io.NodeOutput: + combo_val = combo["combo"] + if combo_val == "option1": + return io.NodeOutput(combo["string"]) + elif combo_val == "option2": + return io.NodeOutput(combo["integer"]) + elif combo_val == "option3": + return io.NodeOutput(combo["image"]) else: - raise ValueError(f"Invalid combo: {combo}") + raise ValueError(f"Invalid combo: {combo_val}") class LogicExtension(ComfyExtension): diff --git a/execution.py b/execution.py index f72ce7d14..4faed5ded 100644 --- a/execution.py +++ b/execution.py @@ -34,7 +34,7 @@ from comfy_execution.validation import validate_node_input from comfy_execution.progress import get_progress_state, reset_progress_state, add_progress_handler, WebUIProgressHandler from comfy_execution.utils import CurrentNodeContext from comfy_api.internal import _ComfyNodeInternal, _NodeOutputInternal, first_real_override, is_class, make_locked_method_func -from comfy_api.latest import io +from comfy_api.latest import io, _io class ExecutionResult(Enum): @@ -268,6 +268,9 @@ async def _async_map_node_over_list(prompt_id, unique_id, obj, input_data_all, f type_obj.VALIDATE_CLASS() class_clone = type_obj.PREPARE_CLASS_CLONE(v3_data) f = make_locked_method_func(type_obj, func, class_clone) + # in case of dynamic inputs, restructure inputs to expected nested dict + if v3_data is not None and v3_data["dynamic_data"] is not None: + inputs = _io.build_nested_inputs(inputs, v3_data["dynamic_data"]) # V1 else: f = getattr(obj, func)