Files
Claude ec2d8fe326 Implement full Pipedream node pack (v0.1.0)
Seven production pipeline utility nodes for ComfyUI:
- PD Project Context: top-level metadata/token source
- PD Shot Token: lightweight template string resolver
- PD Output Namer: structured output filename builder
- PD Network Output: image writer with UNC path support
- PD Switch: multi-channel wildcard input router (up to 8×8)
- PD Wedge: iteration node with optional auto-queue injection
- PD Rasterizer: dry-run wedge cartesian product previewer

Core infrastructure:
- core/compat.py: legacy + Nodes 2.0 (V3) API compatibility shim
- core/tokens.py: standalone token resolution engine
- core/utils.py: path normalization and directory helpers
- web/pipedream_rasterizer.js: minimal JS for rasterizer preview

https://claude.ai/code/session_015UV4rjSvFatMiNtKhd1sT3
2026-04-03 01:08:21 +00:00

187 lines
6.2 KiB
Python

"""
ComfyUI API compatibility shim.
Detects whether the V3 (Nodes 2.0) API is available and provides helpers
so that individual node files can be written once using the legacy class
pattern and automatically registered for whichever API is present.
Node authors write standard legacy-style classes (INPUT_TYPES classmethod,
RETURN_TYPES tuple, instance ``execute`` method, etc.) and add a
``DISPLAY_NAME`` class attribute. The helpers here take care of the rest.
"""
import inspect
# ---------------------------------------------------------------------------
# V3 API detection
# ---------------------------------------------------------------------------
try:
from comfy_api.latest import io # type: ignore[import-not-found]
V3_AVAILABLE = True
except Exception:
V3_AVAILABLE = False
io = None # type: ignore[assignment]
# ---------------------------------------------------------------------------
# Type mapping (legacy type string -> V3 io type)
# ---------------------------------------------------------------------------
_LEGACY_TO_V3: dict[str, object] = {}
if V3_AVAILABLE:
_LEGACY_TO_V3 = {
"IMAGE": io.Image,
"LATENT": io.Latent,
"STRING": io.String,
"INT": io.Int,
"FLOAT": io.Float,
"BOOLEAN": io.Boolean,
}
def _v3_type_for(legacy_type: str):
"""Return the V3 ``io`` type object for a legacy type string."""
if legacy_type == "*":
return io.AnyType if hasattr(io, "AnyType") else io.Custom("*")
if legacy_type in _LEGACY_TO_V3:
return _LEGACY_TO_V3[legacy_type]
return io.Custom(legacy_type)
# ---------------------------------------------------------------------------
# V3 wrapper factory
# ---------------------------------------------------------------------------
def _build_v3_input(name: str, type_str: str, config: dict | None = None):
"""Create a V3 input descriptor from legacy INPUT_TYPES info."""
v3_type = _v3_type_for(type_str)
kwargs: dict = {}
if config:
for key in ("default", "min", "max", "step"):
if key in config:
kwargs[key] = config[key]
if config.get("multiline"):
kwargs["multiline"] = True
return v3_type.Input(name, **kwargs)
def _build_v3_output(type_str: str, display_name: str | None = None):
"""Create a V3 output descriptor from a legacy RETURN_TYPES entry."""
v3_type = _v3_type_for(type_str)
kwargs = {}
if display_name:
kwargs["display_name"] = display_name
return v3_type.Output(**kwargs)
def wrap_as_v3(legacy_cls):
"""
Wrap a legacy-style node class as a V3 ``io.ComfyNode`` subclass.
Returns a new class that delegates execution to an instance of
*legacy_cls* while exposing the V3 schema interface.
"""
if not V3_AVAILABLE:
return legacy_cls
input_spec = legacy_cls.INPUT_TYPES()
func_name = getattr(legacy_cls, "FUNCTION", "execute")
exec_method = getattr(legacy_cls, func_name)
param_names = [
p
for p in inspect.signature(exec_method).parameters
if p != "self"
]
# Build V3 inputs
v3_inputs = []
for section in ("required", "optional"):
for name, spec in input_spec.get(section, {}).items():
if isinstance(spec, tuple) and len(spec) >= 1:
type_str = spec[0]
config = spec[1] if len(spec) > 1 and isinstance(spec[1], dict) else None
# COMBO type — spec[0] is a list of strings
if isinstance(type_str, (list, tuple)):
inp = io.Combo.Input(name, options=list(type_str))
else:
inp = _build_v3_input(name, type_str, config)
if section == "optional":
inp = inp.make_optional()
v3_inputs.append(inp)
# Build V3 outputs
return_types = getattr(legacy_cls, "RETURN_TYPES", ())
return_names = getattr(legacy_cls, "RETURN_NAMES", (None,) * len(return_types))
v3_outputs = [
_build_v3_output(rt, rn) for rt, rn in zip(return_types, return_names)
]
# Capture in closure
_legacy_cls = legacy_cls
_param_names = param_names
_func_name = func_name
class V3Wrapper(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id=_legacy_cls.__name__,
display_name=getattr(_legacy_cls, "DISPLAY_NAME", _legacy_cls.__name__),
category=getattr(_legacy_cls, "CATEGORY", "Pipedream"),
description=getattr(_legacy_cls, "DESCRIPTION", ""),
inputs=v3_inputs,
outputs=v3_outputs,
is_output_node=getattr(_legacy_cls, "OUTPUT_NODE", False),
)
@classmethod
def execute(cls, **kwargs):
instance = _legacy_cls()
ordered = [kwargs.get(p) for p in _param_names]
result = getattr(instance, _func_name)(*ordered)
if isinstance(result, tuple):
return io.NodeOutput(*result)
return io.NodeOutput(result)
V3Wrapper.__name__ = _legacy_cls.__name__ + "_V3"
V3Wrapper.__qualname__ = V3Wrapper.__name__
return V3Wrapper
# ---------------------------------------------------------------------------
# Registration helpers
# ---------------------------------------------------------------------------
def get_node_class_mappings(node_classes: list) -> dict:
"""Build ``NODE_CLASS_MAPPINGS`` dict from a list of legacy node classes."""
return {cls.__name__: cls for cls in node_classes}
def get_display_name_mappings(node_classes: list) -> dict:
"""Build ``NODE_DISPLAY_NAME_MAPPINGS`` dict from a list of legacy node classes."""
return {
cls.__name__: getattr(cls, "DISPLAY_NAME", cls.__name__)
for cls in node_classes
}
def get_v3_nodes(node_classes: list) -> list:
"""
Return a list of V3-wrapped node classes.
Returns an empty list when the V3 API is not available.
"""
if not V3_AVAILABLE:
return []
wrapped = []
for cls in node_classes:
try:
wrapped.append(wrap_as_v3(cls))
except Exception as exc:
print(f"[Pipedream] Warning: failed to wrap {cls.__name__} for V3: {exc}")
return wrapped