mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-20 05:18:15 +08:00
DynamicOutputs: allow selectors to reach nested DynamicCombo/DynamicSlot
Schema.validate() now walks DynamicCombo.Input / DynamicSlot.Input options recursively when building the selector-target map, so DynamicOutputs.ByKey / BySlot can address a dynamic input nested inside another dynamic option via its dotted path (e.g. selector="outer.inner_slot"). The runtime dispatch / type resolver already handled dotted-path nested inputs; this just removes the schema-level rejection. Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
parent
22d467dc84
commit
aad3446c17
@ -2057,11 +2057,22 @@ class Schema:
|
|||||||
nested_inputs.extend(input.get_all())
|
nested_inputs.extend(input.get_all())
|
||||||
input_ids = [i.id for i in nested_inputs]
|
input_ids = [i.id for i in nested_inputs]
|
||||||
input_set = set(input_ids)
|
input_set = set(input_ids)
|
||||||
combo_inputs = {i.id: i for i in self.inputs if isinstance(i, DynamicCombo.Input)}
|
# Walk DynamicCombo / DynamicSlot options so a DynamicOutputs selector can
|
||||||
slot_inputs = {i.id: i for i in self.inputs if isinstance(i, DynamicSlot.Input)}
|
# target a nested dynamic input by dotted path (e.g. "outer.inner_slot").
|
||||||
# selector lookup covers both static (nested) inputs and DynamicCombo/Slot
|
# Other DynamicInput kinds (e.g. Autogrow) are skipped — their nested ids
|
||||||
# top-level inputs, since DynamicInput instances aren't included in nested_inputs.
|
# only exist at prompt time and aren't statically addressable.
|
||||||
all_selectable_ids = input_set | set(combo_inputs) | set(slot_inputs)
|
selector_targets: dict[str, Input] = {}
|
||||||
|
def _walk(inputs: list[Input], prefix: str | None) -> None:
|
||||||
|
for inp in inputs:
|
||||||
|
dotted = inp.id if prefix is None else f"{prefix}.{inp.id}"
|
||||||
|
selector_targets[dotted] = inp
|
||||||
|
if isinstance(inp, (DynamicCombo.Input, DynamicSlot.Input)):
|
||||||
|
for opt in inp.options:
|
||||||
|
_walk(opt.inputs, dotted)
|
||||||
|
_walk(self.inputs, None)
|
||||||
|
combo_inputs = {sid: inp for sid, inp in selector_targets.items() if isinstance(inp, DynamicCombo.Input)}
|
||||||
|
slot_inputs = {sid: inp for sid, inp in selector_targets.items() if isinstance(inp, DynamicSlot.Input)}
|
||||||
|
all_selectable_ids = input_set | set(selector_targets)
|
||||||
|
|
||||||
# ``output_ids`` covers every id that may ever appear in a finalized
|
# ``output_ids`` covers every id that may ever appear in a finalized
|
||||||
# output list — static outputs + every option's outputs across every
|
# output list — static outputs + every option's outputs across every
|
||||||
|
|||||||
@ -440,3 +440,98 @@ def test_v1_info_emits_byslot_entry():
|
|||||||
assert entry["selector"] == "slot"
|
assert entry["selector"] == "slot"
|
||||||
whens = [opt["when"] for opt in entry["options"]]
|
whens = [opt["when"] for opt in entry["options"]]
|
||||||
assert whens == [["IMAGE"], ["LATENT"], None]
|
assert whens == [["IMAGE"], ["LATENT"], None]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Nested dynamic selectors (selector reaches into a DynamicCombo / DynamicSlot
|
||||||
|
# option's nested inputs via a dotted path).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_schema_accepts_byslot_selector_into_nested_dynamic_slot():
|
||||||
|
"""A BySlot selector may target a DynamicSlot nested inside a DynamicCombo option."""
|
||||||
|
class Nested(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="NestedBySlot",
|
||||||
|
inputs=[
|
||||||
|
io.DynamicCombo.Input("outer", options=[
|
||||||
|
io.DynamicCombo.Option(key="a", inputs=[
|
||||||
|
io.DynamicSlot.Input("inner_slot", options=[
|
||||||
|
io.DynamicSlot.Option(when=io.Image),
|
||||||
|
io.DynamicSlot.Option(when=None),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
outputs=[io.DynamicOutputs.BySlot(id="r", selector="outer.inner_slot", options=[
|
||||||
|
io.DynamicOutputs.SlotOption(when=io.Image, outputs=[io.Image.Output("img")]),
|
||||||
|
io.DynamicOutputs.SlotOption(when=None, outputs=[]),
|
||||||
|
])],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, **kwargs):
|
||||||
|
return io.NodeOutput.from_named({})
|
||||||
|
|
||||||
|
# Must not raise — selector resolves to the nested DynamicSlot via dotted path.
|
||||||
|
Nested.GET_SCHEMA()
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_accepts_bykey_selector_into_nested_dynamic_combo():
|
||||||
|
"""A ByKey selector may target a DynamicCombo nested inside another DynamicCombo option."""
|
||||||
|
class Nested(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="NestedByKey",
|
||||||
|
inputs=[
|
||||||
|
io.DynamicCombo.Input("outer", options=[
|
||||||
|
io.DynamicCombo.Option(key="branch", inputs=[
|
||||||
|
io.DynamicCombo.Input("inner", options=[
|
||||||
|
io.DynamicCombo.Option(key="x", inputs=[]),
|
||||||
|
io.DynamicCombo.Option(key="y", inputs=[]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
outputs=[io.DynamicOutputs.ByKey(id="r", selector="outer.inner", options=[
|
||||||
|
io.DynamicOutputs.Option(key="x", outputs=[io.Int.Output("ix")]),
|
||||||
|
io.DynamicOutputs.Option(key="y", outputs=[io.String.Output("iy")]),
|
||||||
|
])],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, **kwargs):
|
||||||
|
return io.NodeOutput.from_named({})
|
||||||
|
|
||||||
|
Nested.GET_SCHEMA()
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_rejects_byslot_nested_when_type_not_on_target_slot():
|
||||||
|
"""The when-type alignment check follows the dotted selector into the nested slot."""
|
||||||
|
class BadNested(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return io.Schema(
|
||||||
|
node_id="BadNested",
|
||||||
|
inputs=[
|
||||||
|
io.DynamicCombo.Input("outer", options=[
|
||||||
|
io.DynamicCombo.Option(key="a", inputs=[
|
||||||
|
io.DynamicSlot.Input("inner_slot", options=[
|
||||||
|
io.DynamicSlot.Option(when=io.Image),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
outputs=[io.DynamicOutputs.BySlot(id="r", selector="outer.inner_slot", options=[
|
||||||
|
io.DynamicOutputs.SlotOption(when=io.Audio, outputs=[io.Audio.Output("x")]),
|
||||||
|
])],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, **kwargs):
|
||||||
|
return io.NodeOutput.from_named({})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=r"type\(s\) \['AUDIO'\] are not accepted"):
|
||||||
|
BadNested.GET_SCHEMA()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user