mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-07-27 08:16:44 +00:00
Separate ComfyNodeV3 into an internal base class and one that only has the functions defined that a developer cares about overriding, reference ComfyNodeInternal in execution.py/server.py instead of ComfyNodeV3 to make the code not bound to a particular version of v3 schema (once placed on api)
This commit is contained in:
parent
b99e3d1336
commit
ab98b65226
@ -1,6 +1,10 @@
|
||||
class ComfyNodeInternal:
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class ComfyNodeInternal(ABC):
|
||||
"""Class that all V3-based APIs inherit from for ComfyNode.
|
||||
|
||||
This is intended to only be referenced within execution.py, as it has to handle all V3 APIs going forward."""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def GET_NODE_INFO_V1(cls):
|
||||
...
|
||||
|
@ -1,10 +1,16 @@
|
||||
from typing import Callable, Optional
|
||||
|
||||
|
||||
def first_real_override(cls: type, name: str, *, base: type) -> Optional[Callable]:
|
||||
def first_real_override(cls: type, name: str, *, base: type=None) -> Optional[Callable]:
|
||||
"""Return the *callable* override of `name` visible on `cls`, or None if every
|
||||
implementation up to (and including) `base` is the placeholder defined on `base`.
|
||||
|
||||
If base is not provided, it will assume cls has a GET_BASE_CLASS
|
||||
"""
|
||||
if base is None:
|
||||
if not hasattr(cls, "GET_BASE_CLASS"):
|
||||
raise ValueError("base is required if cls does not have a GET_BASE_CLASS; is this a valid ComfyNode subclass?")
|
||||
base = cls.GET_BASE_CLASS()
|
||||
base_attr = getattr(base, name, None)
|
||||
if base_attr is None:
|
||||
return None
|
||||
|
@ -6,11 +6,12 @@ from collections import Counter
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Literal, TypedDict, TypeVar
|
||||
from comfy_api.v3.helpers import first_real_override
|
||||
|
||||
# used for type hinting
|
||||
import torch
|
||||
from spandrel import ImageModelDescriptor
|
||||
from typing_extensions import NotRequired
|
||||
from typing_extensions import NotRequired, final
|
||||
|
||||
from comfy.clip_vision import ClipVisionModel
|
||||
from comfy.clip_vision import Output as ClipVisionOutput_
|
||||
@ -1244,8 +1245,9 @@ def add_to_dict_v3(io: InputV3 | OutputV3, d: dict):
|
||||
d[io.id] = (io.get_io_type(), io.as_dict())
|
||||
|
||||
|
||||
class ComfyNodeV3(ComfyNodeInternal):
|
||||
"""Common base class for all V3 nodes."""
|
||||
|
||||
class _ComfyNodeBaseInternal(ComfyNodeInternal):
|
||||
"""Common base class for storing internal methods and properties; DO NOT USE for defining nodes."""
|
||||
|
||||
RELATIVE_PYTHON_MODULE = None
|
||||
SCHEMA = None
|
||||
@ -1264,6 +1266,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def execute(cls, **kwargs) -> NodeOutput:
|
||||
"""Override this function with one that performs node's actions."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
@ -1292,28 +1295,28 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
"""
|
||||
return [name for name in kwargs if kwargs[name] is None]
|
||||
|
||||
@classmethod
|
||||
def GET_SERIALIZERS(cls) -> list[Serializer]:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def GET_NODE_INFO_V3(cls) -> dict[str, Any]:
|
||||
schema = cls.GET_SCHEMA()
|
||||
info = schema.get_v3_info(cls)
|
||||
return asdict(info)
|
||||
|
||||
def __init__(self):
|
||||
self.local_state: NodeStateLocal = None
|
||||
self.local_resources: ResourcesLocal = None
|
||||
self.__class__.VALIDATE_CLASS()
|
||||
|
||||
@classmethod
|
||||
def GET_SERIALIZERS(cls) -> list[Serializer]:
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def GET_BASE_CLASS(cls):
|
||||
return _ComfyNodeBaseInternal
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def VALIDATE_CLASS(cls):
|
||||
if not callable(cls.define_schema):
|
||||
if first_real_override(cls, "define_schema") is None:
|
||||
raise Exception(f"No define_schema function was defined for node class {cls.__name__}.")
|
||||
if not callable(cls.execute):
|
||||
if first_real_override(cls, "execute") is None:
|
||||
raise Exception(f"No execute function was defined for node class {cls.__name__}.")
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def EXECUTE_NORMALIZED(cls, *args, **kwargs) -> NodeOutput:
|
||||
to_return = cls.execute(*args, **kwargs)
|
||||
@ -1330,6 +1333,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
else:
|
||||
raise Exception(f"Invalid return type from node: {type(to_return)}")
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def PREPARE_CLASS_CLONE(cls, hidden_inputs: dict) -> type[ComfyNodeV3]:
|
||||
"""Creates clone of real node class to prevent monkey-patching."""
|
||||
@ -1339,10 +1343,24 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
type_clone.hidden = HiddenHolder.from_dict(hidden_inputs)
|
||||
return type_clone
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def GET_NODE_INFO_V3(cls) -> dict[str, Any]:
|
||||
schema = cls.GET_SCHEMA()
|
||||
info = schema.get_v3_info(cls)
|
||||
return asdict(info)
|
||||
#############################################
|
||||
# V1 Backwards Compatibility code
|
||||
#--------------------------------------------
|
||||
@final
|
||||
@classmethod
|
||||
def GET_NODE_INFO_V1(cls) -> dict[str, Any]:
|
||||
schema = cls.GET_SCHEMA()
|
||||
info = schema.get_v1_info(cls)
|
||||
return asdict(info)
|
||||
|
||||
_DESCRIPTION = None
|
||||
@final
|
||||
@classproperty
|
||||
def DESCRIPTION(cls): # noqa
|
||||
if cls._DESCRIPTION is None:
|
||||
@ -1350,6 +1368,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._DESCRIPTION
|
||||
|
||||
_CATEGORY = None
|
||||
@final
|
||||
@classproperty
|
||||
def CATEGORY(cls): # noqa
|
||||
if cls._CATEGORY is None:
|
||||
@ -1357,6 +1376,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._CATEGORY
|
||||
|
||||
_EXPERIMENTAL = None
|
||||
@final
|
||||
@classproperty
|
||||
def EXPERIMENTAL(cls): # noqa
|
||||
if cls._EXPERIMENTAL is None:
|
||||
@ -1364,6 +1384,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._EXPERIMENTAL
|
||||
|
||||
_DEPRECATED = None
|
||||
@final
|
||||
@classproperty
|
||||
def DEPRECATED(cls): # noqa
|
||||
if cls._DEPRECATED is None:
|
||||
@ -1371,6 +1392,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._DEPRECATED
|
||||
|
||||
_API_NODE = None
|
||||
@final
|
||||
@classproperty
|
||||
def API_NODE(cls): # noqa
|
||||
if cls._API_NODE is None:
|
||||
@ -1378,6 +1400,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._API_NODE
|
||||
|
||||
_OUTPUT_NODE = None
|
||||
@final
|
||||
@classproperty
|
||||
def OUTPUT_NODE(cls): # noqa
|
||||
if cls._OUTPUT_NODE is None:
|
||||
@ -1385,6 +1408,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._OUTPUT_NODE
|
||||
|
||||
_INPUT_IS_LIST = None
|
||||
@final
|
||||
@classproperty
|
||||
def INPUT_IS_LIST(cls): # noqa
|
||||
if cls._INPUT_IS_LIST is None:
|
||||
@ -1392,6 +1416,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._INPUT_IS_LIST
|
||||
_OUTPUT_IS_LIST = None
|
||||
|
||||
@final
|
||||
@classproperty
|
||||
def OUTPUT_IS_LIST(cls): # noqa
|
||||
if cls._OUTPUT_IS_LIST is None:
|
||||
@ -1399,6 +1424,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._OUTPUT_IS_LIST
|
||||
|
||||
_RETURN_TYPES = None
|
||||
@final
|
||||
@classproperty
|
||||
def RETURN_TYPES(cls): # noqa
|
||||
if cls._RETURN_TYPES is None:
|
||||
@ -1406,6 +1432,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._RETURN_TYPES
|
||||
|
||||
_RETURN_NAMES = None
|
||||
@final
|
||||
@classproperty
|
||||
def RETURN_NAMES(cls): # noqa
|
||||
if cls._RETURN_NAMES is None:
|
||||
@ -1413,6 +1440,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._RETURN_NAMES
|
||||
|
||||
_OUTPUT_TOOLTIPS = None
|
||||
@final
|
||||
@classproperty
|
||||
def OUTPUT_TOOLTIPS(cls): # noqa
|
||||
if cls._OUTPUT_TOOLTIPS is None:
|
||||
@ -1420,6 +1448,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return cls._OUTPUT_TOOLTIPS
|
||||
|
||||
_NOT_IDEMPOTENT = None
|
||||
@final
|
||||
@classproperty
|
||||
def NOT_IDEMPOTENT(cls): # noqa
|
||||
if cls._NOT_IDEMPOTENT is None:
|
||||
@ -1428,6 +1457,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
|
||||
FUNCTION = "EXECUTE_NORMALIZED"
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls, include_hidden=True, return_schema=False) -> dict[str, dict] | tuple[dict[str, dict], SchemaV3]:
|
||||
schema = cls.FINALIZE_SCHEMA()
|
||||
@ -1439,6 +1469,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
return input, schema
|
||||
return input
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def FINALIZE_SCHEMA(cls):
|
||||
"""Call define_schema and finalize it."""
|
||||
@ -1446,6 +1477,7 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
schema.finalize()
|
||||
return schema
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def GET_SCHEMA(cls) -> SchemaV3:
|
||||
"""Validate node class, finalize schema, validate schema, and set expected class properties."""
|
||||
@ -1487,16 +1519,58 @@ class ComfyNodeV3(ComfyNodeInternal):
|
||||
cls._OUTPUT_TOOLTIPS = output_tooltips
|
||||
cls.SCHEMA = schema
|
||||
return schema
|
||||
|
||||
@classmethod
|
||||
def GET_NODE_INFO_V1(cls) -> dict[str, Any]:
|
||||
schema = cls.GET_SCHEMA()
|
||||
info = schema.get_v1_info(cls)
|
||||
return asdict(info)
|
||||
#--------------------------------------------
|
||||
#############################################
|
||||
|
||||
|
||||
class ComfyNodeV3(_ComfyNodeBaseInternal):
|
||||
"""Common base class for all V3 nodes."""
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def define_schema(cls) -> SchemaV3:
|
||||
"""Override this function with one that returns a SchemaV3 instance."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def execute(cls, **kwargs) -> NodeOutput:
|
||||
"""Override this function with one that performs node's actions."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def validate_inputs(cls, **kwargs) -> bool:
|
||||
"""Optionally, define this function to validate inputs; equivalent to V1's VALIDATE_INPUTS."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def fingerprint_inputs(cls, **kwargs) -> Any:
|
||||
"""Optionally, define this function to fingerprint inputs; equivalent to V1's IS_CHANGED."""
|
||||
raise NotImplementedError
|
||||
|
||||
@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
|
||||
"""
|
||||
return [name for name in kwargs if kwargs[name] is None]
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def GET_BASE_CLASS(cls):
|
||||
"""DO NOT override this class. Will break things in execution.py."""
|
||||
return ComfyNodeV3
|
||||
|
||||
|
||||
class NodeOutput:
|
||||
'''
|
||||
Standardized output of a node; can pass in any number of args and/or a UIOutput into 'ui' kwarg.
|
||||
|
15
execution.py
15
execution.py
@ -28,6 +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.internal import ComfyNodeInternal
|
||||
from comfy_api.v3 import io, helpers
|
||||
|
||||
|
||||
@ -54,7 +55,7 @@ class IsChangedCache:
|
||||
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
||||
has_is_changed = False
|
||||
is_changed_name = None
|
||||
if issubclass(class_def, io.ComfyNodeV3) and helpers.first_real_override(class_def, "fingerprint_inputs", base=io.ComfyNodeV3) is not None:
|
||||
if issubclass(class_def, ComfyNodeInternal) and helpers.first_real_override(class_def, "fingerprint_inputs") is not None:
|
||||
has_is_changed = True
|
||||
is_changed_name = "fingerprint_inputs"
|
||||
elif hasattr(class_def, "IS_CHANGED"):
|
||||
@ -127,7 +128,7 @@ class CacheSet:
|
||||
return result
|
||||
|
||||
def get_input_data(inputs, class_def, unique_id, outputs=None, dynprompt=None, extra_data={}):
|
||||
is_v3 = issubclass(class_def, io.ComfyNodeV3)
|
||||
is_v3 = issubclass(class_def, ComfyNodeInternal)
|
||||
if is_v3:
|
||||
valid_inputs, schema = class_def.INPUT_TYPES(include_hidden=False, return_schema=True)
|
||||
else:
|
||||
@ -224,7 +225,7 @@ 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, io.ComfyNodeV3) or (io.is_class(obj) and issubclass(obj, io.ComfyNodeV3)):
|
||||
if isinstance(obj, ComfyNodeInternal) or (io.is_class(obj) and issubclass(obj, ComfyNodeInternal)):
|
||||
# if is just a class, then assign no resources or state, just create clone
|
||||
if io.is_class(obj):
|
||||
type_obj = obj
|
||||
@ -411,8 +412,8 @@ def execute(server, dynprompt, caches, current_item, extra_data, executed, promp
|
||||
obj = class_def()
|
||||
caches.objects.set(unique_id, obj)
|
||||
|
||||
if issubclass(class_def, io.ComfyNodeV3):
|
||||
lazy_status_present = helpers.first_real_override(class_def, "check_lazy_status", base=io.ComfyNodeV3) is not None
|
||||
if issubclass(class_def, ComfyNodeInternal):
|
||||
lazy_status_present = helpers.first_real_override(class_def, "check_lazy_status") is not None
|
||||
else:
|
||||
lazy_status_present = getattr(obj, "check_lazy_status", None) is not None
|
||||
if lazy_status_present:
|
||||
@ -674,9 +675,9 @@ def validate_inputs(prompt, item, validated):
|
||||
|
||||
validate_function_inputs = []
|
||||
validate_has_kwargs = False
|
||||
if issubclass(obj_class, io.ComfyNodeV3):
|
||||
if issubclass(obj_class, ComfyNodeInternal):
|
||||
validate_function_name = "validate_inputs"
|
||||
validate_function = helpers.first_real_override(obj_class, validate_function_name, base=io.ComfyNodeV3)
|
||||
validate_function = helpers.first_real_override(obj_class, validate_function_name)
|
||||
else:
|
||||
validate_function_name = "VALIDATE_INPUTS"
|
||||
validate_function = getattr(obj_class, validate_function_name, None)
|
||||
|
@ -29,7 +29,7 @@ import comfy.model_management
|
||||
import node_helpers
|
||||
from comfyui_version import __version__
|
||||
from app.frontend_management import FrontendManager
|
||||
from comfy_api.v3.io import ComfyNodeV3
|
||||
from comfy_api.internal import ComfyNodeInternal
|
||||
|
||||
from app.user_manager import UserManager
|
||||
from app.model_manager import ModelFileManager
|
||||
@ -555,7 +555,7 @@ class PromptServer():
|
||||
|
||||
def node_info(node_class):
|
||||
obj_class = nodes.NODE_CLASS_MAPPINGS[node_class]
|
||||
if issubclass(obj_class, ComfyNodeV3):
|
||||
if issubclass(obj_class, ComfyNodeInternal):
|
||||
return obj_class.GET_NODE_INFO_V1()
|
||||
info = {}
|
||||
info['input'] = obj_class.INPUT_TYPES()
|
||||
|
Loading…
x
Reference in New Issue
Block a user