Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5cccb21a3 | |||
| 36a6124ba9 |
@@ -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.
|
||||
|
||||
@@ -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,11 +19,27 @@ 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
|
||||
|
||||
Clone the public repository into ComfyUI's `custom_nodes` directory and restart
|
||||
ComfyUI:
|
||||
Install the immutable Registry release with Comfy CLI:
|
||||
|
||||
```bash
|
||||
comfy node install etk-from-list-index@1.1.0
|
||||
```
|
||||
|
||||
New Registry releases have a short pending security-scan window. During that
|
||||
window the exact version remains installable, but the `@latest` alias is not
|
||||
added to ComfyUI-Manager's active CNR index yet.
|
||||
|
||||
Alternatively, clone the public repository into ComfyUI's `custom_nodes`
|
||||
directory and restart ComfyUI:
|
||||
|
||||
```bash
|
||||
git clone https://git.hoppingmadgames.com/hmg-comfy/ComfyUI_ETK_From_List_Index.git
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
+2
-2
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -56,3 +57,47 @@ def test_publish_keeps_token_out_of_arguments_and_environment(monkeypatch, tmp_p
|
||||
assert "private-token" not in repr(calls[1][0])
|
||||
assert "private-token" not in repr(publish_kwargs["env"])
|
||||
assert publish_kwargs["env"]["COMFY_NODE_CHANGELOG"] == "release notes"
|
||||
|
||||
|
||||
def test_registry_release_status_finds_exact_published_version(monkeypatch, tmp_path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[project]\nname = "etk-from-list-index"\nversion = "1.0.0"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
class Response(BytesIO):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
payload = b'{"versions":[{"version":"1.0.0","status":"NodeVersionStatusPending"}]}'
|
||||
monkeypatch.setattr(
|
||||
publish_registry_release.urllib.request,
|
||||
"urlopen",
|
||||
lambda *args, **kwargs: Response(payload),
|
||||
)
|
||||
|
||||
assert publish_registry_release.registry_release_status(tmp_path) == (
|
||||
"etk-from-list-index",
|
||||
"1.0.0",
|
||||
"NodeVersionStatusPending",
|
||||
)
|
||||
|
||||
|
||||
def test_pending_registry_status_does_not_claim_latest_is_ready(
|
||||
monkeypatch, capsys, tmp_path
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
publish_registry_release,
|
||||
"registry_release_status",
|
||||
lambda source: ("etk-from-list-index", "1.0.0", "NodeVersionStatusPending"),
|
||||
)
|
||||
|
||||
publish_registry_release.print_registry_install_status(tmp_path)
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "comfy node install etk-from-list-index@1.0.0" in output
|
||||
assert "unavailable until" in output
|
||||
assert "etk-from-list-index@latest" not in output
|
||||
|
||||
@@ -5,15 +5,22 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from tools.verify_manager_install import verify_manager_install
|
||||
|
||||
|
||||
REGISTRY_VERSIONS_URL = "https://api.comfy.org/versions"
|
||||
|
||||
|
||||
def publish_release(
|
||||
source: Path,
|
||||
*,
|
||||
@@ -35,6 +42,41 @@ def publish_release(
|
||||
)
|
||||
|
||||
|
||||
def registry_release_status(source: Path) -> tuple[str, str, str]:
|
||||
with (source / "pyproject.toml").open("rb") as stream:
|
||||
project = tomllib.load(stream)["project"]
|
||||
node_id = str(project["name"])
|
||||
version = str(project["version"])
|
||||
query = urllib.parse.urlencode(
|
||||
{
|
||||
"nodeId": node_id,
|
||||
"include_status_reason": "true",
|
||||
"pageSize": "100",
|
||||
}
|
||||
)
|
||||
with urllib.request.urlopen(
|
||||
f"{REGISTRY_VERSIONS_URL}?{query}", timeout=30
|
||||
) as response:
|
||||
versions = json.load(response)["versions"]
|
||||
release = next((item for item in versions if item.get("version") == version), None)
|
||||
if release is None:
|
||||
raise RuntimeError(f"Registry upload is not visible: {node_id}@{version}")
|
||||
return node_id, version, str(release.get("status", "unknown"))
|
||||
|
||||
|
||||
def print_registry_install_status(source: Path) -> None:
|
||||
node_id, version, status = registry_release_status(source)
|
||||
print(f"REGISTRY VERSION STATUS: {node_id}@{version} {status}")
|
||||
print(f"EXACT CNR INSTALL: comfy node install {node_id}@{version}")
|
||||
if status != "NodeVersionStatusActive":
|
||||
print(
|
||||
"LATEST CNR ALIAS: unavailable until the Registry security scan "
|
||||
"activates this version"
|
||||
)
|
||||
else:
|
||||
print(f"LATEST CNR INSTALL: comfy node install {node_id}@latest")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
@@ -57,6 +99,7 @@ def main() -> int:
|
||||
token=token,
|
||||
changelog=args.changelog,
|
||||
)
|
||||
print_registry_install_status(args.source.resolve())
|
||||
except (OSError, subprocess.CalledProcessError, RuntimeError) as exc:
|
||||
print(f"REGISTRY RELEASE FAILED: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user