mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-03 21:20:49 +08:00
Merge 465d1a95da into 35c1470935
This commit is contained in:
commit
ecade741c7
55
execution.py
55
execution.py
@ -1113,6 +1113,32 @@ def full_type_name(klass):
|
|||||||
return klass.__qualname__
|
return klass.__qualname__
|
||||||
return module + '.' + klass.__qualname__
|
return module + '.' + klass.__qualname__
|
||||||
|
|
||||||
|
def node_not_executable_reason(class_def, class_type):
|
||||||
|
"""Return a human-readable reason the node cannot be executed, or None if it's fine.
|
||||||
|
|
||||||
|
Catches a node whose declared entry point doesn't resolve to a real method
|
||||||
|
(e.g. a V1 ``FUNCTION = "invert"`` where the method is misspelled, or a V3 node
|
||||||
|
missing its ``execute`` override). Running this during validation surfaces the
|
||||||
|
problem before execution starts, instead of after upstream nodes have run.
|
||||||
|
|
||||||
|
Only the class is inspected; the node is never instantiated here, so a node's
|
||||||
|
``__init__`` side effects cannot run (or fail) during validation.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if issubclass(class_def, _ComfyNodeInternal):
|
||||||
|
# V3: validates that execute()/define_schema() overrides exist.
|
||||||
|
class_def.VALIDATE_CLASS()
|
||||||
|
return None
|
||||||
|
# V1: FUNCTION names the method to call; it must exist on the class.
|
||||||
|
function_name = getattr(class_def, "FUNCTION", None)
|
||||||
|
if function_name is None:
|
||||||
|
return f"'{class_type}' does not define FUNCTION"
|
||||||
|
if not callable(getattr(class_def, function_name, None)):
|
||||||
|
return f"'{class_type}' has no method '{function_name}' (declared in FUNCTION)"
|
||||||
|
return None
|
||||||
|
except Exception as ex:
|
||||||
|
return str(ex)
|
||||||
|
|
||||||
async def validate_prompt(prompt_id, prompt, partial_execution_list: Union[list[str], None]):
|
async def validate_prompt(prompt_id, prompt, partial_execution_list: Union[list[str], None]):
|
||||||
outputs = set()
|
outputs = set()
|
||||||
for x in prompt:
|
for x in prompt:
|
||||||
@ -1148,6 +1174,35 @@ async def validate_prompt(prompt_id, prompt, partial_execution_list: Union[list[
|
|||||||
}
|
}
|
||||||
return (False, error, [], {})
|
return (False, error, [], {})
|
||||||
|
|
||||||
|
# Make sure the node is actually executable (its FUNCTION/execute entry
|
||||||
|
# point resolves to a real method) before we touch any schema-derived
|
||||||
|
# attributes below or start execution. Catches code typos up front and
|
||||||
|
# attributes the error to the offending node.
|
||||||
|
not_executable = node_not_executable_reason(class_, class_type)
|
||||||
|
if not_executable is not None:
|
||||||
|
node_title = prompt[x].get('_meta', {}).get('title', class_type)
|
||||||
|
error = {
|
||||||
|
"type": "invalid_node_definition",
|
||||||
|
"message": "Node is not executable",
|
||||||
|
"details": f"{not_executable} (Node ID '#{x}')",
|
||||||
|
"extra_info": {
|
||||||
|
"node_id": x,
|
||||||
|
"class_type": class_type,
|
||||||
|
"node_title": node_title,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node_errors = {x: {
|
||||||
|
"errors": [{
|
||||||
|
"type": "invalid_node_definition",
|
||||||
|
"message": "Node is not executable",
|
||||||
|
"details": not_executable,
|
||||||
|
"extra_info": {},
|
||||||
|
}],
|
||||||
|
"dependent_outputs": [],
|
||||||
|
"class_type": class_type,
|
||||||
|
}}
|
||||||
|
return (False, error, [], node_errors)
|
||||||
|
|
||||||
if hasattr(class_, 'OUTPUT_NODE') and class_.OUTPUT_NODE is True:
|
if hasattr(class_, 'OUTPUT_NODE') and class_.OUTPUT_NODE is True:
|
||||||
if partial_execution_list is None or x in partial_execution_list:
|
if partial_execution_list is None or x in partial_execution_list:
|
||||||
outputs.add(x)
|
outputs.add(x)
|
||||||
|
|||||||
137
tests-unit/execution_test/validate_node_executable_test.py
Normal file
137
tests-unit/execution_test/validate_node_executable_test.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
"""Tests for pre-execution validation that a node is actually executable.
|
||||||
|
|
||||||
|
validate_prompt rejects a node whose declared entry point does not resolve to a
|
||||||
|
real method (a V1 FUNCTION typo, or a V3 node missing its execute override) before
|
||||||
|
any node runs, attributing the error to the offending node.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import nodes
|
||||||
|
from comfy_api.latest import io
|
||||||
|
from execution import node_not_executable_reason, validate_prompt
|
||||||
|
|
||||||
|
|
||||||
|
class _GoodV1Node:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {"required": {}}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "run"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
CATEGORY = "Test"
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
return (None,)
|
||||||
|
|
||||||
|
|
||||||
|
class _TypoV1Node:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {"required": {}}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "invert" # method below is misspelled
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
CATEGORY = "Test"
|
||||||
|
|
||||||
|
def invvert(self):
|
||||||
|
return (None,)
|
||||||
|
|
||||||
|
|
||||||
|
class _SideEffectInitV1Node:
|
||||||
|
"""Valid class-level method, but a constructor that must never run in validation."""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {"required": {}}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "run"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
CATEGORY = "Test"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
raise RuntimeError("__init__ must not run during validation")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
return (None,)
|
||||||
|
|
||||||
|
|
||||||
|
def _v3_schema(node_id):
|
||||||
|
return io.Schema(
|
||||||
|
node_id=node_id,
|
||||||
|
display_name=node_id,
|
||||||
|
category="Test",
|
||||||
|
inputs=[],
|
||||||
|
outputs=[io.Image.Output()],
|
||||||
|
is_output_node=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _GoodV3Node(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return _v3_schema("GoodV3Node")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls):
|
||||||
|
return io.NodeOutput(None)
|
||||||
|
|
||||||
|
|
||||||
|
class _TypoV3Node(io.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls):
|
||||||
|
return _v3_schema("TypoV3Node")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def exicute(cls): # typo: should be "execute"
|
||||||
|
return io.NodeOutput(None)
|
||||||
|
|
||||||
|
|
||||||
|
def _register(class_type, class_def):
|
||||||
|
nodes.NODE_CLASS_MAPPINGS[class_type] = class_def
|
||||||
|
|
||||||
|
|
||||||
|
def _validate(class_type):
|
||||||
|
prompt = {"1": {"class_type": class_type, "inputs": {}}}
|
||||||
|
return asyncio.run(validate_prompt("pid", prompt, None))
|
||||||
|
|
||||||
|
|
||||||
|
def test_good_node_passes():
|
||||||
|
_register("GoodV1Node", _GoodV1Node)
|
||||||
|
assert node_not_executable_reason(_GoodV1Node, "GoodV1Node") is None
|
||||||
|
valid, _, _, _ = _validate("GoodV1Node")
|
||||||
|
assert valid is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_typo_node_rejected_with_node_error():
|
||||||
|
_register("TypoV1Node", _TypoV1Node)
|
||||||
|
valid, error, _, node_errors = _validate("TypoV1Node")
|
||||||
|
assert valid is False
|
||||||
|
assert error["type"] == "invalid_node_definition"
|
||||||
|
assert node_errors["1"]["class_type"] == "TypoV1Node"
|
||||||
|
assert node_errors["1"]["errors"][0]["type"] == "invalid_node_definition"
|
||||||
|
assert "invert" in node_errors["1"]["errors"][0]["details"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_validation_does_not_instantiate_node():
|
||||||
|
"""A valid node is not constructed during validation, so __init__ never runs."""
|
||||||
|
_register("SideEffectInitV1Node", _SideEffectInitV1Node)
|
||||||
|
assert node_not_executable_reason(_SideEffectInitV1Node, "SideEffectInitV1Node") is None
|
||||||
|
valid, _, _, _ = _validate("SideEffectInitV1Node")
|
||||||
|
assert valid is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_good_v3_node_passes():
|
||||||
|
_register("GoodV3Node", _GoodV3Node)
|
||||||
|
assert node_not_executable_reason(_GoodV3Node, "GoodV3Node") is None
|
||||||
|
valid, _, _, _ = _validate("GoodV3Node")
|
||||||
|
assert valid is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_typo_v3_node_rejected_with_node_error():
|
||||||
|
_register("TypoV3Node", _TypoV3Node)
|
||||||
|
valid, error, _, node_errors = _validate("TypoV3Node")
|
||||||
|
assert valid is False
|
||||||
|
assert error["type"] == "invalid_node_definition"
|
||||||
|
assert node_errors["1"]["errors"][0]["type"] == "invalid_node_definition"
|
||||||
Loading…
Reference in New Issue
Block a user