mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-07 15:10:50 +08:00
TypeResolver: trim verbose comments and docstrings
Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
parent
15f55f1b24
commit
004ac8820b
@ -1357,12 +1357,9 @@ class Range(ComfyTypeIO):
|
||||
})
|
||||
|
||||
|
||||
# Signature: (out_dict, live_inputs, value, input_type, curr_prefix, live_input_types)
|
||||
# live_input_types is an optional {input_id: resolved_io_type} dict produced
|
||||
# by comfy_execution.type_resolver.TypeResolver. Existing dynamic-input
|
||||
# implementations may ignore it; future type-discriminated dynamic inputs
|
||||
# (e.g. a per-connected-type variant of DynamicCombo) use it as their
|
||||
# discriminator instead of literal live_inputs values.
|
||||
# Signature: (out_dict, live_inputs, value, input_type, curr_prefix, live_input_types).
|
||||
# live_input_types is {input_id: resolved_io_type} from TypeResolver; existing
|
||||
# expanders ignore it, future type-discriminated ones use it as discriminator.
|
||||
_DynamicInputFunc = Callable[
|
||||
[dict[str, Any], dict[str, Any], tuple[str, dict[str, Any]], str, list[str] | None, dict[str, str] | None],
|
||||
None,
|
||||
@ -1722,15 +1719,8 @@ class Schema:
|
||||
def get_finalized_class_inputs(d: dict[str, Any], live_inputs: dict[str, Any], include_hidden=False, live_input_types: dict[str, str] | None = None) -> tuple[dict[str, Any], V3Data]:
|
||||
"""Expand a node's V3 schema against a concrete prompt.
|
||||
|
||||
Args:
|
||||
d: ``INPUT_TYPES()``-shaped dict for the node.
|
||||
live_inputs: Concrete ``{input_id: value}`` map from the prompt
|
||||
(values may be links ``[node_id, slot_idx]`` or literals).
|
||||
include_hidden: When True, retain hidden inputs in the returned dict.
|
||||
live_input_types: Optional ``{input_id: resolved_io_type}`` map
|
||||
produced by ``comfy_execution.type_resolver.TypeResolver``. Future
|
||||
dynamic-input strategies that branch on connected type use this as
|
||||
their discriminator. Existing dynamic types ignore it.
|
||||
``live_input_types`` is an optional ``{input_id: resolved_io_type}`` map
|
||||
(from ``TypeResolver``) used by future type-discriminated dynamic inputs.
|
||||
"""
|
||||
out_dict = {
|
||||
"required": {},
|
||||
|
||||
@ -26,9 +26,7 @@ class DynamicPrompt:
|
||||
self.ephemeral_prompt = {}
|
||||
self.ephemeral_parents = {}
|
||||
self.ephemeral_display = {}
|
||||
# Lazily-built type resolver, scoped to this DynamicPrompt's lifetime.
|
||||
# Invalidated whenever the graph mutates via add_ephemeral_node.
|
||||
self._type_resolver = None
|
||||
self._type_resolver = None # lazy; invalidated by add_ephemeral_node
|
||||
|
||||
def get_node(self, node_id):
|
||||
if node_id in self.ephemeral_prompt:
|
||||
@ -44,9 +42,8 @@ class DynamicPrompt:
|
||||
self.ephemeral_prompt[node_id] = node_info
|
||||
self.ephemeral_parents[node_id] = parent_id
|
||||
self.ephemeral_display[node_id] = display_id
|
||||
# Conservatively invalidate the entire resolver cache. Selective
|
||||
# downstream invalidation would require topological info we don't have
|
||||
# here cheaply; the resolver's cache is small and easy to rebuild.
|
||||
# Selective downstream invalidation would need topological info; the
|
||||
# cache is small, so just wipe it.
|
||||
if self._type_resolver is not None:
|
||||
self._type_resolver.invalidate()
|
||||
|
||||
|
||||
@ -1,22 +1,12 @@
|
||||
"""Server-side type resolver for prompt graphs.
|
||||
|
||||
Resolves the concrete io_type of an output slot or input slot by walking the
|
||||
prompt graph. Handles:
|
||||
Resolves the concrete io_type of any output/input slot by walking the prompt
|
||||
graph. Handles V1/V3 ``RETURN_TYPES``, V3 ``MatchType`` template chains, and
|
||||
falls back to ``AnyType`` (with a one-shot warning) on cycles, depth overflow,
|
||||
or unresolvable wildcards.
|
||||
|
||||
* Static V1/V3 ``RETURN_TYPES`` (returned as-is).
|
||||
* V3 ``MatchType.Output`` (resolved by walking inputs that share the same
|
||||
``template_id`` until a concrete type is found).
|
||||
* Cycles and unbounded recursion (terminates at ``AnyType`` with a one-shot
|
||||
warning).
|
||||
* Unknown / unresolvable / wildcard outputs (fall back to ``AnyType`` with a
|
||||
one-shot warning).
|
||||
|
||||
The resolver works against either a raw prompt dict
|
||||
(``{node_id: {"class_type": str, "inputs": dict}}``) or a
|
||||
``comfy_execution.graph.DynamicPrompt`` instance.
|
||||
|
||||
All resolved values are plain strings, so the resolver state is trivially
|
||||
serializable across processes if needed.
|
||||
Works against either a raw prompt dict or a ``DynamicPrompt``. All resolved
|
||||
values are strings, so resolver state is cross-process serializable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -29,30 +19,23 @@ from comfy_api.internal import _ComfyNodeInternal
|
||||
|
||||
|
||||
def _parse_link(val: Any) -> tuple[str, int] | None:
|
||||
"""Return (src_node_id, src_slot_idx) if ``val`` is a well-formed link.
|
||||
"""Return ``(src_node_id, src_slot_idx)`` if ``val`` is a well-formed link, else ``None``.
|
||||
|
||||
A link in the prompt schema is a length-2 list/tuple ``[node_id, slot_idx]``
|
||||
where ``node_id`` is a string and ``slot_idx`` is a non-negative int.
|
||||
Anything else (including ``[node_id, "0"]`` from malformed API JSON) returns
|
||||
``None`` so callers can fall back to AnyType instead of crashing.
|
||||
A link is ``[node_id: str, slot_idx: int]``. Malformed shapes return ``None``
|
||||
so callers can fall back to AnyType rather than raise.
|
||||
"""
|
||||
if not isinstance(val, (list, tuple)) or len(val) != 2:
|
||||
return None
|
||||
src_node, src_slot = val[0], val[1]
|
||||
if not isinstance(src_node, str):
|
||||
return None
|
||||
# bool is a subclass of int — reject it to avoid treating True/False as slot 1/0.
|
||||
# bool is a subclass of int — reject so True/False aren't read as slot 1/0.
|
||||
if isinstance(src_slot, bool) or not isinstance(src_slot, int):
|
||||
return None
|
||||
return src_node, src_slot
|
||||
|
||||
# Sentinel for "type is unknown / wildcard". Matches AnyType.io_type ("*").
|
||||
ANY_TYPE: str = io.AnyType.io_type
|
||||
|
||||
# Hard cap on resolver recursion depth. MatchType chains should never be
|
||||
# anywhere near this deep; this is a belt-and-suspenders guard against malformed
|
||||
# graphs and pathological cycles.
|
||||
MAX_RESOLVE_DEPTH: int = 64
|
||||
MAX_RESOLVE_DEPTH: int = 64 # belt-and-suspenders cap; real MatchType chains stay tiny
|
||||
|
||||
|
||||
class TypeResolver:
|
||||
@ -90,8 +73,7 @@ class TypeResolver:
|
||||
|
||||
@staticmethod
|
||||
def _get_class_def(class_type: str):
|
||||
# Local import to avoid a hard import-cycle between nodes.py and
|
||||
# comfy_execution at module-load time.
|
||||
# Local import: nodes <-> comfy_execution would cycle at import time.
|
||||
import nodes
|
||||
return nodes.NODE_CLASS_MAPPINGS.get(class_type)
|
||||
|
||||
@ -112,8 +94,7 @@ class TypeResolver:
|
||||
"""Clear all cached resolutions. Cheap; call after any graph mutation."""
|
||||
self._output_cache.clear()
|
||||
self._is_output_list_cache.clear()
|
||||
# Intentionally do NOT clear self._warned: those messages are already
|
||||
# logged and re-warning would just spam the log.
|
||||
# Keep self._warned: re-emitting already-logged warnings would just spam.
|
||||
|
||||
def invalidate_node(self, node_id: str) -> None:
|
||||
"""Clear cached entries for a single node (e.g. after node-level expand)."""
|
||||
@ -131,9 +112,7 @@ class TypeResolver:
|
||||
out-of-range slot, missing node, malformed link, or unresolved
|
||||
MatchType template.
|
||||
"""
|
||||
# Guard against malformed callers passing non-int slot indices (e.g.
|
||||
# API JSON that sent a string). Falling back to AnyType is safer than
|
||||
# raising TypeError mid-validation.
|
||||
# Degrade gracefully on non-int slot_idx (e.g. malformed API JSON).
|
||||
if isinstance(slot_idx, bool) or not isinstance(slot_idx, int):
|
||||
return ANY_TYPE
|
||||
|
||||
@ -165,15 +144,13 @@ class TypeResolver:
|
||||
|
||||
declared = return_types[slot_idx]
|
||||
|
||||
# V3 nodes may have MatchType outputs that need to be traced through
|
||||
# the schema. V1 nodes (and V3 nodes with plain outputs) just use the
|
||||
# declared RETURN_TYPES string.
|
||||
# Only V3 schemas carry MatchType template info; V1 RETURN_TYPES are
|
||||
# always concrete strings.
|
||||
resolved = declared
|
||||
if isinstance(class_def, type) and issubclass(class_def, _ComfyNodeInternal):
|
||||
schema = getattr(class_def, "SCHEMA", None)
|
||||
if schema is None:
|
||||
# Trigger schema computation. RETURN_TYPES would have done this
|
||||
# already, but be defensive.
|
||||
# RETURN_TYPES access above usually populates SCHEMA — be defensive.
|
||||
try:
|
||||
schema = class_def.GET_SCHEMA()
|
||||
except Exception:
|
||||
@ -185,9 +162,8 @@ class TypeResolver:
|
||||
node_id, schema, out.template.template_id, next_stack
|
||||
)
|
||||
|
||||
# Treat the legacy wildcard literally as AnyType. We warn only when the
|
||||
# source node's *declared* type was already wildcard, so MatchType-style
|
||||
# "no upstream connected" cases (which warn elsewhere) don't double-warn.
|
||||
# Warn only for V1 wildcards declared as "*"; unresolved MatchType
|
||||
# templates warn separately in _resolve_match_template, avoiding double-warns.
|
||||
if isinstance(resolved, str) and resolved == ANY_TYPE and declared == ANY_TYPE:
|
||||
self._warn(
|
||||
node_id, slot_idx,
|
||||
@ -195,7 +171,7 @@ class TypeResolver:
|
||||
)
|
||||
|
||||
if not isinstance(resolved, str):
|
||||
# Non-string types (e.g., legacy combos passed as list) — bail to AnyType.
|
||||
# e.g. legacy combo declared as a list of options.
|
||||
self._warn(node_id, slot_idx,
|
||||
f"node '{class_type}' output slot {slot_idx} has non-string return type {type(resolved).__name__}; defaulting to AnyType")
|
||||
resolved = ANY_TYPE
|
||||
@ -205,13 +181,7 @@ class TypeResolver:
|
||||
|
||||
def _resolve_match_template(self, node_id: str, schema, template_id: str,
|
||||
stack: frozenset[tuple[str, int]]) -> str:
|
||||
"""Resolve a MatchType.Output by inspecting the node's MatchType.Inputs
|
||||
with the same template_id.
|
||||
|
||||
Strategy (per design decision): walk inputs in schema order, pick the
|
||||
FIRST concrete (non-AnyType) resolution. If none resolve, return
|
||||
AnyType with a one-shot warning.
|
||||
"""
|
||||
"""Walk MatchType.Inputs sharing ``template_id``; return first concrete resolution or ``ANY_TYPE``."""
|
||||
node = self._get_node(node_id)
|
||||
inputs_dict = (node or {}).get("inputs", {}) or {}
|
||||
any_input_seen = False
|
||||
@ -229,11 +199,9 @@ class TypeResolver:
|
||||
t = self.resolve_output_type(link[0], link[1], stack)
|
||||
if t != ANY_TYPE:
|
||||
return t
|
||||
# Literal value (or malformed link): a MatchType slot has no
|
||||
# concrete declared type, so we cannot infer anything useful here.
|
||||
# Literal or malformed link: MatchType slots have no declared concrete type.
|
||||
if not any_input_seen:
|
||||
# Schema declared a template_id with no Input bearing it. This is a
|
||||
# node-author bug; warn once.
|
||||
# Node-author bug: output template has no matching Input.
|
||||
self._warn(node_id, None,
|
||||
f"MatchType output template '{template_id}' has no matching Input on the node; defaulting to AnyType")
|
||||
else:
|
||||
@ -315,11 +283,11 @@ class TypeResolver:
|
||||
except Exception:
|
||||
schema = None
|
||||
if schema is not None:
|
||||
# First, try a top-level input id match.
|
||||
# Top-level input id.
|
||||
for inp in schema.inputs:
|
||||
if inp.id == input_id:
|
||||
return self._effective_io_type(inp)
|
||||
# Then a nested match (DynamicSlot / DynamicCombo prefix.child).
|
||||
# Nested (DynamicSlot / DynamicCombo `parent.child`).
|
||||
if "." in input_id:
|
||||
top, _, _ = input_id.partition(".")
|
||||
for inp in schema.inputs:
|
||||
@ -330,9 +298,8 @@ class TypeResolver:
|
||||
continue
|
||||
if child.id == input_id.split(".", 1)[1]:
|
||||
return self._effective_io_type(child)
|
||||
# Fall through to V1 dict for hidden inputs etc.
|
||||
# Fall through to V1 dict (hidden inputs, etc.).
|
||||
|
||||
# V1 fallback: look at INPUT_TYPES() dict.
|
||||
try:
|
||||
inputs = class_def.INPUT_TYPES()
|
||||
except Exception:
|
||||
@ -346,8 +313,7 @@ class TypeResolver:
|
||||
t = entry[0]
|
||||
if isinstance(t, str):
|
||||
return t
|
||||
if isinstance(t, list):
|
||||
# legacy combo declared as a list of options.
|
||||
if isinstance(t, list): # legacy combo declared as list of options
|
||||
return io.Combo.io_type
|
||||
return ANY_TYPE
|
||||
return ANY_TYPE
|
||||
@ -355,22 +321,20 @@ class TypeResolver:
|
||||
@staticmethod
|
||||
def _effective_io_type(inp) -> str:
|
||||
"""Return the consumer-facing io_type of a (possibly dynamic) input."""
|
||||
# Autogrow wraps a template input — the element type is what matters.
|
||||
# Autogrow / DynamicSlot wrap a real element type; that's what consumers care about.
|
||||
if isinstance(inp, io.Autogrow.Input):
|
||||
try:
|
||||
return inp.template.input.get_io_type()
|
||||
except Exception:
|
||||
return ANY_TYPE
|
||||
# DynamicSlot wraps an underlying slot input.
|
||||
if isinstance(inp, io.DynamicSlot.Input):
|
||||
try:
|
||||
return inp.slot.get_io_type()
|
||||
except Exception:
|
||||
return ANY_TYPE
|
||||
# DynamicCombo's "type" is a key value selector, not a connection type.
|
||||
# DynamicCombo's "type" is a key selector, not a connection type.
|
||||
if isinstance(inp, io.DynamicCombo.Input):
|
||||
return ANY_TYPE
|
||||
# Everything else: trust the input's declared io_type.
|
||||
try:
|
||||
return inp.get_io_type()
|
||||
except Exception:
|
||||
@ -380,9 +344,8 @@ class TypeResolver:
|
||||
def compute_live_input_types(self, node_id: str) -> dict[str, str]:
|
||||
"""Build the ``{input_id: resolved_io_type}`` map for a node.
|
||||
|
||||
Used by :py:func:`comfy_api.latest._io.get_finalized_class_inputs` so
|
||||
future dynamic-input expansion strategies (per-type DynamicType, etc.)
|
||||
can branch on what was actually connected.
|
||||
Consumed by ``_io.get_finalized_class_inputs`` so future per-type
|
||||
dynamic-input expansion can branch on what was actually connected.
|
||||
"""
|
||||
node = self._get_node(node_id)
|
||||
if node is None:
|
||||
|
||||
22
execution.py
22
execution.py
@ -84,7 +84,6 @@ class IsChangedCache:
|
||||
return self.is_changed[node_id]
|
||||
|
||||
# Intentionally do not use cached outputs here. We only want constants in IS_CHANGED.
|
||||
# Pass dynprompt so the TypeResolver can resolve link types for V3 dynamic schemas.
|
||||
input_data_all, _, v3_data = get_input_data(node["inputs"], class_def, node_id, None, self.dynprompt)
|
||||
try:
|
||||
is_changed = await _async_map_node_over_list(self.prompt_id, node_id, class_def, input_data_all, is_changed_name, v3_data=v3_data)
|
||||
@ -159,11 +158,7 @@ def get_input_data(inputs, class_def, unique_id, execution_list=None, dynprompt=
|
||||
hidden_inputs_v3 = {}
|
||||
valid_inputs = class_def.INPUT_TYPES()
|
||||
if is_v3:
|
||||
# Build the type-resolution map for this node so dynamic schemas can
|
||||
# branch on resolved upstream types (and not only on literal values).
|
||||
# When no DynamicPrompt is available (e.g. some IsChangedCache paths
|
||||
# in tests), live_input_types stays None and only literal-driven
|
||||
# dynamic types continue to work.
|
||||
# Let dynamic schemas branch on resolved upstream types, not just literal values.
|
||||
live_input_types = None
|
||||
if dynprompt is not None and hasattr(dynprompt, "get_type_resolver"):
|
||||
live_input_types = dynprompt.get_type_resolver().compute_live_input_types(unique_id)
|
||||
@ -833,10 +828,8 @@ class PromptExecutor:
|
||||
async def validate_inputs(prompt_id, prompt, item, validated, visiting=None, type_resolver=None):
|
||||
"""Validate inputs for a single node, recursing into upstream nodes.
|
||||
|
||||
``type_resolver`` (a ``comfy_execution.type_resolver.TypeResolver``) is
|
||||
built once at the top of the recursion and reused so MatchType chains are
|
||||
only walked once. It also gives V3 dynamic schemas an accurate map of
|
||||
resolved upstream types for API-submitted workflows.
|
||||
``type_resolver`` is built once at the top of recursion and shared so
|
||||
MatchType chains are only walked once per prompt.
|
||||
"""
|
||||
if visiting is None:
|
||||
visiting = []
|
||||
@ -929,10 +922,8 @@ async def validate_inputs(prompt_id, prompt, item, validated, visiting=None, typ
|
||||
|
||||
o_id = val[0]
|
||||
o_class_type = prompt[o_id]['class_type']
|
||||
# Resolve the upstream output's effective type through the
|
||||
# TypeResolver. This walks MatchType/template chains, so an API
|
||||
# workflow without frontend-injected type metadata still gets the
|
||||
# same answer the UI does.
|
||||
# Walks MatchType/template chains so API workflows without
|
||||
# frontend-injected type metadata get the same answer as the UI.
|
||||
received_type = type_resolver.resolve_output_type(o_id, val[1])
|
||||
received_types[x] = received_type
|
||||
if 'input_types' not in validate_function_inputs and not validate_node_input(received_type, input_type):
|
||||
@ -1178,8 +1169,7 @@ async def validate_prompt(prompt_id, prompt, partial_execution_list: Union[list[
|
||||
errors = []
|
||||
node_errors = {}
|
||||
validated = {}
|
||||
# Share one TypeResolver across all output validations so MatchType chains
|
||||
# are only walked once per prompt.
|
||||
# Shared across output validations so MatchType chains walk only once.
|
||||
from comfy_execution.type_resolver import TypeResolver
|
||||
type_resolver = TypeResolver(prompt)
|
||||
for o in outputs:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user