diff --git a/README.md b/README.md index 9c6da0b..1c94fd4 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,36 @@ This is a collection of nodes I find useful. # Nodes -## Conditional Interrupt +## SRL Conditional Interrupt -Interrupts the currently running prompt if the "interrupt" input is true. +![Screenshot of SrlConditionalInterrupt](screenshots/SrlConditionalInterrupt.png) -Also passes through an item of any type for sequencing purposes. +Interrupts the currently running prompt if the "interrupt" input is +true. Also passes through an item of any type for sequencing purposes. + +## SRL Format String + +![Screenshot of SrlFormatString](screenshots/SrlFormatString.png) + +Format a string using Python's string format syntax. You can use both +positional and named argument syntax. + +## SRL Eval + +![Screenshot of SrlEval](screenshots/SrlEval.png) + +Evaluate arbitrary Python code as a function. Code is evaluated as: + + +```def func({parameters}): + code\_line1 + code\_line2 + ... +``` + +Right now this uses the globals dictionary from the module, so be +careful about anything that might mutate globals, such as importing +modules. I don't have any intention to try to make it secure because +ComfyUI itself isn't remotely secure, but I will probably try to +improve isolation at some point in the future to prevent unintended +problems. PRs gladly accepted if you have need for this. diff --git a/__init__.py b/__init__.py index 1212cb8..05df5c0 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,6 @@ +import inspect +import textwrap + import nodes @@ -9,9 +12,8 @@ class AnyType(str): any_typ = AnyType("*") -class ConditionalInterrupt: - def __init__(self): - pass +class SrlConditionalInterrupt: + """Interrupt processing if the boolean input is true. Pass through the other input.""" @classmethod def INPUT_TYPES(cls): @@ -34,14 +36,106 @@ class ConditionalInterrupt: return (inp,) +class SrlFormatString: + """Use Python f-string syntax to generate a string using the inputs as the arguments.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "format": ("STRING", { + "multiline": False, + "default": "first input via str(): {}, second input via repr(): {!r}, third input by index: {2}, fifth input by name: {in4}", + }), + }, + "optional": { + "in0": (any_typ,), + "in1": (any_typ,), + "in2": (any_typ,), + "in3": (any_typ,), + "in4": (any_typ,), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "doit" + CATEGORY = "utils" + + def doit(self, format, **kwargs): + # Allow referencing arguments both by name and index. + return (format.format(*kwargs.values(), **kwargs),) + + +class SrlEval: + """Evaluate any Python code as a function with the given inputs.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "parameters": ("STRING", {"multiline": False, "default": "a, b=None, c=\"foo\", *rest"}), + "code": ("STRING", {"multiline": True, "default": "code goes here\nreturn a + b"}), + }, + "optional": { + "arg0": (any_typ,), + "arg1": (any_typ,), + "arg2": (any_typ,), + "arg3": (any_typ,), + "arg4": (any_typ,), + } + } + + RETURN_TYPES = (any_typ,) + FUNCTION = "doit" + CATEGORY = "utils" + + def doit(self, parameters, code, **kw): + # Indent the code for the main body of the function + func_code = textwrap.indent(code, " ") + source = f"def func({parameters}):\n{func_code}" + + # The provided code can mutate globals or really do anything, but ComfyUI isn't secure to begin with. + loc = {} + exec(source, globals(), loc) + func = loc["func"] + + argspec = inspect.getfullargspec(func) + # We don't allow variable keyword arguments or keyword only arguments, but we do allow varargs + assert argspec.varkw is None + assert not argspec.kwonlyargs + + input_names = list(self.INPUT_TYPES()["optional"].keys()) + parameter_names = argspec.args + + # Convert the list of defaults into a dictionary to make it easier to use + default_list = argspec.defaults if argspec.defaults is not None else [] + defaults = {parameter_name: default for parameter_name, default in zip(parameter_names[-len(default_list):], default_list)} + + # We handle substituting default values ourselves in order to support *args + args = [kw[input_name] if input_name in kw else defaults[parameter_name] for parameter_name, input_name in zip(parameter_names, input_names)] + + # Support *args + if argspec.varargs is not None: + unnamed_inputs = input_names[len(argspec.args):] + # I considered requiring the remaining inputs to be contiguous, but I don't think it's helpful. + args += [kw[input_name] for input_name in unnamed_inputs if input_name in kw] + + ret = func(*args) + return (ret,) + + # A dictionary that contains all nodes you want to export with their names # NOTE: names should be globally unique NODE_CLASS_MAPPINGS = { - "Conditional Interrrupt": ConditionalInterrupt, + "SRL Conditional Interrrupt": SrlConditionalInterrupt, + "SRL Format String": SrlFormatString, + "SRL Eval": SrlEval, } # A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS = { - "ConditionalInterrupt": "Conditional Interrupt", + "SrlConditionalInterrupt": "SRL Conditional Interrupt", + "SrlFormatString": "SRL Format String", + "SrlEval": "SRL Eval", } diff --git a/screenshots/SrlConditionalInterrupt.png b/screenshots/SrlConditionalInterrupt.png new file mode 100644 index 0000000..0e8f823 Binary files /dev/null and b/screenshots/SrlConditionalInterrupt.png differ diff --git a/screenshots/SrlEval.png b/screenshots/SrlEval.png new file mode 100644 index 0000000..85ecffb Binary files /dev/null and b/screenshots/SrlEval.png differ diff --git a/screenshots/SrlFormatString.png b/screenshots/SrlFormatString.png new file mode 100644 index 0000000..36b87f5 Binary files /dev/null and b/screenshots/SrlFormatString.png differ