File size: 5,749 Bytes
5e57c6d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import json
import os
import re
from typing import Union
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
class FlexibleOptionalInputType(dict):
"""A special class to make flexible nodes that pass data to our python handlers.
Enables both flexible/dynamic input types (like for Any Switch) or a dynamic number of inputs
(like for Any Switch, Context Switch, Context Merge, Power Lora Loader, etc).
Initially, ComfyUI only needed to return True for `__contains__` below, which told ComfyUI that
our node will handle the input, regardless of what it is.
However, after https://github.com/comfyanonymous/ComfyUI/pull/2666 ComdyUI's execution changed
also checking the data for the key; specifcially, the type which is the first tuple entry. This
type is supplied to our FlexibleOptionalInputType and returned for any non-data key. This can be a
real type, or use the AnyType for additional flexibility.
"""
def __init__(self, type, data: Union[dict, None] = None):
"""Initializes the FlexibleOptionalInputType.
Args:
type: The flexible type to use when ComfyUI retrieves an unknown key (via `__getitem__`).
data: An optional dict to use as the basis. This is stored both in a `data` attribute, so we
can look it up without hitting our overrides, as well as iterated over and adding its key
and values to our `self` keys. This way, when looked at, we will appear to represent this
data. When used in an "optional" INPUT_TYPES, these are the starting optional node types.
"""
self.type = type
self.data = data
if self.data is not None:
for k, v in self.data.items():
self[k] = v
def __getitem__(self, key):
# If we have this key in the initial data, then return it. Otherwise return the tuple with our
# flexible type.
if self.data is not None and key in self.data:
val = self.data[key]
return val
return (self.type,)
def __contains__(self, key):
"""Always contain a key, and we'll always return the tuple above when asked for it."""
return True
any_type = AnyType("*")
def is_dict_value_falsy(data: dict, dict_key: str):
"""Checks if a dict value is falsy."""
val = get_dict_value(data, dict_key)
return not val
def get_dict_value(data: dict, dict_key: str, default=None):
"""Gets a deeply nested value given a dot-delimited key."""
keys = dict_key.split('.')
key = keys.pop(0) if len(keys) > 0 else None
found = data[key] if key in data else None
if found is not None and len(keys) > 0:
return get_dict_value(found, '.'.join(keys), default)
return found if found is not None else default
def set_dict_value(data: dict, dict_key: str, value, create_missing_objects=True):
"""Sets a deeply nested value given a dot-delimited key."""
keys = dict_key.split('.')
key = keys.pop(0) if len(keys) > 0 else None
if key not in data:
if create_missing_objects is False:
return data
data[key] = {}
if len(keys) == 0:
data[key] = value
else:
set_dict_value(data[key], '.'.join(keys), value, create_missing_objects)
return data
def dict_has_key(data: dict, dict_key):
"""Checks if a dict has a deeply nested dot-delimited key."""
keys = dict_key.split('.')
key = keys.pop(0) if len(keys) > 0 else None
if key is None or key not in data:
return False
if len(keys) == 0:
return True
return dict_has_key(data[key], '.'.join(keys))
def load_json_file(file: str, default=None):
"""Reads a json file and returns the json dict, stripping out "//" comments first."""
if path_exists(file):
with open(file, 'r', encoding='UTF-8') as file:
config = file.read()
try:
return json.loads(config)
except json.decoder.JSONDecodeError:
try:
config = re.sub(r"^\s*//\s.*", "", config, flags=re.MULTILINE)
return json.loads(config)
except json.decoder.JSONDecodeError:
try:
config = re.sub(r"(?:^|\s)//.*", "", config, flags=re.MULTILINE)
return json.loads(config)
except json.decoder.JSONDecodeError:
pass
return default
def save_json_file(file_path: str, data: dict):
"""Saves a json file."""
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w+', encoding='UTF-8') as file:
json.dump(data, file, sort_keys=False, indent=2, separators=(",", ": "))
def path_exists(path):
"""Checks if a path exists, accepting None type."""
if path is not None:
return os.path.exists(path)
return False
def file_exists(path):
"""Checks if a file exists, accepting None type."""
if path is not None:
return os.path.isfile(path)
return False
def remove_path(path):
"""Removes a path, if it exists."""
if path_exists(path):
os.remove(path)
return True
return False
def abspath(file_path: str):
"""Resolves the abspath of a file, resolving symlinks and user dirs."""
if file_path and not path_exists(file_path):
maybe_path = os.path.abspath(os.path.realpath(os.path.expanduser(file_path)))
file_path = maybe_path if path_exists(maybe_path) else file_path
return file_path
class ByPassTypeTuple(tuple):
"""A special class that will return additional "AnyType" strings beyond defined values.
Credit to Trung0246
"""
def __getitem__(self, index):
if index > len(self) - 1:
return AnyType("*")
return super().__getitem__(index)
|