From 004ac8820b3726972eef07aed5de04742c03edb3 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Mon, 1 Jun 2026 16:54:05 -0700 Subject: [PATCH] TypeResolver: trim verbose comments and docstrings Amp-Thread-ID: https://ampcode.com/threads/T-019e8568-f382-743d-a97f-0de3ff29d501 Co-authored-by: Amp --- comfy_api/latest/_io.py | 20 ++----- comfy_execution/graph.py | 9 +-- comfy_execution/type_resolver.py | 99 ++++++++++---------------------- execution.py | 22 ++----- 4 files changed, 45 insertions(+), 105 deletions(-) diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index d6e92c17b..98d19d7e0 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -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": {}, diff --git a/comfy_execution/graph.py b/comfy_execution/graph.py index ae5b338e7..eb9ac79fa 100644 --- a/comfy_execution/graph.py +++ b/comfy_execution/graph.py @@ -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() diff --git a/comfy_execution/type_resolver.py b/comfy_execution/type_resolver.py index d5d21d7a9..d22eee1ea 100644 --- a/comfy_execution/type_resolver.py +++ b/comfy_execution/type_resolver.py @@ -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: diff --git a/execution.py b/execution.py index f7d0924c8..cd58f538e 100644 --- a/execution.py +++ b/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: