diff --git a/comfy_api/v3/io.py b/comfy_api/v3/io.py index 0ab736891..41fa13dea 100644 --- a/comfy_api/v3/io.py +++ b/comfy_api/v3/io.py @@ -532,6 +532,30 @@ class SchemaV3: # """Flags a node as an API node. See: https://docs.comfy.org/tutorials/api-nodes/overview.""" +class Serializer: + def __init_subclass__(cls, io_type: IO | str, **kwargs): + cls.io_type = io_type + super().__init_subclass__(**kwargs) + + @classmethod + def serialize(cls, o: Any) -> str: + pass + + @classmethod + def deserialize(cls, s: str) -> Any: + pass + + +def prepare_class_clone(c: ComfyNodeV3 | type[ComfyNodeV3]) -> type[ComfyNodeV3]: + """Creates clone of real node class to prevent monkey-patching.""" + c_type: type[ComfyNodeV3] = c if is_class(c) else type(c) + type_clone: type[ComfyNodeV3] = type(f"CLEAN_{c_type.__name__}", c_type.__bases__, {}) + # TODO: what parameters should be carried over? + type_clone.SCHEMA = c_type.SCHEMA + # TODO: add anything we would want to expose inside node's EXECUTE function + return type_clone + + class classproperty(object): def __init__(self, f): self.f = f @@ -543,6 +567,43 @@ class ComfyNodeV3(ABC): """Common base class for all V3 nodes.""" RELATIVE_PYTHON_MODULE = None + SCHEMA = 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: + """ + Override this function with one that returns a SchemaV3 instance. + """ + return None + DEFINE_SCHEMA = None + + @classmethod + @abstractmethod + def EXECUTE(cls, **kwargs) -> NodeOutput: + pass + EXECUTE = None + + @classmethod + def GET_SERIALIZERS(cls) -> list[Serializer]: + return [] + + def __init__(self): + self.__class__.VALIDATE_CLASS() + + @classmethod + def VALIDATE_CLASS(cls): + if not callable(cls.DEFINE_SCHEMA): + raise Exception(f"No DEFINE_SCHEMA function was defined for node class {cls.__name__}.") + if not callable(cls.EXECUTE): + raise Exception(f"No execute function was defined for node class {cls.__name__}.") + ############################################# # V1 Backwards Compatibility code #-------------------------------------------- @@ -623,7 +684,7 @@ class ComfyNodeV3(ABC): cls.GET_SCHEMA() return cls._OUTPUT_TOOLTIPS - FUNCTION = "execute" + FUNCTION = "EXECUTE" @classmethod def INPUT_TYPES(cls) -> dict[str, dict]: @@ -642,6 +703,7 @@ class ComfyNodeV3(ABC): @classmethod def GET_SCHEMA(cls) -> SchemaV3: + cls.VALIDATE_CLASS() schema = cls.DEFINE_SCHEMA() if cls._DESCRIPTION is None: cls._DESCRIPTION = schema.description @@ -674,7 +736,7 @@ class ComfyNodeV3(ABC): cls._RETURN_NAMES = output_name cls._OUTPUT_IS_LIST = output_is_list cls._OUTPUT_TOOLTIPS = output_tooltips - + cls.SCHEMA = schema return schema @classmethod @@ -716,31 +778,6 @@ class ComfyNodeV3(ABC): #-------------------------------------------- ############################################# - @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: - """ - Override this function with one that returns a SchemaV3 instance. - """ - return None - DEFINE_SCHEMA = None - - def __init__(self): - if self.DEFINE_SCHEMA is None: - raise Exception("No DEFINE_SCHEMA function was defined for this node.") - - @abstractmethod - def execute(self, **kwargs) -> NodeOutput: - pass - - # class ReturnedInputs: # def __init__(self): # pass @@ -857,19 +894,20 @@ class TestNode(ComfyNodeV3): def DEFINE_SCHEMA(cls): return cls.SCHEMA - def execute(**kwargs): + def EXECUTE(**kwargs): pass if __name__ == "__main__": print("hello there") inputs: list[InputV3] = [ + IntegerInput("tessfes", widgetType=IO.STRING), IntegerInput("my_int"), CustomInput("xyz", "XYZ"), CustomInput("model1", "MODEL_M"), ImageInput("my_image"), FloatInput("my_float"), - MultitypedInput("my_inputs", [CustomType("MODEL_M"), CustomType("XYZ")]), + MultitypedInput("my_inputs", [StringInput, CustomType("MODEL_M"), CustomType("XYZ")]), ] outputs: list[OutputV3] = [ diff --git a/comfy_extras/nodes_v3_test.py b/comfy_extras/nodes_v3_test.py index 0060c8790..5c514d42a 100644 --- a/comfy_extras/nodes_v3_test.py +++ b/comfy_extras/nodes_v3_test.py @@ -1,13 +1,18 @@ import torch from comfy_api.v3.io import ( ComfyNodeV3, SchemaV3, InputBehavior, NumberDisplay, - IntegerInput, MaskInput, ImageInput, ComboInput, CustomInput, - IntegerOutput, ImageOutput, - NodeOutput, + IntegerInput, MaskInput, ImageInput, ComboInput, CustomInput, StringInput, CustomType, + IntegerOutput, ImageOutput, MultitypedInput, + NodeOutput, Hidden ) +import logging class V3TestNode(ComfyNodeV3): + + def __init__(self): + self.hahajkunless = ";)" + @classmethod def DEFINE_SCHEMA(cls): return SchemaV3( @@ -17,7 +22,7 @@ class V3TestNode(ComfyNodeV3): category="v3 nodes", inputs=[ ImageInput("image", display_name="new_image"), - CustomInput("xyz", "XYZ"), + CustomInput("xyz", "XYZ", behavior=InputBehavior.optional), MaskInput("mask", behavior=InputBehavior.optional), IntegerInput("some_int", display_name="new_name", min=0, max=127, default=42, tooltip="My tooltip 😎", display_mode=NumberDisplay.slider), @@ -42,11 +47,20 @@ class V3TestNode(ComfyNodeV3): outputs=[ IntegerOutput("int_output"), ImageOutput("img_output", display_name="img🖼️", tooltip="This is an image"), + ], + hidden=[ + ], is_output_node=True, ) - def execute(self, image: torch.Tensor, xyz, some_int: int, combo: str, mask: torch.Tensor=None): + @classmethod + def EXECUTE(cls, image: torch.Tensor, some_int: int, combo: str, xyz=None, mask: torch.Tensor=None): + if hasattr(cls, "hahajkunless"): + raise Exception("The 'cls' variable leaked instance state between runs!") + if hasattr(cls, "doohickey"): + raise Exception("The 'cls' variable leaked state on class properties between runs!") + cls.doohickey = "LOLJK" return NodeOutput(some_int, image) diff --git a/execution.py b/execution.py index 64707c28a..bbba08952 100644 --- a/execution.py +++ b/execution.py @@ -17,7 +17,7 @@ from comfy_execution.graph import get_input_info, ExecutionList, DynamicPrompt, from comfy_execution.graph_utils import is_link, GraphBuilder from comfy_execution.caching import HierarchicalCache, LRUCache, DependencyAwareCache, CacheKeySetInputSignature, CacheKeySetID from comfy_execution.validation import validate_node_input -from comfy_api.v3.io import NodeOutput +from comfy_api.v3.io import NodeOutput, ComfyNodeV3, prepare_class_clone class ExecutionResult(Enum): SUCCESS = 0 @@ -183,7 +183,14 @@ def _map_node_over_list(obj, input_data_all, func, allow_interrupt=False, execut if execution_block is None: if pre_execute_cb is not None and index is not None: pre_execute_cb(index) - results.append(getattr(obj, func)(**inputs)) + # V3 + if isinstance(obj, ComfyNodeV3): + type(obj).VALIDATE_CLASS() + class_clone = prepare_class_clone(obj) + results.append(type(obj).EXECUTE.__func__(class_clone, **inputs)) + # V1 + else: + results.append(getattr(obj, func)(**inputs)) else: results.append(execution_block)