Moved helper functions into internal.__init__.py instead of in io.helpers.py as the functions will likely stay the same across different revisions of v3, move helper functions out of io.py to clean up the file a bit, remove Serialization class as not needed at the moment, fix ComfyNodeInternal inherting from ABC breaking lock_class function by removing ABC parent; will need better solution later

This commit is contained in:
Jedrzej Kosinski 2025-07-17 17:32:41 -07:00
parent f8b7170103
commit 95289b3952
4 changed files with 115 additions and 130 deletions

View File

@ -1,10 +1,115 @@
from abc import ABC, abstractmethod 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. """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.""" This is intended to only be referenced within execution.py, as it has to handle all V3 APIs going forward."""
@classmethod @classmethod
@abstractmethod
def GET_NODE_INFO_V1(cls): 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)

View File

@ -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

View File

@ -6,7 +6,6 @@ from collections import Counter
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from enum import Enum from enum import Enum
from typing import Any, Callable, Literal, TypedDict, TypeVar from typing import Any, Callable, Literal, TypedDict, TypeVar
from comfy_api.v3.helpers import first_real_override
# used for type hinting # used for type hinting
import torch import torch
@ -22,7 +21,8 @@ from comfy.samplers import CFGGuider, Sampler
from comfy.sd import CLIP, VAE from comfy.sd import CLIP, VAE
from comfy.sd import StyleModel as StyleModel_ from comfy.sd import StyleModel as StyleModel_
from comfy_api.input import VideoInput 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_api.v3.resources import Resources, ResourcesLocal
from comfy_execution.graph import ExecutionBlocker 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): class NumberDisplay(str, Enum):
number = "number" number = "number"
slider = "slider" slider = "slider"
@ -999,13 +969,6 @@ class NodeInfoV3:
experimental: bool=None experimental: bool=None
api_node: 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 @dataclass
class SchemaV3: class SchemaV3:
@ -1179,60 +1142,6 @@ class SchemaV3:
) )
return info 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): def add_to_dict_v1(i: InputV3, input: dict):
key = "optional" if i.optional else "required" key = "optional" if i.optional else "required"
@ -1300,10 +1209,6 @@ class _ComfyNodeBaseInternal(ComfyNodeInternal):
self.local_resources: ResourcesLocal = None self.local_resources: ResourcesLocal = None
self.__class__.VALIDATE_CLASS() self.__class__.VALIDATE_CLASS()
@classmethod
def GET_SERIALIZERS(cls) -> list[Serializer]:
return []
@classmethod @classmethod
def GET_BASE_CLASS(cls): def GET_BASE_CLASS(cls):
return _ComfyNodeBaseInternal return _ComfyNodeBaseInternal

View File

@ -28,8 +28,8 @@ from comfy_execution.graph import (
) )
from comfy_execution.graph_utils import GraphBuilder, is_link from comfy_execution.graph_utils import GraphBuilder, is_link
from comfy_execution.validation import validate_node_input from comfy_execution.validation import validate_node_input
from comfy_api.internal import ComfyNodeInternal from comfy_api.internal import ComfyNodeInternal, lock_class, first_real_override
from comfy_api.v3 import io, helpers from comfy_api.v3 import io
class ExecutionResult(Enum): class ExecutionResult(Enum):
@ -55,7 +55,7 @@ class IsChangedCache:
class_def = nodes.NODE_CLASS_MAPPINGS[class_type] class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
has_is_changed = False has_is_changed = False
is_changed_name = None 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 has_is_changed = True
is_changed_name = "fingerprint_inputs" is_changed_name = "fingerprint_inputs"
elif hasattr(class_def, "IS_CHANGED"): 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.append(real_inputs.pop(d.id, None))
dynamic_list = [x for x in dynamic_list if x is not None] dynamic_list = [x for x in dynamic_list if x is not None]
inputs = {**real_inputs, add_key: dynamic_list} 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 # V1
else: else:
results.append(getattr(obj, func)(**inputs)) 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) caches.objects.set(unique_id, obj)
if issubclass(class_def, ComfyNodeInternal): 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: else:
lazy_status_present = getattr(obj, "check_lazy_status", None) is not None lazy_status_present = getattr(obj, "check_lazy_status", None) is not None
if lazy_status_present: if lazy_status_present:
@ -677,7 +677,7 @@ def validate_inputs(prompt, item, validated):
validate_has_kwargs = False validate_has_kwargs = False
if issubclass(obj_class, ComfyNodeInternal): if issubclass(obj_class, ComfyNodeInternal):
validate_function_name = "validate_inputs" 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: else:
validate_function_name = "VALIDATE_INPUTS" validate_function_name = "VALIDATE_INPUTS"
validate_function = getattr(obj_class, validate_function_name, None) validate_function = getattr(obj_class, validate_function_name, None)