Skip to content

Add io.DynamicGroup input#14507

Open
Talmaj wants to merge 3 commits into
masterfrom
ListInput
Open

Add io.DynamicGroup input#14507
Talmaj wants to merge 3 commits into
masterfrom
ListInput

Conversation

@Talmaj

@Talmaj Talmaj commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

io.DynamicGroup – repeatable widget-group input

What this adds

A new dynamic input type io.DynamicGroup (COMFY_DYNAMICGROUP_V3) that lets a node declare a repeatable group of widget fields. The frontend can render N rows of those fields; the backend reconstructs them into a list[dict] before calling execute.

Previously, Autogrow could repeat a single widget N times and DynamicCombo could show a different sub-schema per combo value, but nothing could repeat a group of widgets. io.DynamicGroup fills that gap.

Example

class LoraStacker(ComfyNode):
    @classmethod
    def define_schema(cls):
        return Schema(
            node_id="LoraStacker",
            inputs=[
                io.DynamicGroup.Input(
                    "loras",
                    template=[
                        io.Combo.Input("lora_name", options=folder_paths.get_filename_list("loras")),
                        io.Float.Input("strength", default=1.0, min=-10, max=10, step=0.01),
                    ],
                    min=0,
                    max=50,
                ),
            ],
        )

    @classmethod
    def execute(cls, loras: list[dict]):
        # loras = [{"lora_name": "foo.safetensors", "strength": 0.8}, ...]
        for row in loras:
            apply_lora(row["lora_name"], row["strength"])

How it works

  1. SchemaDynamicGroup.Input serializes a template (standard V1 input dict), min, and max into /object_info.
  2. Frontend – reads COMFY_DYNAMICGROUP_V3, renders stackable rows, submits flat keys: loras.0.lora_name, loras.0.strength, loras.1.lora_name, …
  3. Expansion – at execution time DynamicGroup._expand_schema_for_dynamic scans live_inputs to determine the current row count, registers each {groupId}.{row}.{fieldId} slot in required/optional, and records the group root in list_paths.
  4. Reconstructionbuild_nested_inputs collects the flat values into {0: {…}, 1: {…}}, then a post-pass converts every list_paths entry to a sorted list[dict].

Parameters

Parameter Default Notes
template required list[WidgetInput] – the fields in each row
min 0 Rows below this count are required (non-removable in UI)
max 50 Hard cap; absolute max is 100

Limitations

  • Frontend work required – the repeatable-row UI needs a matching change in comfyui-frontend-package. Until then the schema is served correctly but rows won't stack in the UI.
  • Widget-only template – template fields must be WidgetInput subclasses (Combo, Float, Int, String, Boolean, Color). Plain socket inputs and nested dynamic types (DynamicGroup, Autogrow, DynamicCombo) are not allowed.
  • Top-level groups onlyio.DynamicGroup cannot be nested inside another io.DynamicGroup or Autogrow.

