diff --git a/py/nodes/lora_stack_combiner.py b/py/nodes/lora_stack_combiner.py index 9f3412da..fb3c428b 100644 --- a/py/nodes/lora_stack_combiner.py +++ b/py/nodes/lora_stack_combiner.py @@ -1,6 +1,5 @@ from __future__ import annotations -import inspect import re from typing import Any @@ -22,20 +21,29 @@ def _stack_slot_number(name: str) -> int: return 1 if letter == "a" else 2 -class _LoraStackOptionalInputs: - """Lookup that preserves explicit optional inputs and dynamic lora_stack slots.""" +class _LoraStackOptionalInputs(dict): + """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: - self._explicit_inputs = explicit_inputs + super().__init__(explicit_inputs) def __contains__(self, item: object) -> bool: if not isinstance(item, str): 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]]: - if key in self._explicit_inputs: - return self._explicit_inputs[key] + if super().__contains__(key): + return super().__getitem__(key) if _is_stack_input(key): return ( "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 { "required": {}, - "optional": optional_inputs, + "optional": _LoraStackOptionalInputs(optional_inputs), } RETURN_TYPES = ("LORA_STACK",) diff --git a/py/nodes/prompt.py b/py/nodes/prompt.py index 6eba2ab3..4fafa1c7 100644 --- a/py/nodes/prompt.py +++ b/py/nodes/prompt.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import Any -import inspect from ..services.wildcard_service import ( contains_dynamic_syntax, @@ -11,20 +10,29 @@ from ..services.wildcard_service import ( ) -class _PromptOptionalInputs: - """Lookup that preserves explicit optional inputs and dynamic trigger slots.""" +class _PromptOptionalInputs(dict): + """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: - self._explicit_inputs = explicit_inputs + super().__init__(explicit_inputs) def __contains__(self, item: object) -> bool: if not isinstance(item, str): 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]]: - if key in self._explicit_inputs: - return self._explicit_inputs[key] + if super().__contains__(key): + return super().__getitem__(key) if is_trigger_words_input(key): return ( "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 { "required": { "text": ( @@ -85,7 +89,7 @@ class PromptLM: {"tooltip": "The CLIP model used for encoding the text."}, ), }, - "optional": optional_inputs, + "optional": _PromptOptionalInputs(optional_inputs), "hidden": { "prompt": "PROMPT", "unique_id": "UNIQUE_ID", diff --git a/tests/nodes/test_lora_stack_combiner.py b/tests/nodes/test_lora_stack_combiner.py index 79e52ef9..8485eb40 100644 --- a/tests/nodes/test_lora_stack_combiner.py +++ b/tests/nodes/test_lora_stack_combiner.py @@ -1,4 +1,4 @@ -import types +import json import pytest @@ -130,12 +130,7 @@ def test_input_types_exposes_two_default_slots(): assert input_types["optional"]["lora_stack2"][0] == "LORA_STACK" -def test_input_types_recognizes_dynamic_slots_from_get_input_info(monkeypatch): - frames = [None, None, types.SimpleNamespace(function="get_input_info")] - monkeypatch.setattr( - "py.nodes.lora_stack_combiner.inspect.stack", lambda: frames - ) - +def test_input_types_resolves_dynamic_slots(): input_types = LoraStackCombinerLM.INPUT_TYPES() 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" +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(): proxy = _LoraStackOptionalInputs({"lora_stack1": ("LORA_STACK", {})}) diff --git a/tests/nodes/test_prompt_text_wildcards.py b/tests/nodes/test_prompt_text_wildcards.py index e8a2beb5..b704400c 100644 --- a/tests/nodes/test_prompt_text_wildcards.py +++ b/tests/nodes/test_prompt_text_wildcards.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from typing import Any, cast 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"] +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(): result = TextLM.IS_CHANGED("__flower__", seed=None)