diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d6b14..8e16c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.1.0 - 2026-08-27 + +- Add `List To ComfyUI List`, which exposes an ETK `LIST` through ComfyUI's + native list-mapped output contract without changing the contained items. +- Preserve all nine historical `*FromListIdx` workflow class IDs and behavior. + ## 1.0.0 - 2026-08-26 - Extract all historical `*FromListIdx` selectors into one standalone pack. diff --git a/README.md b/README.md index 660dd18..48d0839 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # ETK From List Index -A standalone ComfyUI node pack for selecting one item from a list by index. +A standalone ComfyUI node pack for selecting one item from a list by index and +converting an ETK `LIST` value into a native ComfyUI list-mapped output. The pack preserves the historical workflow class IDs, input socket names, output order, categories, and selection behavior previously supplied by @@ -18,13 +19,19 @@ replacement or rewiring. - `BytesFromListIdx` - `IntFromListIdx` - `FloatFromListIdx` +- `ETKListToComfyList` (`List To ComfyUI List`) + +`List To ComfyUI List` returns the contained items through ComfyUI's native +list-output contract. A downstream node that does not use `INPUT_IS_LIST` is +therefore executed once for every item, instead of receiving one Python list +object. ## Install Install the immutable Registry release with Comfy CLI: ```bash -comfy node install etk-from-list-index@1.0.0 +comfy node install etk-from-list-index@1.1.0 ``` New Registry releases have a short pending security-scan window. During that diff --git a/__init__.py b/__init__.py index 3f6fc08..b3154bf 100644 --- a/__init__.py +++ b/__init__.py @@ -5,6 +5,7 @@ from .nodes import ( BytesFromListIdx, ConditioningFromListIdx, DictFromListIdx, + ETKListToComfyList, FloatFromListIdx, ImageFromListIdx, IntFromListIdx, @@ -23,10 +24,12 @@ NODE_CLASS_MAPPINGS = { "BytesFromListIdx": BytesFromListIdx, "IntFromListIdx": IntFromListIdx, "FloatFromListIdx": FloatFromListIdx, + "ETKListToComfyList": ETKListToComfyList, } NODE_DISPLAY_NAME_MAPPINGS = { "AudioFromListIdx": "Audio From List Index", + "ETKListToComfyList": "List To ComfyUI List", } __all__ = [ @@ -34,6 +37,7 @@ __all__ = [ "BytesFromListIdx", "ConditioningFromListIdx", "DictFromListIdx", + "ETKListToComfyList", "FloatFromListIdx", "ImageFromListIdx", "IntFromListIdx", diff --git a/nodes.py b/nodes.py index 06b4166..4e081ed 100644 --- a/nodes.py +++ b/nodes.py @@ -6,6 +6,37 @@ from collections.abc import Mapping from copy import deepcopy +class AnyType(str): + """ComfyUI wildcard socket type for a list whose item type is not fixed.""" + + def __ne__(self, other): + return False + + +ANY_TYPE = AnyType("*") + + +class ETKListToComfyList: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "items": ("LIST", {"default": []}), + } + } + + RETURN_TYPES = (ANY_TYPE,) + RETURN_NAMES = ("items",) + OUTPUT_IS_LIST = (True,) + FUNCTION = "convert" + CATEGORY = "ETK/List" + + def convert(self, items): + if not isinstance(items, list): + raise TypeError(f"items must be a LIST, got {type(items).__name__}") + return (items,) + + class ImageFromListIdx: @classmethod def INPUT_TYPES(cls): diff --git a/pyproject.toml b/pyproject.toml index 27945da..c0d69cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "etk-from-list-index" -version = "1.0.0" -description = "Workflow-compatible list-index selector nodes for ComfyUI images, latents, conditioning, audio, and primitive values." +version = "1.1.0" +description = "Workflow-compatible ComfyUI list-index selectors and ETK LIST to native list-output conversion." license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [] diff --git a/tests/test_node_contract.py b/tests/test_node_contract.py index 321f792..d788205 100644 --- a/tests/test_node_contract.py +++ b/tests/test_node_contract.py @@ -29,16 +29,37 @@ EXPECTED_IDS = { "BytesFromListIdx", "IntFromListIdx", "FloatFromListIdx", + "ETKListToComfyList", } def test_exact_workflow_ids_are_registered(): assert set(PACKAGE.NODE_CLASS_MAPPINGS) == EXPECTED_IDS assert PACKAGE.NODE_DISPLAY_NAME_MAPPINGS == { - "AudioFromListIdx": "Audio From List Index" + "AudioFromListIdx": "Audio From List Index", + "ETKListToComfyList": "List To ComfyUI List", } +def test_etk_list_to_comfy_list_uses_native_list_output_contract(): + node = PACKAGE.ETKListToComfyList + source = ["first", {"second": 2}, 3] + + assert node.INPUT_TYPES() == { + "required": {"items": ("LIST", {"default": []})} + } + assert node.RETURN_NAMES == ("items",) + assert node.OUTPUT_IS_LIST == (True,) + assert node.CATEGORY == "ETK/List" + assert node().convert(source) == (source,) + assert node().convert(source)[0] is source + assert node().convert([]) == ([],) + assert (node.RETURN_TYPES[0] != "IMAGE") is False + + with pytest.raises(TypeError, match="items must be a LIST"): + node().convert("not-a-list") + + def test_etk_schema_and_selection_contracts(): image = PACKAGE.ImageFromListIdx latent = PACKAGE.LatentFromListIdx