Files changed

  • comfy_api/latest/_io.pyDynamicGroup class, DynamicPathsDefaultValue.EMPTY_LIST, list_paths in V3Data/get_finalized_class_inputs, list post-pass in build_nested_inputs, registered in setup_dynamic_input_funcs, exported in __all__
  • tests-unit/comfy_api_test/io_dynamic_group_test.py – 18 unit tests covering schema construction, 0-row, N-row, sort order, min enforcement, partial values, and no-leftover-flat-keys

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds DynamicGroup in comfy_api/latest/_io.py with template validation, per-row dynamic schema expansion, and an EMPTY_LIST default for empty groups. The parser setup registers DynamicGroup.io_type; V3Data and get_finalized_class_inputs() now track list_paths, and build_nested_inputs reconstructs index-keyed dict leaves into sorted lists or empty lists for missing roots. A new unit test module covers constructor validation, zero-row and multi-row reconstruction, row ordering, and output cleanup.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately names the new DynamicGroup input added by the PR and matches the main change.
Description check ✅ Passed The description clearly matches the PR’s DynamicGroup repeatable widget-group input changes and new tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@comfy_api/latest/_io.py`:
- Line 1366: The row_count calculation at the line with max(min_rows,
present_rows) does not enforce the max parameter that was serialized in
as_dict(). Read the max parameter from the List.Input object and modify the
row_count assignment to cap it so that row_count respects both the minimum
constraint (min_rows) and the maximum constraint (max parameter). This ensures
that row_count falls within the allowed range and prevents the backend from
accepting row indices beyond the specified maximum.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd605340-c975-4ad9-8610-e97f9f52618f

📥 Commits

Reviewing files that changed from the base of the PR and between 90eeeb2 and a3b9cf8.

📒 Files selected for processing (2)
  • comfy_api/latest/_io.py
  • tests-unit/comfy_api_test/io_list_test.py

Comment thread comfy_api/latest/_io.py
@Talmaj Talmaj changed the title Add io.List input Add io.DynamicGroup input Jun 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests-unit/comfy_api_test/io_dynamic_group_test.py (1)

198-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for max overflow.

The implementation now rejects live rows beyond max, but this error path is not covered. A small test locks the backend cap contract.

Proposed test
     def test_no_leftover_flat_keys(self):
         """Flat keys must be consumed; only the reconstructed list remains."""
         inp = DynamicGroup.Input("rows", template=[Float.Input("x", default=0.0)], min=0, max=5)
         result = _run(inp, {"rows.0.x": 1.0, "rows.1.x": 2.0})
         assert "rows.0.x" not in result
         assert "rows.1.x" not in result
         assert isinstance(result["rows"], list)
+
+    def test_rows_beyond_max_raise(self):
+        inp = DynamicGroup.Input("rows", template=[Float.Input("x", default=0.0)], min=0, max=2)
+
+        with pytest.raises(ValueError):
+            get_finalized_class_inputs(_make_class_inputs(inp), {"rows.2.x": 1.0})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests-unit/comfy_api_test/io_dynamic_group_test.py` around lines 198 - 204,
Add a regression test in io_dynamic_group_test around DynamicGroup.Input/_run
that covers the max overflow path: create a group with a small max and submit
more indexed rows than allowed, then assert the backend rejects the extra row(s)
instead of reconstructing them. Reuse the existing test helpers and symbols like
test_no_leftover_flat_keys, DynamicGroup.Input, and _run so the new case sits
alongside the current flat-key coverage and verifies the cap contract
explicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@comfy_api/latest/_io.py`:
- Around line 1305-1308: The DynamicGroup ID validation currently checks only
for uniqueness, but it also needs to reject any “.” characters in both group IDs
and template field IDs because slot_id encoding and path.split(".") decoding
will rebuild the wrong nested row shape. Update the DynamicGroup validation
logic around field_ids and the related DynamicGroup ID handling paths to assert
that neither the group id nor any template field id contains ".", and surface a
clear validation error before rows are encoded or decoded.

---

Nitpick comments:
In `@tests-unit/comfy_api_test/io_dynamic_group_test.py`:
- Around line 198-204: Add a regression test in io_dynamic_group_test around
DynamicGroup.Input/_run that covers the max overflow path: create a group with a
small max and submit more indexed rows than allowed, then assert the backend
rejects the extra row(s) instead of reconstructing them. Reuse the existing test
helpers and symbols like test_no_leftover_flat_keys, DynamicGroup.Input, and
_run so the new case sits alongside the current flat-key coverage and verifies
the cap contract explicitly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cdfa785-547c-4784-a1bd-881bbd413d05

📥 Commits

Reviewing files that changed from the base of the PR and between a3b9cf8 and abd185c.

📒 Files selected for processing (2)
  • comfy_api/latest/_io.py
  • tests-unit/comfy_api_test/io_dynamic_group_test.py

Comment thread comfy_api/latest/_io.py
Comment on lines +1305 to +1308
field_ids = [t.id for t in template]
assert len(field_ids) == len(set(field_ids)), (
f"DynamicGroup template field ids must be unique within a row. Got: {field_ids}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject . in DynamicGroup IDs and template field IDs.

slot_id is encoded with "." and later decoded with path.split("."); a group or field id containing . reconstructs into a different nested shape than the documented list[dict] row contract.

Proposed validation
             super().__init__(id, display_name, optional, tooltip, lazy, extra_dict)
+            assert "." not in id, "DynamicGroup id must not contain '.'."
             # Validate template entries: only WidgetInput subclasses, no nesting
             assert len(template) > 0, "DynamicGroup template must have at least one field."
             for t in template:
                 assert isinstance(t, WidgetInput), (
                     f"DynamicGroup template field '{t.id}' must be a WidgetInput subclass "
                     f"(Combo, Float, Int, String, Boolean, Color). Got {type(t).__name__}."
                 )
+                assert "." not in t.id, f"DynamicGroup template field id '{t.id}' must not contain '.'."
                 assert not isinstance(t, DynamicInput), (
                     f"DynamicGroup template field '{t.id}' must not be a DynamicInput. "
                     "Nesting dynamic inputs inside DynamicGroup is not supported."
                 )

Also applies to: 1375-1382, 1942-1962

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy_api/latest/_io.py` around lines 1305 - 1308, The DynamicGroup ID
validation currently checks only for uniqueness, but it also needs to reject any
“.” characters in both group IDs and template field IDs because slot_id encoding
and path.split(".") decoding will rebuild the wrong nested row shape. Update the
DynamicGroup validation logic around field_ids and the related DynamicGroup ID
handling paths to assert that neither the group id nor any template field id
contains ".", and surface a clear validation error before rows are encoded or
decoded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant