From fb14f49984b224a8f8d8c8a921747251957a1ea3 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Wed, 11 Feb 2026 01:44:04 -0800 Subject: [PATCH] Create OptionalSwitch node + EmptyInputSentinel to deal with inputs that should be seen as 'empty' when passed into nodes --- comfy_api/latest/_io.py | 41 +++++++++++++++++++++++++++++++++++++ comfy_extras/nodes_logic.py | 36 ++++++++++++++++++++++++++++++++ execution.py | 4 ++++ 3 files changed, 81 insertions(+) diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 93cf482ca..4ffcc859c 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -30,6 +30,46 @@ from comfy_execution.graph_utils import ExecutionBlocker from ._util import MESH, VOXEL, SVG as _SVG, File3D +class EmptyInputSentinel: + """ + Sentinel class indicating an empty/missing input. + + Use the class itself (not an instance) as the sentinel. + Compare using 'is' or 'is not' only. + """ + + def __new__(cls): + raise TypeError("EmptyInputSentinel cannot be instantiated, use the class itself") + + def __init_subclass__(cls, **kwargs): + raise TypeError("EmptyInputSentinel cannot be subclassed") + + @classmethod + def __class_getitem__(cls, item): + raise TypeError("EmptyInputSentinel cannot be subscripted") + + def __repr__(self): + return "" + + def __bool__(self): + raise TypeError("EmptyInputSentinel cannot be used in boolean context") + + def __eq__(self, other): + raise TypeError("EmptyInputSentinel cannot be compared with ==, use 'is' instead") + + def __ne__(self, other): + raise TypeError("EmptyInputSentinel cannot be compared with !=, use 'is not' instead") + + def __hash__(self): + raise TypeError("EmptyInputSentinel cannot be hashed") + + def __iter__(self): + raise TypeError("EmptyInputSentinel cannot be iterated") + + def __len__(self): + raise TypeError("EmptyInputSentinel has no length") + + class FolderType(str, Enum): input = "input" output = "output" @@ -2110,6 +2150,7 @@ __all__ = [ "DynamicCombo", "Autogrow", # Other classes + "EmptyInputSentinel", "HiddenHolder", "Hidden", "NodeInfoV1", diff --git a/comfy_extras/nodes_logic.py b/comfy_extras/nodes_logic.py index c066064ac..de10fd05a 100644 --- a/comfy_extras/nodes_logic.py +++ b/comfy_extras/nodes_logic.py @@ -91,6 +91,41 @@ class SoftSwitchNode(io.ComfyNode): return io.NodeOutput(on_true if switch else on_false) +class OptionalSwitchNode(io.ComfyNode): + @classmethod + def define_schema(cls): + template = io.MatchType.Template("switch") + return io.Schema( + node_id="ComfyOptionalSwitchNode", + display_name="Optional Switch", + category="logic", + is_experimental=True, + inputs=[ + io.Boolean.Input("switch"), + io.MatchType.Input("on_false", template=template, lazy=True, optional=True), + io.MatchType.Input("on_true", template=template, lazy=True, optional=True), + ], + outputs=[ + io.MatchType.Output(template=template, display_name="output"), + ], + ) + + @classmethod + def check_lazy_status(cls, switch, on_false=MISSING, on_true=MISSING): + # Only evaluate the input that corresponds to the switch value + if switch and on_true is None: + return ["on_true"] + if not switch and on_false is None: + return ["on_false"] + + @classmethod + def execute(cls, switch, on_true=MISSING, on_false=MISSING) -> io.NodeOutput: + selected = on_true if switch else on_false + if selected is MISSING: + return io.NodeOutput(io.EmptyInputSentinel) + return io.NodeOutput(selected) + + class CustomComboNode(io.ComfyNode): """ Frontend node that allows user to write their own options for a combo. @@ -260,6 +295,7 @@ class LogicExtension(ComfyExtension): async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ SwitchNode, + OptionalSwitchNode, CustomComboNode, # SoftSwitchNode, # ConvertStringToComboNode, diff --git a/execution.py b/execution.py index 896862c6b..20b14dafa 100644 --- a/execution.py +++ b/execution.py @@ -980,6 +980,10 @@ async def validate_inputs(prompt_id, prompt, item, validated): input_filtered[x] = input_data_all[x] if 'input_types' in validate_function_inputs: input_filtered['input_types'] = [received_types] + for x in list(input_filtered.keys()): + if input_filtered[x] is io.EmptyInputSentinel: + del input_filtered[x] + ret = await _async_map_node_over_list(prompt_id, unique_id, obj_class, input_filtered, validate_function_name, v3_data=v3_data) ret = await resolve_map_node_over_list_results(ret)