diff --git a/comfy_api/v3/io.py b/comfy_api/v3/io.py index 88eabd3fe..58ce0bfc8 100644 --- a/comfy_api/v3/io.py +++ b/comfy_api/v3/io.py @@ -1025,12 +1025,6 @@ class ComfyNodeV3: resources: Resources = None hidden: HiddenHolder = None - @classmethod - def GET_NODE_INFO_V3(cls) -> dict[str, Any]: - schema = cls.GET_SCHEMA() - # TODO: finish - return None - @classmethod @abstractmethod def DEFINE_SCHEMA(cls) -> SchemaV3: @@ -1046,10 +1040,46 @@ class ComfyNodeV3: pass execute = None + @classmethod + def validate_inputs(cls, **kwargs) -> bool: + """Optionally, define this function to validate inputs; equivalnet to V1's VALIDATE_INPUTS.""" + pass + validate_inputs = None + + @classmethod + def fingerprint_inputs(cls, **kwargs) -> Any: + """Optionally, define this function to fingerprint inputs; equivalent to V1's IS_CHANGED.""" + pass + fingerprint_inputs = None + + @classmethod + def check_lazy_status(cls, **kwargs) -> list[str]: + """Optionally, define this function to return a list of input names that should be evaluated. + + This basic mixin impl. requires all inputs. + + :kwargs: All node inputs will be included here. If the input is ``None``, it should be assumed that it has not yet been evaluated. \ + When using ``INPUT_IS_LIST = True``, unevaluated will instead be ``(None,)``. + + Params should match the nodes execution ``FUNCTION`` (self, and all inputs by name). + Will be executed repeatedly until it returns an empty list, or all requested items were already evaluated (and sent as params). + + Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lazy_evaluation#defining-check-lazy-status + """ + need = [name for name in kwargs if kwargs[name] is None] + return need + check_lazy_status = None + @classmethod def GET_SERIALIZERS(cls) -> list[Serializer]: return [] + @classmethod + def GET_NODE_INFO_V3(cls) -> dict[str, Any]: + schema = cls.GET_SCHEMA() + # TODO: finish + return None + def __init__(self): self.local_state: NodeStateLocal = None self.local_resources: ResourcesLocal = None diff --git a/comfy_extras/nodes_v3_test.py b/comfy_extras/nodes_v3_test.py index 01fbfa2ba..a73516ae0 100644 --- a/comfy_extras/nodes_v3_test.py +++ b/comfy_extras/nodes_v3_test.py @@ -73,7 +73,7 @@ class V3TestNode(io.ComfyNodeV3): ) @classmethod - def VALIDATE_INPUTS(cls, image: io.Image.Type, some_int: int, combo: io.Combo.Type, combo2: io.MultiCombo.Type, xyz: XYZ.Type=None, mask: io.Mask.Type=None, **kwargs): + def validate_inputs(cls, image: io.Image.Type, some_int: int, combo: io.Combo.Type, combo2: io.MultiCombo.Type, xyz: XYZ.Type=None, mask: io.Mask.Type=None, **kwargs): if some_int < 0: raise Exception("some_int must be greater than 0") if combo == "c": @@ -172,6 +172,15 @@ class NInputsTest(io.ComfyNodeV3): ], ) + @classmethod + def validate_inputs(cls, nmock, nmock2): + return True + + @classmethod + def check_lazy_status(cls, **kwargs) -> list[str]: + need = [name for name in kwargs if kwargs[name] is None] + return need + @classmethod def execute(cls, nmock, nmock2): first_image = nmock[0] diff --git a/execution.py b/execution.py index 3e58b53ef..d7e46e5b2 100644 --- a/execution.py +++ b/execution.py @@ -28,7 +28,7 @@ from comfy_execution.graph import ( ) from comfy_execution.graph_utils import GraphBuilder, is_link from comfy_execution.validation import validate_node_input -from comfy_api.v3.io import NodeOutput, ComfyNodeV3, Hidden, NodeStateLocal, ResourcesLocal, AutogrowDynamic +from comfy_api.v3.io import NodeOutput, ComfyNodeV3, Hidden, NodeStateLocal, ResourcesLocal, AutogrowDynamic, is_class class ExecutionResult(Enum): @@ -216,19 +216,27 @@ def _map_node_over_list(obj, input_data_all, func, allow_interrupt=False, execut if pre_execute_cb is not None and index is not None: pre_execute_cb(index) # V3 - if isinstance(obj, ComfyNodeV3): - type(obj).VALIDATE_CLASS() - class_clone = type(obj).prepare_class_clone(hidden_inputs) - # NOTE: this is a mock of state management; for local, just stores NodeStateLocal on node instance - if hasattr(obj, "local_state"): - if obj.local_state is None: - obj.local_state = NodeStateLocal(class_clone.hidden.unique_id) - class_clone.state = obj.local_state - # NOTE: this is a mock of resource management; for local, just stores ResourcesLocal on node instance - if hasattr(obj, "local_resources"): - if obj.local_resources is None: - obj.local_resources = ResourcesLocal() - class_clone.resources = obj.local_resources + if isinstance(obj, ComfyNodeV3) or (is_class(obj) and issubclass(obj, ComfyNodeV3)): + # if is just a class, then assign no resources or state, just create clone + if is_class(obj): + type_obj = obj + obj.VALIDATE_CLASS() + class_clone = obj.prepare_class_clone(hidden_inputs) + # otherwise, use class instance to populate/reuse some fields + else: + type_obj = type(obj) + type(obj).VALIDATE_CLASS() + class_clone = type(obj).prepare_class_clone(hidden_inputs) + # NOTE: this is a mock of state management; for local, just stores NodeStateLocal on node instance + if hasattr(obj, "local_state"): + if obj.local_state is None: + obj.local_state = NodeStateLocal(class_clone.hidden.unique_id) + class_clone.state = obj.local_state + # NOTE: this is a mock of resource management; for local, just stores ResourcesLocal on node instance + if hasattr(obj, "local_resources"): + if obj.local_resources is None: + obj.local_resources = ResourcesLocal() + class_clone.resources = obj.local_resources # TODO: delete this when done testing mocking dynamic inputs for si in obj.SCHEMA.inputs: if isinstance(si, AutogrowDynamic.Input): @@ -239,7 +247,7 @@ def _map_node_over_list(obj, input_data_all, func, allow_interrupt=False, execut dynamic_list.append(real_inputs.pop(d.id, None)) dynamic_list = [x for x in dynamic_list if x is not None] inputs = {**real_inputs, add_key: dynamic_list} - results.append(getattr(type(obj), func).__func__(class_clone, **inputs)) + results.append(getattr(type_obj, func).__func__(class_clone, **inputs)) # V1 else: results.append(getattr(obj, func)(**inputs)) @@ -392,7 +400,7 @@ def execute(server, dynprompt, caches, current_item, extra_data, executed, promp obj = class_def() caches.objects.set(unique_id, obj) - if hasattr(obj, "check_lazy_status"): + if getattr(obj, "check_lazy_status", None) is not None: required_inputs = _map_node_over_list(obj, input_data_all, "check_lazy_status", allow_interrupt=True, hidden_inputs=hidden_inputs) required_inputs = set(sum([r for r in required_inputs if isinstance(r,list)], [])) required_inputs = [x for x in required_inputs if isinstance(x,str) and ( @@ -651,8 +659,16 @@ def validate_inputs(prompt, item, validated): validate_function_inputs = [] validate_has_kwargs = False - if hasattr(obj_class, "VALIDATE_INPUTS"): - argspec = inspect.getfullargspec(obj_class.VALIDATE_INPUTS) + validate_function_name = None + validate_function = None + if issubclass(obj_class, ComfyNodeV3): + validate_function_name = "validate_inputs" + validate_function = getattr(obj_class, validate_function_name, None) + else: + validate_function_name = "VALIDATE_INPUTS" + validate_function = getattr(obj_class, validate_function_name, None) + if validate_function is not None: + argspec = inspect.getfullargspec(validate_function) validate_function_inputs = argspec.args validate_has_kwargs = argspec.varkw is not None received_types = {} @@ -835,8 +851,7 @@ def validate_inputs(prompt, item, validated): if 'input_types' in validate_function_inputs: input_filtered['input_types'] = [received_types] - #ret = obj_class.VALIDATE_INPUTS(**input_filtered) - ret = _map_node_over_list(obj_class, input_filtered, "VALIDATE_INPUTS", hidden_inputs=hidden_inputs) + ret = _map_node_over_list(obj_class, input_filtered, validate_function_name, hidden_inputs=hidden_inputs) for x in input_filtered: for i, r in enumerate(ret): if r is not True and not isinstance(r, ExecutionBlocker):