diff --git a/comfy_api/internal/__init__.py b/comfy_api/internal/__init__.py index 6b0187529..afe4a88d7 100644 --- a/comfy_api/internal/__init__.py +++ b/comfy_api/internal/__init__.py @@ -1,10 +1,115 @@ from abc import ABC, abstractmethod +from dataclasses import asdict +from typing import Callable, Optional -class ComfyNodeInternal(ABC): + +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 + base_func = base_attr.__func__ + for c in cls.mro(): # NodeB, NodeA, ComfyNodeV3, object … + if c is base: # reached the placeholder – we're done + break + if name in c.__dict__: # first class that *defines* the attr + func = getattr(c, name).__func__ + if func is not base_func: # real override + return getattr(cls, name) # bound to *cls* + return None + + +class ComfyNodeInternal: """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): ... + + +def as_pruned_dict(dataclass_obj): + '''Return dict of dataclass object with pruned None values.''' + return prune_dict(asdict(dataclass_obj)) + +def prune_dict(d: dict): + return {k: v for k,v in d.items() if v is not None} + + +def is_class(obj): + ''' + Returns True if is a class type. + Returns False if is a class instance. + ''' + return isinstance(obj, type) + + +def copy_class(cls: type) -> type: + ''' + Copy a class and its attributes. + ''' + if cls is None: + return None + cls_dict = { + k: v for k, v in cls.__dict__.items() + if k not in ('__dict__', '__weakref__', '__module__', '__doc__') + } + # new class + new_cls = type( + cls.__name__, + (cls,), + cls_dict + ) + # metadata preservation + new_cls.__module__ = cls.__module__ + new_cls.__doc__ = cls.__doc__ + return new_cls + + +class classproperty(object): + def __init__(self, f): + self.f = f + def __get__(self, obj, owner): + return self.f(owner) + + +# NOTE: this was ai generated and validated by hand +def shallow_clone_class(cls, new_name=None): + ''' + Shallow clone a class. + ''' + return type( + new_name or f"{cls.__name__}Clone", + cls.__bases__, + dict(cls.__dict__) + ) + +# NOTE: this was ai generated and validated by hand +def lock_class(cls): + ''' + Lock a class so that its top-levelattributes cannot be modified. + ''' + # Locked instance __setattr__ + def locked_instance_setattr(self, name, value): + raise AttributeError( + f"Cannot set attribute '{name}' on immutable instance of {type(self).__name__}" + ) + # Locked metaclass + class LockedMeta(type(cls)): + def __setattr__(cls_, name, value): + raise AttributeError( + f"Cannot modify class attribute '{name}' on locked class '{cls_.__name__}'" + ) + # Rebuild class with locked behavior + locked_dict = dict(cls.__dict__) + locked_dict['__setattr__'] = locked_instance_setattr + + return LockedMeta(cls.__name__, cls.__bases__, locked_dict) diff --git a/comfy_api/v3/helpers.py b/comfy_api/v3/helpers.py deleted file mode 100644 index 810107ac4..000000000 --- a/comfy_api/v3/helpers.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Callable, Optional - - -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 - base_func = base_attr.__func__ - for c in cls.mro(): # NodeB, NodeA, ComfyNodeV3, object … - if c is base: # reached the placeholder – we're done - break - if name in c.__dict__: # first class that *defines* the attr - func = getattr(c, name).__func__ - if func is not base_func: # real override - return getattr(cls, name) # bound to *cls* - return None diff --git a/comfy_api/v3/io.py b/comfy_api/v3/io.py index f81c12df0..c15d77542 100644 --- a/comfy_api/v3/io.py +++ b/comfy_api/v3/io.py @@ -6,7 +6,6 @@ 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 @@ -22,7 +21,8 @@ from comfy.samplers import CFGGuider, Sampler from comfy.sd import CLIP, VAE from comfy.sd import StyleModel as StyleModel_ from comfy_api.input import VideoInput -from comfy_api.internal import ComfyNodeInternal +from comfy_api.internal import (ComfyNodeInternal, classproperty, copy_class, first_real_override, is_class, + prune_dict, shallow_clone_class) from comfy_api.v3.resources import Resources, ResourcesLocal from comfy_execution.graph import ExecutionBlocker @@ -69,36 +69,6 @@ class RemoteOptions: }) -def is_class(obj): - ''' - Returns True if is a class type. - Returns False if is a class instance. - ''' - return isinstance(obj, type) - - -def copy_class(cls: type) -> type: - ''' - Copy a class and its attributes. - ''' - if cls is None: - return None - cls_dict = { - k: v for k, v in cls.__dict__.items() - if k not in ('__dict__', '__weakref__', '__module__', '__doc__') - } - # new class - new_cls = type( - cls.__name__, - (cls,), - cls_dict - ) - # metadata preservation - new_cls.__module__ = cls.__module__ - new_cls.__doc__ = cls.__doc__ - return new_cls - - class NumberDisplay(str, Enum): number = "number" slider = "slider" @@ -999,13 +969,6 @@ class NodeInfoV3: experimental: bool=None api_node: bool=None -def as_pruned_dict(dataclass_obj): - '''Return dict of dataclass object with pruned None values.''' - return prune_dict(asdict(dataclass_obj)) - -def prune_dict(d: dict): - return {k: v for k,v in d.items() if v is not None} - @dataclass class SchemaV3: @@ -1179,60 +1142,6 @@ class SchemaV3: ) return info -class Serializer: - def __init_subclass__(cls, io_type: 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 - - -class classproperty(object): - def __init__(self, f): - self.f = f - def __get__(self, obj, owner): - return self.f(owner) - - -# NOTE: this was ai generated and validated by hand -def shallow_clone_class(cls, new_name=None): - ''' - Shallow clone a class. - ''' - return type( - new_name or f"{cls.__name__}Clone", - cls.__bases__, - dict(cls.__dict__) - ) - -# NOTE: this was ai generated and validated by hand -def lock_class(cls): - ''' - Lock a class so that its top-levelattributes cannot be modified. - ''' - # Locked instance __setattr__ - def locked_instance_setattr(self, name, value): - raise AttributeError( - f"Cannot set attribute '{name}' on immutable instance of {type(self).__name__}" - ) - # Locked metaclass - class LockedMeta(type(cls)): - def __setattr__(cls_, name, value): - raise AttributeError( - f"Cannot modify class attribute '{name}' on locked class '{cls_.__name__}'" - ) - # Rebuild class with locked behavior - locked_dict = dict(cls.__dict__) - locked_dict['__setattr__'] = locked_instance_setattr - - return LockedMeta(cls.__name__, cls.__bases__, locked_dict) - def add_to_dict_v1(i: InputV3, input: dict): key = "optional" if i.optional else "required" @@ -1300,10 +1209,6 @@ class _ComfyNodeBaseInternal(ComfyNodeInternal): 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 diff --git a/execution.py b/execution.py index 30dc20d48..666a094b8 100644 --- a/execution.py +++ b/execution.py @@ -28,8 +28,8 @@ 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 +from comfy_api.internal import ComfyNodeInternal, lock_class, first_real_override +from comfy_api.v3 import io class ExecutionResult(Enum): @@ -55,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, ComfyNodeInternal) and helpers.first_real_override(class_def, "fingerprint_inputs") is not None: + if issubclass(class_def, ComfyNodeInternal) and 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"): @@ -256,7 +256,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__(io.lock_class(class_clone), **inputs)) + results.append(getattr(type_obj, func).__func__(lock_class(class_clone), **inputs)) # V1 else: results.append(getattr(obj, func)(**inputs)) @@ -413,7 +413,7 @@ def execute(server, dynprompt, caches, current_item, extra_data, executed, promp caches.objects.set(unique_id, obj) if issubclass(class_def, ComfyNodeInternal): - lazy_status_present = helpers.first_real_override(class_def, "check_lazy_status") is not None + lazy_status_present = 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: @@ -677,7 +677,7 @@ def validate_inputs(prompt, item, validated): validate_has_kwargs = False if issubclass(obj_class, ComfyNodeInternal): validate_function_name = "validate_inputs" - validate_function = helpers.first_real_override(obj_class, validate_function_name) + validate_function = first_real_override(obj_class, validate_function_name) else: validate_function_name = "VALIDATE_INPUTS" validate_function = getattr(obj_class, validate_function_name, None)