Critical:
- rename nodes/ -> mp_nodes/, shared/ -> mp_shared/ to avoid shadowing
ComfyUI core's nodes.py via sys.path injection (root cause of v0.1.1
'nodes is not a package' startup failure)
- http_post now streams response and enforces 50MB cap mid-stream (DoS)
- AES key derivation: HKDF -> PBKDF2-HMAC-SHA256 600k iters + random salt
(work factor against weak-key brute force; ciphertext format changed)
- fs_mkdir/copy/move/delete confined to ComfyUI input/output/temp + ~ by
default; UTILITY_MEGAPACK_ALLOW_ARBITRARY_PATHS=1 to override
UX / correctness:
- combo settings now declare options array (themes dropdown was empty)
- LATENT default 1x4x8x8 zeros -> {} (avoids Flux/SD3 shape crashes)
- @op decorator rejects reserved widget names mode/theme at register time
- _pad_outputs docstring updated to match relaxed opaque-type behavior
157 lines
4.1 KiB
Python
157 lines
4.1 KiB
Python
"""Dict and list data-structure operations."""
|
|
|
|
from .. import op
|
|
|
|
|
|
@op(
|
|
op_id="dict_get",
|
|
display_name="Dict Get",
|
|
category="Data Structures",
|
|
input_schema={"required": {
|
|
"data": ("DICT", {"default": {}}),
|
|
"key": ("STRING", {"default": ""}),
|
|
"default": ("STRING", {"default": ""}),
|
|
}},
|
|
output_indices=(6,),
|
|
description="Returns data[key] if present, otherwise default. Default is a string; pass JSON if you need structured defaults.",
|
|
)
|
|
def dict_get(self, data, key, default=""):
|
|
if not isinstance(data, dict):
|
|
return (default,)
|
|
return (data.get(key, default),)
|
|
|
|
|
|
@op(
|
|
op_id="dict_set",
|
|
display_name="Dict Set",
|
|
category="Data Structures",
|
|
input_schema={"required": {
|
|
"data": ("DICT", {"default": {}}),
|
|
"key": ("STRING", {"default": ""}),
|
|
"value": ("STRING", {"default": ""}),
|
|
}},
|
|
output_indices=(5,),
|
|
description="Returns a new dict with data[key] = value. Original is not mutated.",
|
|
)
|
|
def dict_set(self, data, key, value):
|
|
new_data = dict(data) if isinstance(data, dict) else {}
|
|
new_data[key] = value
|
|
return (new_data,)
|
|
|
|
|
|
@op(
|
|
op_id="dict_keys",
|
|
display_name="Dict Keys",
|
|
category="Data Structures",
|
|
input_schema={"required": {"data": ("DICT", {"default": {}})}},
|
|
output_indices=(4,),
|
|
description="Returns sorted list of keys.",
|
|
)
|
|
def dict_keys(self, data):
|
|
if not isinstance(data, dict):
|
|
return ([],)
|
|
return (sorted(data.keys()),)
|
|
|
|
|
|
@op(
|
|
op_id="dict_values",
|
|
display_name="Dict Values",
|
|
category="Data Structures",
|
|
input_schema={"required": {"data": ("DICT", {"default": {}})}},
|
|
output_indices=(4,),
|
|
description="Returns values in key-sorted order.",
|
|
)
|
|
def dict_values(self, data):
|
|
if not isinstance(data, dict):
|
|
return ([],)
|
|
return ([data[k] for k in sorted(data.keys())],)
|
|
|
|
|
|
@op(
|
|
op_id="dict_merge",
|
|
display_name="Dict Merge",
|
|
category="Data Structures",
|
|
input_schema={"required": {
|
|
"a": ("DICT", {"default": {}}),
|
|
"b": ("DICT", {"default": {}}),
|
|
}},
|
|
output_indices=(5,),
|
|
description="Shallow merge. b's keys win on conflicts.",
|
|
)
|
|
def dict_merge(self, a, b):
|
|
out = dict(a) if isinstance(a, dict) else {}
|
|
if isinstance(b, dict):
|
|
out.update(b)
|
|
return (out,)
|
|
|
|
|
|
@op(
|
|
op_id="list_length",
|
|
display_name="List Length",
|
|
category="Data Structures",
|
|
input_schema={"required": {"data": ("LIST", {"default": []})}},
|
|
output_indices=(1,),
|
|
)
|
|
def list_length(self, data):
|
|
return (len(data),)
|
|
|
|
|
|
@op(
|
|
op_id="list_index",
|
|
display_name="List Index",
|
|
category="Data Structures",
|
|
input_schema={"required": {
|
|
"data": ("LIST", {"default": []}),
|
|
"index": ("INT", {"default": 0}),
|
|
}},
|
|
output_indices=(6,),
|
|
description="Negative indexes supported. Raises IndexError on out-of-range.",
|
|
)
|
|
def list_index(self, data, index):
|
|
if not data:
|
|
raise IndexError("list is empty")
|
|
return (data[index],)
|
|
|
|
|
|
@op(
|
|
op_id="list_slice",
|
|
display_name="List Slice",
|
|
category="Data Structures",
|
|
input_schema={"required": {
|
|
"data": ("LIST", {"default": []}),
|
|
"start": ("INT", {"default": 0}),
|
|
"stop": ("INT", {"default": 0}),
|
|
}},
|
|
output_indices=(4,),
|
|
description="stop = 0 means 'to end'.",
|
|
)
|
|
def list_slice(self, data, start=0, stop=0):
|
|
end = len(data) if stop == 0 else stop
|
|
return (list(data[start:end]),)
|
|
|
|
|
|
@op(
|
|
op_id="list_sort",
|
|
display_name="List Sort",
|
|
category="Data Structures",
|
|
input_schema={"required": {
|
|
"data": ("LIST", {"default": []}),
|
|
"reverse": ("BOOLEAN", {"default": False}),
|
|
}},
|
|
output_indices=(4,),
|
|
description="Sorts by string repr (so mixed-type lists don't crash).",
|
|
)
|
|
def list_sort(self, data, reverse=False):
|
|
return (sorted(data, key=str, reverse=bool(reverse)),)
|
|
|
|
|
|
@op(
|
|
op_id="list_reverse",
|
|
display_name="List Reverse",
|
|
category="Data Structures",
|
|
input_schema={"required": {"data": ("LIST", {"default": []})}},
|
|
output_indices=(4,),
|
|
)
|
|
def list_reverse(self, data):
|
|
return (list(reversed(data)),)
|