diff --git a/execution.py b/execution.py index ca8a02846..00e4c2aae 100644 --- a/execution.py +++ b/execution.py @@ -1113,12 +1113,6 @@ def full_type_name(klass): return 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): """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 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. Check the class first (the common - # 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 + # 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" - 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: return str(ex) diff --git a/tests-unit/execution_test/validate_node_executable_test.py b/tests-unit/execution_test/validate_node_executable_test.py index 7c3203ccb..694163b58 100644 --- a/tests-unit/execution_test/validate_node_executable_test.py +++ b/tests-unit/execution_test/validate_node_executable_test.py @@ -39,8 +39,8 @@ class _TypoV1Node: return (None,) -class _InstanceMethodV1Node: - """Defines its FUNCTION method on the instance, as the runtime resolves it.""" +class _SideEffectInitV1Node: + """Valid class-level method, but a constructor that must never run in validation.""" @classmethod def INPUT_TYPES(cls): return {"required": {}} @@ -51,7 +51,10 @@ class _InstanceMethodV1Node: CATEGORY = "Test" 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): @@ -111,11 +114,11 @@ def test_typo_node_rejected_with_node_error(): assert "invert" in node_errors["1"]["errors"][0]["details"] -def test_instance_defined_method_not_false_positived(): - """A node whose method is defined in __init__ runs fine and must not be blocked.""" - _register("InstanceMethodV1Node", _InstanceMethodV1Node) - assert node_not_executable_reason(_InstanceMethodV1Node, "InstanceMethodV1Node") is None - valid, _, _, _ = _validate("InstanceMethodV1Node") +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