mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-07 10:02:14 -03:00
refactor(nodes): resolve dynamic inputs without inspect.stack()
The Prompt and Lora Stack Combiner nodes expose unbounded dynamic input
slots (trigger_wordsN / lora_stackN). They resolved them by having
INPUT_TYPES() return a custom lookup object, but only when the caller was
ComfyUI's get_input_info() -- detected with inspect.stack(). That frame
inspection is what the registry security scan reports as
python_anti_debugging under the obfuscated-code admin tag.
Make the lookup a dict subclass instead, so INPUT_TYPES() can always
return it:
* /object_info (server.py) json.dumps INPUT_TYPES() directly, and a
dict subclass serializes its stored entries -- byte-identical to the
plain dict that was returned before.
* input_order (list(value.keys())), validate_inputs'
set(class_inputs["optional"]) and every other iteration still see only
the static slots.
* get_input_info() (graph.py) keeps resolving dynamic names through the
overridden __contains__/__getitem__, which no longer depends on who
the caller is.
The one behaviour change is in execution.py:get_input_data -- a dynamic
input passed as a constant rather than a link now reaches the node
instead of being silently dropped. These inputs are declared forceInput,
so the frontend only offers links; where it can happen the new behaviour
is the intended one.
Verified against ComfyUI's own consumer code: json.dumps output, keys(),
set(optional) and get_input_info() lookups all match the old behaviour,
and the two nodes no longer cross-resolve each other's slots.
3657 passed, 7 skipped.
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -22,20 +21,29 @@ def _stack_slot_number(name: str) -> int:
|
|||||||
return 1 if letter == "a" else 2
|
return 1 if letter == "a" else 2
|
||||||
|
|
||||||
|
|
||||||
class _LoraStackOptionalInputs:
|
class _LoraStackOptionalInputs(dict):
|
||||||
"""Lookup that preserves explicit optional inputs and dynamic lora_stack slots."""
|
"""Optional-input mapping that also resolves dynamically added stack slots.
|
||||||
|
|
||||||
|
Inheriting ``dict`` keeps ``INPUT_TYPES()`` JSON-serializable for ComfyUI's
|
||||||
|
``/object_info`` route: it serializes the stored entries, exactly as the plain
|
||||||
|
dict did before. The overridden ``__contains__``/``__getitem__`` let the
|
||||||
|
execution side resolve ``lora_stack3``-style inputs the frontend adds on
|
||||||
|
demand. This replaces the previous ``inspect.stack()`` check for the
|
||||||
|
``get_input_info`` caller, which the registry security scan reports as
|
||||||
|
anti-debugging.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, explicit_inputs: dict[str, tuple[str, dict[str, Any]]]) -> None:
|
def __init__(self, explicit_inputs: dict[str, tuple[str, dict[str, Any]]]) -> None:
|
||||||
self._explicit_inputs = explicit_inputs
|
super().__init__(explicit_inputs)
|
||||||
|
|
||||||
def __contains__(self, item: object) -> bool:
|
def __contains__(self, item: object) -> bool:
|
||||||
if not isinstance(item, str):
|
if not isinstance(item, str):
|
||||||
return False
|
return False
|
||||||
return item in self._explicit_inputs or _is_stack_input(item)
|
return super().__contains__(item) or _is_stack_input(item)
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> tuple[str, dict[str, Any]]:
|
def __getitem__(self, key: str) -> tuple[str, dict[str, Any]]:
|
||||||
if key in self._explicit_inputs:
|
if super().__contains__(key):
|
||||||
return self._explicit_inputs[key]
|
return super().__getitem__(key)
|
||||||
if _is_stack_input(key):
|
if _is_stack_input(key):
|
||||||
return (
|
return (
|
||||||
"LORA_STACK",
|
"LORA_STACK",
|
||||||
@@ -71,13 +79,9 @@ class LoraStackCombinerLM:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
stack = inspect.stack()
|
|
||||||
if len(stack) > 2 and stack[2].function == "get_input_info":
|
|
||||||
optional_inputs = _LoraStackOptionalInputs(optional_inputs) # pyright: ignore[reportAssignmentType]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"required": {},
|
"required": {},
|
||||||
"optional": optional_inputs,
|
"optional": _LoraStackOptionalInputs(optional_inputs),
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("LORA_STACK",)
|
RETURN_TYPES = ("LORA_STACK",)
|
||||||
|
|||||||
+16
-12
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import inspect
|
|
||||||
|
|
||||||
from ..services.wildcard_service import (
|
from ..services.wildcard_service import (
|
||||||
contains_dynamic_syntax,
|
contains_dynamic_syntax,
|
||||||
@@ -11,20 +10,29 @@ from ..services.wildcard_service import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class _PromptOptionalInputs:
|
class _PromptOptionalInputs(dict):
|
||||||
"""Lookup that preserves explicit optional inputs and dynamic trigger slots."""
|
"""Optional-input mapping that also resolves dynamically added trigger slots.
|
||||||
|
|
||||||
|
Inheriting ``dict`` keeps ``INPUT_TYPES()`` JSON-serializable for ComfyUI's
|
||||||
|
``/object_info`` route: it serializes the stored entries, exactly as the plain
|
||||||
|
dict did before. The overridden ``__contains__``/``__getitem__`` let the
|
||||||
|
execution side resolve ``trigger_words3``-style inputs the frontend adds on
|
||||||
|
demand. This replaces the previous ``inspect.stack()`` check for the
|
||||||
|
``get_input_info`` caller, which the registry security scan reports as
|
||||||
|
anti-debugging.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, explicit_inputs: dict[str, tuple[str, dict[str, Any]]]) -> None:
|
def __init__(self, explicit_inputs: dict[str, tuple[str, dict[str, Any]]]) -> None:
|
||||||
self._explicit_inputs = explicit_inputs
|
super().__init__(explicit_inputs)
|
||||||
|
|
||||||
def __contains__(self, item: object) -> bool:
|
def __contains__(self, item: object) -> bool:
|
||||||
if not isinstance(item, str):
|
if not isinstance(item, str):
|
||||||
return False
|
return False
|
||||||
return item in self._explicit_inputs or is_trigger_words_input(item)
|
return super().__contains__(item) or is_trigger_words_input(item)
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> tuple[str, dict[str, Any]]:
|
def __getitem__(self, key: str) -> tuple[str, dict[str, Any]]:
|
||||||
if key in self._explicit_inputs:
|
if super().__contains__(key):
|
||||||
return self._explicit_inputs[key]
|
return super().__getitem__(key)
|
||||||
if is_trigger_words_input(key):
|
if is_trigger_words_input(key):
|
||||||
return (
|
return (
|
||||||
"STRING",
|
"STRING",
|
||||||
@@ -66,10 +74,6 @@ class PromptLM:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
stack = inspect.stack()
|
|
||||||
if len(stack) > 2 and stack[2].function == "get_input_info":
|
|
||||||
optional_inputs = _PromptOptionalInputs(optional_inputs) # pyright: ignore[reportAssignmentType]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"text": (
|
"text": (
|
||||||
@@ -85,7 +89,7 @@ class PromptLM:
|
|||||||
{"tooltip": "The CLIP model used for encoding the text."},
|
{"tooltip": "The CLIP model used for encoding the text."},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"optional": optional_inputs,
|
"optional": _PromptOptionalInputs(optional_inputs),
|
||||||
"hidden": {
|
"hidden": {
|
||||||
"prompt": "PROMPT",
|
"prompt": "PROMPT",
|
||||||
"unique_id": "UNIQUE_ID",
|
"unique_id": "UNIQUE_ID",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import types
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -130,12 +130,7 @@ def test_input_types_exposes_two_default_slots():
|
|||||||
assert input_types["optional"]["lora_stack2"][0] == "LORA_STACK"
|
assert input_types["optional"]["lora_stack2"][0] == "LORA_STACK"
|
||||||
|
|
||||||
|
|
||||||
def test_input_types_recognizes_dynamic_slots_from_get_input_info(monkeypatch):
|
def test_input_types_resolves_dynamic_slots():
|
||||||
frames = [None, None, types.SimpleNamespace(function="get_input_info")]
|
|
||||||
monkeypatch.setattr(
|
|
||||||
"py.nodes.lora_stack_combiner.inspect.stack", lambda: frames
|
|
||||||
)
|
|
||||||
|
|
||||||
input_types = LoraStackCombinerLM.INPUT_TYPES()
|
input_types = LoraStackCombinerLM.INPUT_TYPES()
|
||||||
optional = input_types["optional"]
|
optional = input_types["optional"]
|
||||||
|
|
||||||
@@ -145,6 +140,29 @@ def test_input_types_recognizes_dynamic_slots_from_get_input_info(monkeypatch):
|
|||||||
assert optional["lora_stack25"][0] == "LORA_STACK"
|
assert optional["lora_stack25"][0] == "LORA_STACK"
|
||||||
|
|
||||||
|
|
||||||
|
def test_input_types_serializes_only_static_slots():
|
||||||
|
"""ComfyUI's /object_info route json.dumps INPUT_TYPES() directly."""
|
||||||
|
input_types = LoraStackCombinerLM.INPUT_TYPES()
|
||||||
|
|
||||||
|
payload = json.loads(json.dumps(input_types))
|
||||||
|
|
||||||
|
assert set(payload["optional"]) == {"lora_stack1", "lora_stack2"}
|
||||||
|
assert payload["optional"]["lora_stack1"][0] == "LORA_STACK"
|
||||||
|
|
||||||
|
|
||||||
|
def test_input_types_supports_get_input_info_lookup_order():
|
||||||
|
"""Mirror comfy_execution.graph.get_input_info()'s containment then lookup."""
|
||||||
|
valid_inputs = LoraStackCombinerLM.INPUT_TYPES()
|
||||||
|
input_name = "lora_stack7"
|
||||||
|
|
||||||
|
assert "optional" in valid_inputs
|
||||||
|
assert input_name in valid_inputs["optional"]
|
||||||
|
input_type, extra_info = valid_inputs["optional"][input_name]
|
||||||
|
|
||||||
|
assert input_type == "LORA_STACK"
|
||||||
|
assert "tooltip" in extra_info
|
||||||
|
|
||||||
|
|
||||||
def test_lora_stack_optional_inputs_proxy():
|
def test_lora_stack_optional_inputs_proxy():
|
||||||
proxy = _LoraStackOptionalInputs({"lora_stack1": ("LORA_STACK", {})})
|
proxy = _LoraStackOptionalInputs({"lora_stack1": ("LORA_STACK", {})})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from py.nodes.prompt import PromptLM
|
from py.nodes.prompt import PromptLM
|
||||||
@@ -63,6 +64,29 @@ def test_text_lm_input_types_expose_input_only_seed():
|
|||||||
assert "wildcard generation" in cast(Any, seed_options)["tooltip"]
|
assert "wildcard generation" in cast(Any, seed_options)["tooltip"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_lm_input_types_resolve_dynamic_trigger_words_slots():
|
||||||
|
optional = PromptLM.INPUT_TYPES()["optional"]
|
||||||
|
|
||||||
|
assert "trigger_words1" in optional
|
||||||
|
assert optional["trigger_words1"][0] == "STRING"
|
||||||
|
|
||||||
|
assert "trigger_words4" in optional
|
||||||
|
trigger_type, trigger_options = optional["trigger_words4"]
|
||||||
|
assert trigger_type == "STRING"
|
||||||
|
assert cast(Any, trigger_options)["forceInput"] is True
|
||||||
|
|
||||||
|
# Dynamic slots resolve on demand without becoming real entries.
|
||||||
|
assert "trigger_words4" not in dict(optional)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_lm_input_types_serialize_only_static_slots():
|
||||||
|
"""ComfyUI's /object_info route json.dumps INPUT_TYPES() directly."""
|
||||||
|
payload = json.loads(json.dumps(PromptLM.INPUT_TYPES()))
|
||||||
|
|
||||||
|
assert set(payload["optional"]) == {"seed", "trigger_words1"}
|
||||||
|
assert payload["optional"]["trigger_words1"][0] == "STRING"
|
||||||
|
|
||||||
|
|
||||||
def test_text_lm_is_changed_forces_rerun_without_seed_when_text_is_dynamic():
|
def test_text_lm_is_changed_forces_rerun_without_seed_when_text_is_dynamic():
|
||||||
result = TextLM.IS_CHANGED("__flower__", seed=None)
|
result = TextLM.IS_CHANGED("__flower__", seed=None)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user