Don't instantiate nodes during validation

Addresses review feedback: the V1 executability check fell back to
constructing the node (class_def()) when the FUNCTION method wasn't found on
the class. That runs __init__ during validation, so a constructor's side
effects or failure could be misreported as invalid_node_definition for an
otherwise valid node.

Inspect only the class. No core/extra node defines its FUNCTION method on the
instance, so this loses no real coverage while removing the side-effect risk.

Replace the instance-fallback test with one asserting a node with a raising
__init__ but a valid class-level method still passes validation (i.e. it is
never instantiated).
This commit is contained in:
Wei Hai 2026-06-26 16:04:29 -07:00
parent 82c954bd2a
commit bf00c39705
2 changed files with 18 additions and 20 deletions

View File

@ -1113,12 +1113,6 @@ def full_type_name(klass):
return klass.__qualname__ return klass.__qualname__
return module + '.' + klass.__qualname__ return module + '.' + klass.__qualname__
def _v1_function_resolves(node):
"""Whether node.FUNCTION names a callable on node (a class or an instance)."""
function_name = getattr(node, "FUNCTION", None)
return function_name is not None and callable(getattr(node, function_name, None))
def node_not_executable_reason(class_def, class_type): 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. """Return a human-readable reason the node cannot be executed, or None if it's fine.
@ -1126,21 +1120,22 @@ def node_not_executable_reason(class_def, class_type):
(e.g. a V1 ``FUNCTION = "invert"`` where the method is misspelled, or a V3 node (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 missing its ``execute`` override). Running this during validation surfaces the
problem before execution starts, instead of after upstream nodes have run. 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: try:
if issubclass(class_def, _ComfyNodeInternal): if issubclass(class_def, _ComfyNodeInternal):
# V3: validates that execute()/define_schema() overrides exist. # V3: validates that execute()/define_schema() overrides exist.
class_def.VALIDATE_CLASS() class_def.VALIDATE_CLASS()
return None return None
# V1: FUNCTION names the method to call. Check the class first (the common # V1: FUNCTION names the method to call; it must exist on the class.
# case); fall back to an instance, since the node is invoked on an instance
# and may define FUNCTION or its method in __init__.
if _v1_function_resolves(class_def) or _v1_function_resolves(class_def()):
return None
function_name = getattr(class_def, "FUNCTION", None) function_name = getattr(class_def, "FUNCTION", None)
if function_name is None: if function_name is None:
return f"'{class_type}' does not define FUNCTION" return f"'{class_type}' does not define FUNCTION"
return f"'{class_type}' has no method '{function_name}' (declared in 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: except Exception as ex:
return str(ex) return str(ex)

View File

@ -39,8 +39,8 @@ class _TypoV1Node:
return (None,) return (None,)
class _InstanceMethodV1Node: class _SideEffectInitV1Node:
"""Defines its FUNCTION method on the instance, as the runtime resolves it.""" """Valid class-level method, but a constructor that must never run in validation."""
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return {"required": {}} return {"required": {}}
@ -51,7 +51,10 @@ class _InstanceMethodV1Node:
CATEGORY = "Test" CATEGORY = "Test"
def __init__(self): def __init__(self):
self.run = lambda: (None,) raise RuntimeError("__init__ must not run during validation")
def run(self):
return (None,)
def _v3_schema(node_id): def _v3_schema(node_id):
@ -111,11 +114,11 @@ def test_typo_node_rejected_with_node_error():
assert "invert" in node_errors["1"]["errors"][0]["details"] assert "invert" in node_errors["1"]["errors"][0]["details"]
def test_instance_defined_method_not_false_positived(): def test_validation_does_not_instantiate_node():
"""A node whose method is defined in __init__ runs fine and must not be blocked.""" """A valid node is not constructed during validation, so __init__ never runs."""
_register("InstanceMethodV1Node", _InstanceMethodV1Node) _register("SideEffectInitV1Node", _SideEffectInitV1Node)
assert node_not_executable_reason(_InstanceMethodV1Node, "InstanceMethodV1Node") is None assert node_not_executable_reason(_SideEffectInitV1Node, "SideEffectInitV1Node") is None
valid, _, _, _ = _validate("InstanceMethodV1Node") valid, _, _, _ = _validate("SideEffectInitV1Node")
assert valid is True assert valid is True