Files
olivv-cs-ComfyUI-FunPack/core/nodes.py
T
digital-garbageandClaude Opus 5 74671329f9 log: a severity, a source, and a message
[Log] Info: FunPack Diffusion Model Loader | flux.safetensors loaded, attention sage
    [Log] Alert: MiniMaxH3ImageToVideo node | no "first_image" input is connected
    [Log] Warning: FunPack LoRA Loader | style.safetensors is empty and will not load
    [Log] Critical: torch | cannot allocate 14 GB (3 GB free); the run will not start

The difference between the middle two is the one that matters. Alert is a
choice you may not have made on purpose. Warning is something that will NOT
do what its name says -- the level v4 needed and did not have, which is how
560 handlers came to swallow a failure and carry on, so a feature could be
switched on, do nothing, and never say so.

Info is not decoration either: "loaded on GPU using sage attention" is how
an attention setting that did nothing gets noticed months before the
pictures look wrong enough to investigate.

Every call site moved -- there is no note() left to be vague through -- and
the loaders now say what they actually loaded rather than only what they
were asked for. Records are kept in a bounded ring as well as printed, so a
long rental session cannot turn the log into a memory leak, and served at
/funpack/api/log so a person can read it somewhere other than a terminal
full of ComfyUI's own output. There is a page for it.

Also dropped the preflight this was traded for: min and max are already
declared and already enforced at both doors, and a second protection layer
around a check that exists is ceremony.

Two mistakes worth keeping: a log line reached into the model's shape to
describe it and could have failed the load it was only narrating; and both
new pages passed `children` to an element that takes `rows`, which the kit
accepted and silently dropped. It throws on an unknown element and not on an
unknown prop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:09:33 +03:00

103 lines
4.5 KiB
Python

"""Collecting the ComfyUI nodes that modules announced.
Core holds no list of nodes. It walks the registry, takes whatever each module
declared in `NODES`, and hands the result to ComfyUI -- the same inversion the
panels use, applied to node registration.
Nothing here may raise. ComfyUI wraps `comfy_entrypoint` in one try/except and
skips the ENTIRE pack on any exception (nodes.py, the `elif` branch), so a single
bad module must not take every other node down with it. Every failure is caught,
logged once, and turns into that node's absence.
"""
from typing import List, Optional, Tuple
from . import log, registry as registry_mod
# Node ids are global in ComfyUI: `NODE_CLASS_MAPPINGS[schema.node_id]`, with no
# collision check -- a clash silently overwrites whoever registered first. The
# prefix is what keeps FunPack out of everyone else's namespace.
PREFIX = "FunPack"
def _schema_of(node) -> Optional[object]:
"""The node's V3 schema, or None if it cannot produce one."""
getter = getattr(node, "GET_SCHEMA", None)
if not callable(getter):
return None
return getter()
def collect(registry=None) -> Tuple[List[type], List[Tuple[str, str]]]:
"""(nodes, rejected) -- rejected carries the reason, for the modules dump."""
reg = registry if registry is not None else registry_mod.current()
nodes: List[type] = []
rejected: List[Tuple[str, str]] = []
claimed = {} # node_id -> module id
# Sorted so the set of registered nodes does not depend on filesystem order.
for spec in sorted(reg.specs.values(), key=lambda s: s.id):
for node in spec.nodes:
# Named inside the guard, and starting from a name that cannot fail.
# `getattr(node, "__name__", repr(node))` looks safe and is not:
# Python evaluates the default eagerly, so a class whose metaclass
# raises in __repr__ threw BEFORE the try and took every other
# module's nodes with it -- the one thing this loop exists to stop.
where = f"{spec.id}.<unnamed node>"
try:
name = getattr(node, "__name__", None)
if isinstance(name, str) and name:
where = f"{spec.id}.{name}"
schema = _schema_of(node)
except Exception as exc: # noqa: BLE001 -- absence, not a crash
log.failed(where, exc)
rejected.append((where, f"{type(exc).__name__}: {exc}"))
continue
if schema is None:
rejected.append((where, "is not a comfy_api io.ComfyNode (no GET_SCHEMA)"))
log.warning(where, "is not a ComfyUI V3 node and was not registered")
continue
node_id = getattr(schema, "node_id", None)
if not isinstance(node_id, str) or not node_id.startswith(PREFIX):
rejected.append((where, f"node_id {node_id!r} does not start with {PREFIX!r}"))
log.warning(where, f"declares node_id {node_id!r}, which is not prefixed "
f"{PREFIX!r}, and was not registered")
continue
if node_id in claimed:
# ComfyUI would let the second one silently replace the first,
# and which one wins would depend on import order.
rejected.append((where, f"duplicate node_id {node_id!r}, already "
f"declared by {claimed[node_id]}"))
log.warning(where, f"reuses node_id {node_id!r}, already declared by "
f"{claimed[node_id]}, and was not registered")
continue
claimed[node_id] = where
nodes.append(node)
return nodes, rejected
def extension():
"""A ComfyExtension over whatever announced itself, or None without ComfyUI."""
try:
from comfy_api.latest import ComfyExtension
except Exception as exc: # noqa: BLE001 -- not inside ComfyUI
log.failed("comfy_api.latest", exc)
return None
nodes, rejected = collect()
class FunPackExtension(ComfyExtension):
async def get_node_list(self) -> List[type]:
return list(nodes)
if rejected:
log.warning("nodes", f"{len(rejected)} node(s) were not registered; "
+ "; ".join(f"{where}: {why}" for where, why in rejected))
log.info("nodes", f"registered {len(nodes)} node(s)")
return FunPackExtension()