v0.0.2 Alpha
Tons of new nodes and functionality implemented, lots of bugs fixed. Should be mostly stable-ish now.
@@ -0,0 +1,676 @@
|
||||
import ast
|
||||
import operator
|
||||
import math
|
||||
import random
|
||||
import fnmatch
|
||||
import os
|
||||
|
||||
DEBUG_MODE = True #Enable this flag to get all sorts of useful debug information in the console from most of the nodes in this pack.
|
||||
|
||||
# HELPER FUNCTIONS
|
||||
#******************
|
||||
'''
|
||||
FUNCTION NAME: cbool
|
||||
PURPOSE: Converts values to Boolean
|
||||
PARAMETERS:
|
||||
- value (Any): The value to convert
|
||||
RETURNS: True or False based on whether value can be interpreted as a Boolean
|
||||
'''
|
||||
|
||||
def debug_print(*args,end=" "):
|
||||
if DEBUG_MODE:
|
||||
print(end.join(map(str, args)),sep="")
|
||||
|
||||
|
||||
def cbool(value):
|
||||
if str(value).lower() in ("yes", "y", "true", "t", "1"):
|
||||
return True
|
||||
if str(value).lower() in ("no", "n", "false", "f", "0", "0.0", "", "none", "[]", "{}"):
|
||||
return False
|
||||
raise Exception('Invalid value for boolean conversion:', value)
|
||||
|
||||
'''
|
||||
FUNCTION NAME: cint
|
||||
PURPOSE: Converts values to Integer
|
||||
PARAMETERS:
|
||||
- value (Any): The value to convert
|
||||
RETURNS: An integer rounded to the nearest even number
|
||||
'''
|
||||
def cint(value):
|
||||
|
||||
if value == "":
|
||||
return 0
|
||||
|
||||
d = 0 #How many decimals to round to. For integers this is always 0
|
||||
try:
|
||||
value=float(value)
|
||||
except:
|
||||
try:
|
||||
value = len(value)
|
||||
if l == 0:
|
||||
return 0
|
||||
except:
|
||||
raise Exception('Invalid value for integer conversion:',value)
|
||||
|
||||
p = 10 ** d
|
||||
|
||||
if value > 0:
|
||||
z = float(math.floor((value * p) + 0.5))/p
|
||||
else:
|
||||
z = float(math.ceil((value * p) - 0.5))/p
|
||||
|
||||
return int(z)
|
||||
|
||||
def is_list(x):
|
||||
|
||||
if type(x) is str:
|
||||
return False
|
||||
|
||||
try:
|
||||
iter(x)
|
||||
return True
|
||||
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def search_folder(folder_path, pattern, recursive, full_path, include_directories,relative_filenames):
|
||||
|
||||
if relative_filenames == True:
|
||||
relative_filenames = folder_path
|
||||
elif relative_filenames == False:
|
||||
relative_filenames = ""
|
||||
|
||||
entries = os.scandir(folder_path)
|
||||
|
||||
try:
|
||||
for entry in entries:
|
||||
if fnmatch.fnmatch(entry.name, pattern):
|
||||
if entry.is_file() or (include_directories and entry.is_dir()):
|
||||
if not full_path:
|
||||
if relative_filenames!="":
|
||||
yield os.path.relpath(entry.path,relative_filenames)
|
||||
else:
|
||||
yield entry.name
|
||||
else:
|
||||
yield entry.path
|
||||
if entry.is_dir() and recursive:
|
||||
yield from search_folder(entry.path, pattern, recursive, full_path, include_directories,relative_filenames)
|
||||
finally:
|
||||
entries.close()
|
||||
|
||||
"""
|
||||
def search_folder(folder_path, pattern, recursive,full_path,include_directories):
|
||||
with os.scandir(folder_path) as files:
|
||||
for f in files:
|
||||
if fnmatch.fnmatch(f.name, pattern):
|
||||
if f.is_file() or (include_directories and f.is_dir()):
|
||||
if not full_path:
|
||||
yield os.path.relpath(f.path,folder_path)
|
||||
else:
|
||||
yield f.path
|
||||
if f.is_dir() and recursive: yield from search_folder(f.path, pattern, recursive, full_path,include_directories)
|
||||
"""
|
||||
|
||||
"""
|
||||
def search_folder(folder_path, pattern, recursive,full_path,include_directories):
|
||||
with os.scandir(folder_path) as files:
|
||||
for f in files:
|
||||
if f.is_file() and fnmatch.fnmatch(f.name, pattern):
|
||||
if not full_path:
|
||||
print ("RET =", os.path.relpath(f.path,folder_path), " FROM ",f.path)
|
||||
yield os.path.relpath(f.path,folder_path)
|
||||
else:
|
||||
yield f.path
|
||||
elif f.is_dir():
|
||||
if not full_path:
|
||||
yield os.path.relpath(f.path,folder_path)
|
||||
else:
|
||||
yield f.path
|
||||
if recursive: yield from search_folder(f.path, pattern, recursive, full_path,include_directories)
|
||||
"""
|
||||
def word_test(op,expr):
|
||||
|
||||
if op is None or op == "":
|
||||
return False
|
||||
|
||||
result = False
|
||||
|
||||
try:
|
||||
|
||||
if len(expr) != 0:
|
||||
|
||||
if not is_list(expr):
|
||||
expr = [expr]
|
||||
|
||||
for x in expr:
|
||||
|
||||
for y in " ".join(str(x).splitlines()).split(" "):
|
||||
|
||||
match op.casefold():
|
||||
case "alpha":
|
||||
result = ( y.isalpha() ) if y!="" else True
|
||||
case "numeric":
|
||||
print ("Y=",y)
|
||||
if y!= "":
|
||||
try:
|
||||
float(y)
|
||||
result = True
|
||||
except ValueError:
|
||||
pass
|
||||
return False
|
||||
else:
|
||||
result = True
|
||||
#if y!= "": y = y.replace('.','').replace('+','').replace('-','')
|
||||
#result = (y.isdigit() ) if y!="" else True
|
||||
case _:
|
||||
return False
|
||||
|
||||
if not result:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def extract_between(expr,token1,token2=None):
|
||||
|
||||
ret = []
|
||||
|
||||
if token2 is None:
|
||||
token2 = token1
|
||||
|
||||
if token1 == token2:
|
||||
|
||||
tmp=expr.split(token1)
|
||||
|
||||
try:
|
||||
for x in range(1,len(tmp)-1,2):
|
||||
ret.append (tmp[x])
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
|
||||
i = len(expr)
|
||||
|
||||
while (i != 0):
|
||||
|
||||
L = expr.partition(token1)[2]
|
||||
R = L.partition(token2)[0]
|
||||
expr = L[ len(R)+len(token2):]
|
||||
i = len(expr)
|
||||
if (R!="" and L!=R): ret.append (R)
|
||||
|
||||
return ret
|
||||
|
||||
def replace_caseless(text="", old="",new="",max=0):
|
||||
|
||||
idx,c = 0,0
|
||||
|
||||
if old is None: old = ""
|
||||
if new is None: new = ""
|
||||
|
||||
while idx < len(text):
|
||||
|
||||
index_l = text.casefold().find(old.casefold(), idx)
|
||||
|
||||
if index_l == -1:
|
||||
return text
|
||||
|
||||
text = text[:index_l] + new + text[index_l + len(old):]
|
||||
idx = index_l + len(new)
|
||||
|
||||
c+=1
|
||||
if c == max:
|
||||
break
|
||||
|
||||
return text
|
||||
|
||||
import ast
|
||||
import operator
|
||||
import math
|
||||
import random
|
||||
|
||||
# Define supported operators
|
||||
operators = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.FloorDiv: operator.floordiv,
|
||||
ast.Mod: operator.mod,
|
||||
ast.Pow: operator.pow,
|
||||
ast.BitXor: operator.xor,
|
||||
ast.USub: operator.neg,
|
||||
ast.UAdd: operator.pos, # Unary addition
|
||||
ast.Invert: operator.inv, # Bitwise inversion
|
||||
ast.Eq: operator.eq,
|
||||
ast.NotEq: operator.ne,
|
||||
ast.Lt: operator.lt,
|
||||
ast.LtE: operator.le,
|
||||
ast.Gt: operator.gt,
|
||||
ast.GtE: operator.ge,
|
||||
ast.And: operator.and_,
|
||||
ast.Or: operator.or_,
|
||||
ast.Not: operator.not_,
|
||||
ast.Is: operator.is_,
|
||||
ast.IsNot: operator.is_not,
|
||||
ast.In: lambda x, y: operator.contains(y, x),
|
||||
ast.NotIn: lambda x, y: not operator.contains(y, x),
|
||||
ast.BitAnd: operator.and_,
|
||||
ast.BitOr: operator.or_,
|
||||
ast.LShift: operator.lshift,
|
||||
ast.RShift: operator.rshift,
|
||||
ast.MatMult: operator.matmul, # Matrix multiplication
|
||||
}
|
||||
|
||||
# Define supported functions
|
||||
default_functions = {
|
||||
'abs': abs,
|
||||
'all': all,
|
||||
'any': any,
|
||||
'ascii': ascii,
|
||||
'bin': bin,
|
||||
'bool': bool,
|
||||
'chr': chr,
|
||||
'dict': dict,
|
||||
'divmod': divmod,
|
||||
'enumerate': enumerate,
|
||||
'filter': filter,
|
||||
'float': float,
|
||||
'format': format,
|
||||
'hex': hex,
|
||||
'id': id,
|
||||
'int': int,
|
||||
'len': len,
|
||||
'list': list,
|
||||
'map': map,
|
||||
'max': max,
|
||||
'min': min,
|
||||
'oct': oct,
|
||||
'ord': ord,
|
||||
'pow': pow,
|
||||
'print': print,
|
||||
'range': range,
|
||||
'repr': repr,
|
||||
'reversed': reversed,
|
||||
'round': round,
|
||||
'set': set,
|
||||
'sorted': sorted,
|
||||
'str': str,
|
||||
'sum': sum,
|
||||
'tuple': tuple,
|
||||
'type': type,
|
||||
'zip': zip,
|
||||
'math': math,
|
||||
'random': random,
|
||||
'randrange': random.randrange,
|
||||
'randint': random.randint,
|
||||
'choice': random.choice,
|
||||
'shuffle': random.shuffle,
|
||||
'sample': random.sample,
|
||||
'uniform': random.uniform,
|
||||
'rnd': random.random,
|
||||
'seed': random.seed,
|
||||
}
|
||||
|
||||
def safe_eval(expr, variables=None, additional_functions=None):
|
||||
"""
|
||||
Safely evaluate a mathematical expression with named variables, including list and dictionary indexing,
|
||||
logical operators, predefined function calls, list comprehensions, and conditionals.
|
||||
|
||||
:param expr: The expression to evaluate as a string.
|
||||
:param variables: A dictionary of variable names and their values.
|
||||
:param additional_functions: A dictionary of additional functions to support.
|
||||
:return: The result of the evaluated expression.
|
||||
"""
|
||||
if variables is None:
|
||||
variables = {}
|
||||
|
||||
if additional_functions is None:
|
||||
additional_functions = {}
|
||||
|
||||
# Merge default functions with additional functions
|
||||
functions = {**default_functions, **additional_functions}
|
||||
|
||||
# Parse expression into AST
|
||||
node = ast.parse(expr, mode='exec')
|
||||
|
||||
def _eval(node, local_vars=None):
|
||||
if local_vars is None:
|
||||
local_vars = {}
|
||||
|
||||
if isinstance(node, ast.Expression):
|
||||
return _eval(node.body, local_vars)
|
||||
elif isinstance(node, ast.Assign):
|
||||
targets = node.targets
|
||||
if len(targets) != 1:
|
||||
raise ValueError("Only single target assignments are supported")
|
||||
target = targets[0]
|
||||
value = _eval(node.value, local_vars)
|
||||
if isinstance(target, ast.Tuple):
|
||||
if not isinstance(value, (tuple, list)) or len(target.elts) != len(value):
|
||||
raise ValueError("Mismatch between tuple assignment and values")
|
||||
for elt, val in zip(target.elts, value):
|
||||
if not isinstance(elt, ast.Name):
|
||||
raise ValueError("Only simple variable assignments are supported")
|
||||
local_vars[elt.id] = val
|
||||
else:
|
||||
if not isinstance(target, ast.Name):
|
||||
raise ValueError("Only simple variable assignments are supported")
|
||||
local_vars[target.id] = value
|
||||
return value
|
||||
elif isinstance(node, ast.NamedExpr): # Handling the walrus operator :=
|
||||
target = node.target
|
||||
value = _eval(node.value, local_vars)
|
||||
if isinstance(target, ast.Tuple):
|
||||
if not isinstance(value, (tuple, list)) or len(target.elts) != len(value):
|
||||
raise ValueError("Mismatch between tuple assignment and values")
|
||||
for elt, val in zip(target.elts, value):
|
||||
if not isinstance(elt, ast.Name):
|
||||
raise ValueError("Only simple variable assignments are supported")
|
||||
local_vars[elt.id] = val
|
||||
else:
|
||||
if not isinstance(target, ast.Name):
|
||||
raise ValueError("Only simple variable assignments are supported")
|
||||
local_vars[target.id] = value
|
||||
return value
|
||||
elif isinstance(node, ast.BinOp):
|
||||
left = _eval(node.left, local_vars)
|
||||
right = _eval(node.right, local_vars)
|
||||
return operators[type(node.op)](left, right)
|
||||
elif isinstance(node, ast.UnaryOp):
|
||||
operand = _eval(node.operand, local_vars)
|
||||
return operators[type(node.op)](operand)
|
||||
elif isinstance(node, ast.BoolOp):
|
||||
if isinstance(node.op, ast.And):
|
||||
for value in node.values:
|
||||
result = _eval(value, local_vars)
|
||||
if not result:
|
||||
return result
|
||||
return result
|
||||
elif isinstance(node.op, ast.Or):
|
||||
for value in node.values:
|
||||
result = _eval(value, local_vars)
|
||||
if result:
|
||||
return result
|
||||
return result
|
||||
elif isinstance(node, ast.Compare):
|
||||
left = _eval(node.left, local_vars)
|
||||
for operation, comparator in zip(node.ops, node.comparators):
|
||||
right = _eval(comparator, local_vars)
|
||||
if not operators[type(operation)](left, right):
|
||||
return False
|
||||
left = right
|
||||
return True
|
||||
elif isinstance(node, ast.Num): # For Python 3.8 and earlier
|
||||
return node.n
|
||||
elif isinstance(node, ast.Constant): # For Python 3.8 and later
|
||||
return node.value
|
||||
elif isinstance(node, ast.Name):
|
||||
if node.id in local_vars:
|
||||
return local_vars[node.id]
|
||||
elif node.id in variables:
|
||||
return variables[node.id]
|
||||
elif node.id in functions:
|
||||
return functions[node.id]
|
||||
elif node.id in {'True', 'False', 'None'}:
|
||||
return {'True': True, 'False': False, 'None': None}[node.id]
|
||||
else:
|
||||
raise NameError(f"Variable '{node.id}' is not defined")
|
||||
elif isinstance(node, ast.Subscript):
|
||||
value = _eval(node.value, local_vars)
|
||||
index = _eval(node.slice, local_vars)
|
||||
return value[index]
|
||||
elif isinstance(node, ast.Index): # For Python 3.8 and earlier
|
||||
return _eval(node.value, local_vars)
|
||||
elif isinstance(node, ast.Slice):
|
||||
lower = _eval(node.lower, local_vars) if node.lower else None
|
||||
upper = _eval(node.upper, local_vars) if node.upper else None
|
||||
step = _eval(node.step, local_vars) if node.step else None
|
||||
return slice(lower, upper, step)
|
||||
elif isinstance(node, ast.Tuple):
|
||||
return tuple(_eval(elt, local_vars) for elt in node.elts)
|
||||
elif isinstance(node, ast.List):
|
||||
return [_eval(elt, local_vars) for elt in node.elts]
|
||||
elif isinstance(node, ast.Dict):
|
||||
return {_eval(key, local_vars): _eval(value, local_vars) for key, value in zip(node.keys, node.values)}
|
||||
elif isinstance(node, ast.Call):
|
||||
func = _eval(node.func, local_vars)
|
||||
args = [_eval(arg, local_vars) for arg in node.args]
|
||||
if callable(func):
|
||||
return func(*args)
|
||||
else:
|
||||
raise TypeError(f"Unsupported function: {func}")
|
||||
elif isinstance(node, ast.Attribute):
|
||||
value = _eval(node.value, local_vars)
|
||||
if hasattr(value, node.attr):
|
||||
return getattr(value, node.attr)
|
||||
else:
|
||||
raise AttributeError(f"Attribute '{node.attr}' not found in {value}")
|
||||
elif isinstance(node, ast.IfExp):
|
||||
test = _eval(node.test, local_vars)
|
||||
if test:
|
||||
return _eval(node.body, local_vars)
|
||||
else:
|
||||
return _eval(node.orelse, local_vars)
|
||||
elif isinstance(node, ast.ListComp):
|
||||
elt = node.elt
|
||||
generators = node.generators
|
||||
return _eval_listcomp(elt, generators, local_vars)
|
||||
elif isinstance(node, ast.Lambda):
|
||||
return _eval_lambda(node, local_vars)
|
||||
elif isinstance(node, ast.Expr):
|
||||
return _eval(node.value, local_vars)
|
||||
elif isinstance(node, ast.Module):
|
||||
for stmt in node.body:
|
||||
result = _eval(stmt, local_vars)
|
||||
return result
|
||||
else:
|
||||
raise TypeError(f"Unsupported type: {type(node)}")
|
||||
|
||||
def _eval_listcomp(elt, generators, local_vars):
|
||||
"""
|
||||
Evaluate a list comprehension.
|
||||
|
||||
:param elt: The element expression of the list comprehension.
|
||||
:param generators: The generators of the list comprehension.
|
||||
:param local_vars: The local variables for the list comprehension.
|
||||
:return: The evaluated list comprehension.
|
||||
"""
|
||||
if not generators:
|
||||
return [_eval(elt, local_vars)]
|
||||
|
||||
gen = generators[0]
|
||||
iter_ = _eval(gen.iter, local_vars)
|
||||
result = []
|
||||
|
||||
for item in iter_:
|
||||
new_local_vars = local_vars.copy()
|
||||
if isinstance(gen.target, ast.Name):
|
||||
new_local_vars[gen.target.id] = item
|
||||
elif isinstance(gen.target, ast.Tuple):
|
||||
if isinstance(item, tuple) and len(gen.target.elts) == len(item):
|
||||
for elt, value in zip(gen.target.elts, item):
|
||||
new_local_vars[elt.id] = value
|
||||
else:
|
||||
raise ValueError("Invalid tuple unpacking in list comprehension")
|
||||
if all(_eval(cond, new_local_vars) for cond in gen.ifs):
|
||||
result.extend(_eval_listcomp(elt, generators[1:], new_local_vars))
|
||||
|
||||
return result
|
||||
|
||||
def _eval_lambda(node, local_vars):
|
||||
"""
|
||||
Evaluate a lambda function.
|
||||
|
||||
:param node: The lambda node.
|
||||
:param local_vars: The local variables for the lambda function.
|
||||
:return: The evaluated lambda function.
|
||||
"""
|
||||
if not isinstance(node, ast.Lambda):
|
||||
raise TypeError(f"Expected ast.Lambda, got {type(node)}")
|
||||
|
||||
arg_names = [arg.arg for arg in node.args.args]
|
||||
|
||||
def lambda_func(*args):
|
||||
if len(args) != len(arg_names):
|
||||
raise TypeError(f"Expected {len(arg_names)} arguments, got {len(args)}")
|
||||
lambda_local_vars = local_vars.copy()
|
||||
lambda_local_vars.update(zip(arg_names, args))
|
||||
return _eval(node.body, lambda_local_vars)
|
||||
|
||||
return lambda_func
|
||||
|
||||
return _eval(node, variables)
|
||||
|
||||
"""
|
||||
# Example usage:
|
||||
variables = {
|
||||
'x': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
'y': 5,
|
||||
'z': {'a': 1, 'b': 2},
|
||||
'a': 3,
|
||||
'b': 4
|
||||
}
|
||||
expression1 = "x[y] + 2 ** 3"
|
||||
result1 = safe_eval(expression1, variables)
|
||||
print(result1) # Output: 13
|
||||
|
||||
expression2 = "z['a'] + z['b']"
|
||||
result2 = safe_eval(expression2, variables)
|
||||
print(result2) # Output: 3
|
||||
|
||||
expression3 = "a < b and z['a'] == 1"
|
||||
result3 = safe_eval(expression3, variables)
|
||||
print(result3) # Output: True
|
||||
|
||||
expression4 = "not (a > b or z['b'] == 3)"
|
||||
result4 = safe_eval(expression4, variables)
|
||||
print(result4) # Output: True
|
||||
|
||||
expression5 = "abs(-10) + len(x)"
|
||||
result5 = safe_eval(expression5, variables)
|
||||
print(result5) # Output: 20
|
||||
|
||||
expression6 = "math.sqrt(16)"
|
||||
result6 = safe_eval(expression6, variables)
|
||||
print(result6) # Output: 4.0
|
||||
|
||||
expression7 = "{'key1': 1, 'key2': 2}['key1'] + [1, 2, 3][1]"
|
||||
result7 = safe_eval(expression7, variables)
|
||||
print(result7) # Output: 3
|
||||
|
||||
expression8 = "[i * 2 for i in range(5)]"
|
||||
result8 = safe_eval(expression8, variables)
|
||||
print(result8) # Output: [0, 2, 4, 6, 8]
|
||||
|
||||
expression9 = "[i * 2 for i in range(5) if i % 2 == 0]"
|
||||
result9 = safe_eval(expression9, variables)
|
||||
print(result9) # Output: [0, 4, 8]
|
||||
|
||||
expression10 = "[[i * j for j in range(3)] for i in range(3)]"
|
||||
result10 = safe_eval(expression10, variables)
|
||||
print(result10) # Output: [[0, 0, 0], [0, 1, 2], [0, 2, 4]]
|
||||
|
||||
expression11 = "3 if a < b else 4"
|
||||
result11 = safe_eval(expression11, variables)
|
||||
print(result11) # Output: 3
|
||||
|
||||
expression12 = "sorted([3, 1, 2])"
|
||||
result12 = safe_eval(expression12, variables)
|
||||
print(result12) # Output: [1, 2, 3]
|
||||
|
||||
expression13 = "list(reversed([1, 2, 3]))"
|
||||
result13 = safe_eval(expression13, variables)
|
||||
print(result13) # Output: [3, 2, 1]
|
||||
|
||||
expression14 = "list(map(lambda x: x * 2, [1, 2, 3]))"
|
||||
result14 = safe_eval(expression14, variables)
|
||||
print(result14) # Output: [2, 4, 6]
|
||||
|
||||
expression15 = "list(filter(lambda x: x % 2 == 0, [1, 2, 3, 4]))"
|
||||
result15 = safe_eval(expression15, variables)
|
||||
print(result15) # Output: [2, 4]
|
||||
|
||||
expression16 = "all([True, True, False])"
|
||||
result16 = safe_eval(expression16, variables)
|
||||
print(result16) # Output: False
|
||||
|
||||
expression17 = "any([False, False, True])"
|
||||
result17 = safe_eval(expression17, variables)
|
||||
print(result17) # Output: True
|
||||
|
||||
expression18 = "list(zip([1, 2], ['a', 'b']))"
|
||||
result18 = safe_eval(expression18, variables)
|
||||
print(result18) # Output: [(1, 'a'), (2, 'b')]
|
||||
|
||||
expression19 = "list(enumerate(['a', 'b', 'c']))"
|
||||
result19 = safe_eval(expression19, variables)
|
||||
print(result19) # Output: [(0, 'a'), (1, 'b'), (2, 'c')]
|
||||
|
||||
# Example with additional functions
|
||||
additional_functions = {
|
||||
'custom_func': lambda x: x * 2
|
||||
}
|
||||
expression20 = "custom_func(5)"
|
||||
result20 = safe_eval(expression20, variables, additional_functions)
|
||||
print(result20) # Output: 10
|
||||
|
||||
# Example with bitwise inversion
|
||||
expression21 = "~5"
|
||||
result21 = safe_eval(expression21, variables)
|
||||
print(result21) # Output: -6
|
||||
|
||||
# Example with math.pi
|
||||
expression22 = "math.pi"
|
||||
result22 = safe_eval(expression22, variables)
|
||||
print(result22) # Output: 3.141592653589793
|
||||
|
||||
# Example with inline if assignment
|
||||
expression23 = "x = 10 if a < b else 20"
|
||||
safe_eval(expression23, variables)
|
||||
print(variables['x']) # Output: 10
|
||||
|
||||
# Example with walrus operator
|
||||
expression24 = "(y := 10) + 5"
|
||||
result24 = safe_eval(expression24, variables)
|
||||
print(result24) # Output: 15
|
||||
print(variables['y']) # Output: 10
|
||||
|
||||
# Example with dictionary merging
|
||||
expression25 = "{'a': 1} | {'b': 2}"
|
||||
result25 = safe_eval(expression25, variables)
|
||||
print(result25) # Output: {'a': 1, 'b': 2}
|
||||
|
||||
# Example with random functions
|
||||
expression26 = "randrange(1, 10)"
|
||||
result26 = safe_eval(expression26, variables)
|
||||
print(result26) # Output: Random number between 1 and 9
|
||||
|
||||
expression27 = "choice(['apple', 'banana', 'cherry'])"
|
||||
result27 = safe_eval(expression27, variables)
|
||||
print(result27) # Output: Randomly chosen fruit from the list
|
||||
|
||||
# Example with short-circuiting
|
||||
variables.update({'a': None, 'b': 7})
|
||||
expression28 = "False if a is None else a if a < b else False"
|
||||
result28 = safe_eval(expression28, variables)
|
||||
print(result28) # Output: False
|
||||
|
||||
# Example with multiple variable assignment
|
||||
expression29 = "a, b, c, d, e = 0, 1, 2, 3, 4"
|
||||
safe_eval(expression29, variables)
|
||||
print(variables['a'], variables['b'], variables['c'], variables['d'], variables['e']) # Output: 0 1 2 3 4
|
||||
|
||||
# Example with walrus operator and multiple variable assignment
|
||||
expression30 = "(a, b, c, d, e := 0, 1, 2, 3, 4)"
|
||||
safe_eval(expression30, variables)
|
||||
print(variables['a'], variables['b'], variables['c'], variables['d'], variables['e']) # Output: 0 1 2 3 4
|
||||
|
||||
# Example with logical short-circuiting
|
||||
variables.update({'A': False, 'B': True, 'C': 'Short-circuited'})
|
||||
expression31 = "A and B or C"
|
||||
result31 = safe_eval(expression31, variables)
|
||||
print(result31) # Output: 'Short-circuited'
|
||||
"""
|
||||
@@ -0,0 +1,261 @@
|
||||
import ast
|
||||
import operator
|
||||
import math
|
||||
|
||||
# Define supported operators
|
||||
operators = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.Pow: operator.pow,
|
||||
ast.BitXor: operator.xor,
|
||||
ast.USub: operator.neg,
|
||||
ast.Eq: operator.eq,
|
||||
ast.NotEq: operator.ne,
|
||||
ast.Lt: operator.lt,
|
||||
ast.LtE: operator.le,
|
||||
ast.Gt: operator.gt,
|
||||
ast.GtE: operator.ge,
|
||||
ast.And: operator.and_,
|
||||
ast.Or: operator.or_,
|
||||
ast.Not: operator.not_
|
||||
}
|
||||
|
||||
# Define supported functions
|
||||
default_functions = {
|
||||
'abs': abs,
|
||||
'len': len,
|
||||
'max': max,
|
||||
'min': min,
|
||||
'sum': sum,
|
||||
'round': round,
|
||||
'range': range,
|
||||
'sorted': sorted,
|
||||
'reversed': reversed,
|
||||
'map': map,
|
||||
'filter': filter,
|
||||
'all': all,
|
||||
'any': any,
|
||||
'zip': zip,
|
||||
'enumerate': enumerate,
|
||||
'math': math
|
||||
}
|
||||
|
||||
def safe_eval(expr, variables=None, additional_functions=None):
|
||||
"""
|
||||
Safely evaluate a mathematical expression with named variables, including list and dictionary indexing,
|
||||
logical operators, predefined function calls, list comprehensions, and conditionals.
|
||||
|
||||
:param expr: The expression to evaluate as a string.
|
||||
:param variables: A dictionary of variable names and their values.
|
||||
:param additional_functions: A dictionary of additional functions to support.
|
||||
:return: The result of the evaluated expression.
|
||||
"""
|
||||
if variables is None:
|
||||
variables = {}
|
||||
|
||||
if additional_functions is None:
|
||||
additional_functions = {}
|
||||
|
||||
# Merge default functions with additional functions
|
||||
functions = {**default_functions, **additional_functions}
|
||||
|
||||
# Parse expression into AST
|
||||
node = ast.parse(expr, mode='eval')
|
||||
|
||||
def _eval(node):
|
||||
if isinstance(node, ast.Expression):
|
||||
return _eval(node.body)
|
||||
elif isinstance(node, ast.BinOp):
|
||||
left = _eval(node.left)
|
||||
right = _eval(node.right)
|
||||
return operators[type(node.op)](left, right)
|
||||
elif isinstance(node, ast.UnaryOp):
|
||||
operand = _eval(node.operand)
|
||||
return operators[type(node.op)](operand)
|
||||
elif isinstance(node, ast.BoolOp):
|
||||
values = [_eval(v) for v in node.values]
|
||||
if isinstance(node.op, ast.And):
|
||||
return all(values)
|
||||
elif isinstance(node.op, ast.Or):
|
||||
return any(values)
|
||||
elif isinstance(node, ast.Compare):
|
||||
left = _eval(node.left)
|
||||
for operation, comparator in zip(node.ops, node.comparators):
|
||||
right = _eval(comparator)
|
||||
if not operators[type(operation)](left, right):
|
||||
return False
|
||||
left = right
|
||||
return True
|
||||
elif isinstance(node, ast.Num): # For Python 3.8 and earlier
|
||||
return node.n
|
||||
elif isinstance(node, ast.Constant): # For Python 3.8 and later
|
||||
return node.value
|
||||
elif isinstance(node, ast.Name):
|
||||
if node.id in variables:
|
||||
return variables[node.id]
|
||||
elif node.id in functions:
|
||||
return functions[node.id]
|
||||
elif node.id in {'True', 'False', 'None'}:
|
||||
return {'True': True, 'False': False, 'None': None}[node.id]
|
||||
else:
|
||||
raise NameError(f"Variable '{node.id}' is not defined")
|
||||
elif isinstance(node, ast.Subscript):
|
||||
value = _eval(node.value)
|
||||
index = _eval(node.slice)
|
||||
return value[index]
|
||||
elif isinstance(node, ast.Index): # For Python 3.8 and earlier
|
||||
return _eval(node.value)
|
||||
elif isinstance(node, ast.Slice):
|
||||
lower = _eval(node.lower) if node.lower else None
|
||||
upper = _eval(node.upper) if node.upper else None
|
||||
step = _eval(node.step) if node.step else None
|
||||
return slice(lower, upper, step)
|
||||
elif isinstance(node, ast.Tuple):
|
||||
return tuple(_eval(elt) for elt in node.elts)
|
||||
elif isinstance(node, ast.List):
|
||||
return [_eval(elt) for elt in node.elts]
|
||||
elif isinstance(node, ast.Dict):
|
||||
return {_eval(key): _eval(value) for key, value in zip(node.keys, node.values)}
|
||||
elif isinstance(node, ast.Call):
|
||||
func = _eval(node.func)
|
||||
args = [_eval(arg) for arg in node.args]
|
||||
if func in functions.values() or callable(func):
|
||||
return func(*args)
|
||||
else:
|
||||
raise TypeError(f"Unsupported function: {func}")
|
||||
elif isinstance(node, ast.Attribute):
|
||||
value = _eval(node.value)
|
||||
if value in functions.values():
|
||||
return getattr(value, node.attr)
|
||||
else:
|
||||
raise AttributeError(f"Access to attribute '{node.attr}' is not allowed")
|
||||
elif isinstance(node, ast.IfExp):
|
||||
test = _eval(node.test)
|
||||
body = _eval(node.body)
|
||||
orelse = _eval(node.orelse)
|
||||
return body if test else orelse
|
||||
elif isinstance(node, ast.ListComp):
|
||||
elt = node.elt
|
||||
generators = node.generators
|
||||
return _eval_listcomp(elt, generators)
|
||||
else:
|
||||
raise TypeError(f"Unsupported type: {type(node)}")
|
||||
|
||||
def _eval_listcomp(elt, generators):
|
||||
"""
|
||||
Evaluate a list comprehension.
|
||||
|
||||
:param elt: The element expression of the list comprehension.
|
||||
:param generators: The generators of the list comprehension.
|
||||
:return: The evaluated list comprehension.
|
||||
"""
|
||||
if not generators:
|
||||
return [_eval(elt)]
|
||||
|
||||
gen = generators[0]
|
||||
iter_ = _eval(gen.iter)
|
||||
result = []
|
||||
|
||||
for item in iter_:
|
||||
new_variables = variables.copy()
|
||||
new_variables[gen.target.id] = item
|
||||
if all(_eval(cond) for cond in gen.ifs):
|
||||
result.extend(_eval_listcomp(elt, generators[1:]))
|
||||
|
||||
return result
|
||||
|
||||
return _eval(node.body)
|
||||
|
||||
# Example usage:
|
||||
variables = {
|
||||
'x': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
'y': 5,
|
||||
'z': {'a': 1, 'b': 2},
|
||||
'a': 3,
|
||||
'b': 4
|
||||
}
|
||||
expression1 = "x[y] + 2 ** 3"
|
||||
result1 = safe_eval(expression1, variables)
|
||||
print(result1) # Output: 13
|
||||
|
||||
expression2 = "z['a'] + z['b']"
|
||||
result2 = safe_eval(expression2, variables)
|
||||
print(result2) # Output: 3
|
||||
|
||||
expression3 = "a < b and z['a'] == 1"
|
||||
result3 = safe_eval(expression3, variables)
|
||||
print(result3) # Output: True
|
||||
|
||||
expression4 = "not (a > b or z['b'] == 3)"
|
||||
result4 = safe_eval(expression4, variables)
|
||||
print(result4) # Output: True
|
||||
|
||||
expression5 = "abs(-10) + len(x)"
|
||||
result5 = safe_eval(expression5, variables)
|
||||
print(result5) # Output: 20
|
||||
|
||||
expression6 = "math.sqrt(16)"
|
||||
result6 = safe_eval(expression6, variables)
|
||||
print(result6) # Output: 4.0
|
||||
|
||||
expression7 = "{'key1': 1, 'key2': 2}['key1'] + [1, 2, 3][1]"
|
||||
result7 = safe_eval(expression7, variables)
|
||||
print(result7) # Output: 3
|
||||
|
||||
expression8 = "[i * 2 for i in range(5)]"
|
||||
result8 = safe_eval(expression8, variables)
|
||||
print(result8) # Output: [0, 2, 4, 6, 8]
|
||||
|
||||
expression9 = "[i * 2 for i in range(5) if i % 2 == 0]"
|
||||
result9 = safe_eval(expression9, variables)
|
||||
print(result9) # Output: [0, 4, 8]
|
||||
|
||||
expression10 = "[[i * j for j in range(3)] for i in range(3)]"
|
||||
result10 = safe_eval(expression10, variables)
|
||||
print(result10) # Output: [[0, 0, 0], [0, 1, 2], [0, 2, 4]]
|
||||
|
||||
expression11 = "3 if a < b else 4"
|
||||
result11 = safe_eval(expression11, variables)
|
||||
print(result11) # Output: 3
|
||||
|
||||
expression12 = "sorted([3, 1, 2])"
|
||||
result12 = safe_eval(expression12, variables)
|
||||
print(result12) # Output: [1, 2, 3]
|
||||
|
||||
expression13 = "list(reversed([1, 2, 3]))"
|
||||
result13 = safe_eval(expression13, variables)
|
||||
print(result13) # Output: [3, 2, 1]
|
||||
|
||||
expression14 = "list(map(lambda x: x * 2, [1, 2, 3]))"
|
||||
result14 = safe_eval(expression14, variables)
|
||||
print(result14) # Output: [2, 4, 6]
|
||||
|
||||
expression15 = "list(filter(lambda x: x % 2 == 0, [1, 2, 3, 4]))"
|
||||
result15 = safe_eval(expression15, variables)
|
||||
print(result15) # Output: [2, 4]
|
||||
|
||||
expression16 = "all([True, True, False])"
|
||||
result16 = safe_eval(expression16, variables)
|
||||
print(result16) # Output: False
|
||||
|
||||
expression17 = "any([False, False, True])"
|
||||
result17 = safe_eval(expression17, variables)
|
||||
print(result17) # Output: True
|
||||
|
||||
expression18 = "list(zip([1, 2], ['a', 'b']))"
|
||||
result18 = safe_eval(expression18, variables)
|
||||
print(result18) # Output: [(1, 'a'), (2, 'b')]
|
||||
|
||||
expression19 = "list(enumerate(['a', 'b', 'c']))"
|
||||
result19 = safe_eval(expression19, variables)
|
||||
print(result19) # Output: [(0, 'a'), (1, 'b'), (2, 'c')]
|
||||
|
||||
# Example with additional functions
|
||||
additional_functions = {
|
||||
'custom_func': lambda x: x * 2
|
||||
}
|
||||
expression20 = "custom_func(5)"
|
||||
result20 = safe_eval(expression20, variables, additional_functions)
|
||||
print(result20) # Output: 10
|
||||
@@ -14,45 +14,55 @@ from . import ControlFlowUtils
|
||||
any_type = Types.AnyType("*")
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Cycle": ControlFlowUtils.Cycle,
|
||||
"CycleEnd": ControlFlowUtils.CycleEnd,
|
||||
"CycleStart": ControlFlowUtils.CycleStart,
|
||||
"DataMonitor": ControlFlowUtils.DataMonitor,
|
||||
"CheckpointSelector": ControlFlowUtils.CheckpointSelector,
|
||||
"LoraSelector": ControlFlowUtils.LoraSelector,
|
||||
"VAESelector": ControlFlowUtils.VAESelector,
|
||||
"NullInput": ControlFlowUtils.NullInput,
|
||||
"NullOutput": ControlFlowUtils.NullOutput,
|
||||
"ImageResolutionAdjust": ControlFlowUtils.ImageResolutionAdjust,
|
||||
"ReadTextFile": ControlFlowUtils.ReadTextFile,
|
||||
"SaveTextFile": ControlFlowUtils.SaveTextFile,
|
||||
"Wait": ControlFlowUtils.DelayExecution,
|
||||
"GarbageCollector": ControlFlowUtils.GarbageCollector,
|
||||
"UnloadModels": ControlFlowUtils.UnloadModels,
|
||||
"IfConditionSelector": ControlFlowUtils.IfConditionSelector,
|
||||
"UniversalSwitch": ControlFlowUtils.UniversalSwitch,
|
||||
"HaltExecution": ControlFlowUtils.HaltExecution,
|
||||
"MemoryStorage": ControlFlowUtils.MemoryStorage
|
||||
"DataMonitor": ControlFlowUtils.DataMonitor,
|
||||
"MemoryStorage": ControlFlowUtils.MemoryStorage,
|
||||
"StringOperation": ControlFlowUtils.StringOperation,
|
||||
"IfConditionSelector": ControlFlowUtils.IfConditionSelector,
|
||||
"UniversalSwitch": ControlFlowUtils.UniversalSwitch,
|
||||
"Cycle": ControlFlowUtils.Cycle,
|
||||
"CycleContinue": ControlFlowUtils.CycleContinue,
|
||||
"CycleEnd": ControlFlowUtils.CycleEnd,
|
||||
"NullOutput": ControlFlowUtils.NullOutput,
|
||||
"NullInput": ControlFlowUtils.NullInput,
|
||||
"SimpleToggle": ControlFlowUtils.SimpleToggle,
|
||||
"InvertCondition": ControlFlowUtils.InvertCondition,
|
||||
"Wait": ControlFlowUtils.DelayExecution,
|
||||
"HaltExecution": ControlFlowUtils.HaltExecution,
|
||||
"GarbageCollector": ControlFlowUtils.GarbageCollector,
|
||||
"UnloadModels": ControlFlowUtils.UnloadModels,
|
||||
"ImageResolutionAdjust": ControlFlowUtils.ImageResolutionAdjust,
|
||||
"FallbackImagePreviewer": ControlFlowUtils.FallbackImagePreviewer,
|
||||
"FolderSearch": ControlFlowUtils.FolderSearch,
|
||||
"ReadTextFile": ControlFlowUtils.ReadTextFile,
|
||||
"SaveTextFile": ControlFlowUtils.SaveTextFile,
|
||||
"CheckpointSelector": ControlFlowUtils.CheckpointSelector,
|
||||
"LoraSelector": ControlFlowUtils.LoraSelector,
|
||||
"VAESelector": ControlFlowUtils.VAESelector,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Cycle": "🔄 Cycle",
|
||||
"CycleEnd": "⏪ Cycle End",
|
||||
"CycleStart": "⏩ Cycle Start",
|
||||
"DataMonitor": "👁🗨Data Monitor/Generator",
|
||||
"CheckpointSelector": "🏁 Checkpoint Selector",
|
||||
"LoraSelector": "🏴 LoRA Selector",
|
||||
"VAESelector": "🚩 VAE Selector",
|
||||
"NullInput": "🟦 Null Input",
|
||||
"NullOutput": "🔵 Null Output",
|
||||
"ImageResolutionAdjust": "🌄 Image Resolution Adjust",
|
||||
"ReadTextFile": "📄 Read Text File",
|
||||
"SaveTextFile": "💾 Save Text File",
|
||||
"Wait": "⌛ Wait",
|
||||
"GarbageCollector": "🗑 Garbage Collector",
|
||||
"UnloadModels": "❌ Unload Models",
|
||||
"IfConditionSelector": "🔀IF (Condition Selector)",
|
||||
"UniversalSwitch": "💠 Universal Switch",
|
||||
"HaltExecution": "🛑 Halt Execution",
|
||||
"MemoryStorage": "🗒️ Memory Storage",
|
||||
"DataMonitor": "👁🗨Data Monitor ⁄ Generator ⁄ Converter",
|
||||
"MemoryStorage": "🗒️ Memory Storage",
|
||||
"StringOperation": "🔠 String ⁄ List Operations",
|
||||
"IfConditionSelector": "🔀 IF (Condition Selector)",
|
||||
"UniversalSwitch": "💠 Universal Switch",
|
||||
"Cycle": "🔄 Cycle",
|
||||
"CycleContinue": "⏩ Cycle Continue",
|
||||
"CycleEnd": "⏪ Cycle End",
|
||||
"NullOutput": "🔵 Null Output",
|
||||
"NullInput": "🟦 Null Input",
|
||||
"SimpleToggle": "🔶 Simple Toggle",
|
||||
"InvertCondition": "🚫 NOT (Invert Condition)",
|
||||
"Wait": "⌛ Wait",
|
||||
"HaltExecution": "🛑 Halt Execution",
|
||||
"GarbageCollector": "🗑 Garbage Collector",
|
||||
"UnloadModels": "❌ Unload Models",
|
||||
"ImageResolutionAdjust": "🌄 Image Resolution Adjust",
|
||||
"FallbackImagePreviewer": "🖼️ Fallback Image Previewer",
|
||||
"FolderSearch": "📁 Folder Search",
|
||||
"ReadTextFile": "📄 Read Text File",
|
||||
"SaveTextFile": "💾 Save Text File",
|
||||
"CheckpointSelector": "🏁 Checkpoint Selector",
|
||||
"LoraSelector": "🏴 LoRA Selector",
|
||||
"VAESelector": "🚩 VAE Selector",
|
||||
}
|
||||
@@ -4,12 +4,34 @@ import { $el } from "../../scripts/ui.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
|
||||
// Let it be known that I barely know what I'm doing with Javascript, so any help would be greatly appreciated
|
||||
// ~ VykosX
|
||||
// ~ VykosX
|
||||
|
||||
const stopAll = async function(interrupt){
|
||||
document.querySelector("div.comfyui-queue-mode input:first-child").click();
|
||||
|
||||
await fetch("/queue", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ clear: true }),
|
||||
}).catch(e => console.log("exception while clearing queue:",e));
|
||||
|
||||
if (interrupt) {
|
||||
|
||||
console.log ("Interrupting prompt...")
|
||||
|
||||
await fetch("/api/interrupt", {
|
||||
method: "POST",
|
||||
}).catch(e => console.log("exception while interrupting prompt:",e));
|
||||
}
|
||||
};
|
||||
|
||||
app.registerExtension({
|
||||
name: "ControlFlowUtils",
|
||||
|
||||
async setup(app) {
|
||||
async setup(app) {
|
||||
|
||||
api.addEventListener("VykosX.UnloadModels", ( {node_id} ) => {
|
||||
|
||||
@@ -25,39 +47,301 @@ app.registerExtension({
|
||||
|
||||
});
|
||||
|
||||
api.addEventListener("VykosX.ClearQueue", ( {} ) => {
|
||||
api.addEventListener("VykosX.ClearQueue", ({detail}) => {
|
||||
|
||||
console.log ("Clearing queue...");
|
||||
|
||||
//app.ui.queue.clear();
|
||||
let { interrupt } = detail;
|
||||
stopAll(interrupt);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
|
||||
//console.log ("NODE FOUND:",nodeData.name,nodeData.id);
|
||||
|
||||
if (nodeData.name === "HaltExecution") {
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
|
||||
this.showValueWidget = ComfyWidgets["STRING"](this, "Status", ["STRING", { multiline: true }], app).widget;
|
||||
this.showValueWidget.inputEl.readOnly = true;
|
||||
this.showValueWidget.serializeValue = async (node, index) => {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]);
|
||||
this.showValueWidget.value = message.text.join("");
|
||||
};
|
||||
}
|
||||
|
||||
if (nodeData.name === "MemoryStorage") {
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
|
||||
this.showValueWidget = ComfyWidgets["STRING"](this, "Value", ["STRING", { multiline: true }], app).widget;
|
||||
this.showValueWidget.inputEl.readOnly = true;
|
||||
this.showValueWidget.serializeValue = async (node, index) => {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]);
|
||||
this.showValueWidget.value = "Value: " + message.text.join("");
|
||||
};
|
||||
}
|
||||
if (nodeData.name === "DataMonitor") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = onNodeCreated?.apply(this, arguments);
|
||||
return r;
|
||||
};
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
|
||||
if (nodeData.name === "DataMonitor") {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = onNodeCreated?.apply(this, arguments);
|
||||
return r;
|
||||
};
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
|
||||
onExecuted?.apply(this, arguments);
|
||||
|
||||
const onExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments);
|
||||
|
||||
for (const widget of this.widgets) {
|
||||
if (widget.type === "customtext") {
|
||||
widget.value = message.text.join("");
|
||||
}
|
||||
}
|
||||
|
||||
this.onResize?.(this.size);
|
||||
};
|
||||
}
|
||||
for (const widget of this.widgets) {
|
||||
if (widget.type === "customtext") {
|
||||
|
||||
widget.value = message.text.join("");
|
||||
}
|
||||
}
|
||||
this.onResize?.(this.size);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
// ALL SUBSEQUENT CODE BELONGS TO PYTHONGOSSSSSS AND HIS EXCELLENT COMFYUI-CUSTOM-SCRIPTS PACKAGE!
|
||||
|
||||
const PathHelper = {
|
||||
get(obj, path) {
|
||||
if (typeof path !== "string") {
|
||||
// Hardcoded value
|
||||
return path;
|
||||
}
|
||||
|
||||
if (path[0] === '"' && path[path.length - 1] === '"') {
|
||||
// Hardcoded string
|
||||
return JSON.parse(path);
|
||||
}
|
||||
|
||||
// Evaluate the path
|
||||
path = path.split(".").filter(Boolean);
|
||||
for (const p of path) {
|
||||
const k = isNaN(+p) ? p : +p;
|
||||
obj = obj[k];
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
set(obj, path, value) {
|
||||
// https://stackoverflow.com/a/54733755
|
||||
if (Object(obj) !== obj) return obj; // When obj is not an object
|
||||
// If not yet an array, get the keys from the string-path
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path.slice(0, -1).reduce(
|
||||
(
|
||||
a,
|
||||
c,
|
||||
i // Iterate all of them except the last one
|
||||
) =>
|
||||
Object(a[c]) === a[c] // Does the key exist and is its value an object?
|
||||
? // Yes: then follow that path
|
||||
a[c]
|
||||
: // No: create the key. Is the next key a potential array-index?
|
||||
(a[c] =
|
||||
Math.abs(path[i + 1]) >> 0 === +path[i + 1]
|
||||
? [] // Yes: assign a new array object
|
||||
: {}), // No: assign a new plain object
|
||||
obj
|
||||
)[path[path.length - 1]] = value; // Finally assign the value to the last key
|
||||
return obj; // Return the top-level object to allow chaining
|
||||
},
|
||||
};
|
||||
|
||||
/***
|
||||
@typedef { {
|
||||
left: string;
|
||||
op: "eq" | "ne",
|
||||
right: string
|
||||
} } IfCondition
|
||||
|
||||
@typedef { {
|
||||
type: "if",
|
||||
condition: Array<IfCondition>,
|
||||
true?: Array<BindingCallback>,
|
||||
false?: Array<BindingCallback>
|
||||
} } IfCallback
|
||||
|
||||
@typedef { {
|
||||
type: "fetch",
|
||||
url: string,
|
||||
then: Array<BindingCallback>
|
||||
} } FetchCallback
|
||||
|
||||
@typedef { {
|
||||
type: "set",
|
||||
target: string,
|
||||
value: string
|
||||
} } SetCallback
|
||||
|
||||
@typedef { {
|
||||
type: "validate-combo",
|
||||
} } ValidateComboCallback
|
||||
|
||||
@typedef { IfCallback | FetchCallback | SetCallback | ValidateComboCallback } BindingCallback
|
||||
|
||||
@typedef { {
|
||||
source: string,
|
||||
callback: Array<BindingCallback>
|
||||
} } Binding
|
||||
***/
|
||||
|
||||
/**
|
||||
* @param {IfCondition} condition
|
||||
*/
|
||||
function evaluateCondition(condition, state) {
|
||||
const left = PathHelper.get(state, condition.left);
|
||||
const right = PathHelper.get(state, condition.right);
|
||||
|
||||
let r;
|
||||
if (condition.op === "eq") {
|
||||
r = left === right;
|
||||
} else {
|
||||
r = left !== right;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type { Record<BindingCallback["type"], (cb: any, state: Record<string, any>) => Promise<void>> }
|
||||
*/
|
||||
const callbacks = {
|
||||
/**
|
||||
* @param {IfCallback} cb
|
||||
*/
|
||||
async if(cb, state) {
|
||||
// For now only support ANDs
|
||||
let success = true;
|
||||
for (const condition of cb.condition) {
|
||||
const r = evaluateCondition(condition, state);
|
||||
if (!r) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const m of cb[success + ""] ?? []) {
|
||||
await invokeCallback(m, state);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {FetchCallback} cb
|
||||
*/
|
||||
async fetch(cb, state) {
|
||||
const url = cb.url.replace(/\{([^\}]+)\}/g, (m, v) => {
|
||||
return PathHelper.get(state, v);
|
||||
});
|
||||
const res = await (await api.fetchApi(url)).json();
|
||||
state["$result"] = res;
|
||||
for (const m of cb.then) {
|
||||
await invokeCallback(m, state);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @param {SetCallback} cb
|
||||
*/
|
||||
async set(cb, state) {
|
||||
const value = PathHelper.get(state, cb.value);
|
||||
PathHelper.set(state, cb.target, value);
|
||||
},
|
||||
async "validate-combo"(cb, state) {
|
||||
const w = state["$this"];
|
||||
const valid = w.options.values.includes(w.value);
|
||||
if (!valid) {
|
||||
w.value = w.options.values[0];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function invokeCallback(callback, state) {
|
||||
if (callback.type in callbacks) {
|
||||
// @ts-ignore
|
||||
await callbacks[callback.type](callback, state);
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "vykosx.binding",
|
||||
beforeRegisterNodeDef(node, nodeData) {
|
||||
const hasBinding = (v) => {
|
||||
if (!v) return false;
|
||||
return Object.values(v).find((c) => c[1]?.["vykosx.binding"]);
|
||||
};
|
||||
const inputs = { ...nodeData.input?.required, ...nodeData.input?.optional };
|
||||
if (hasBinding(inputs)) {
|
||||
const onAdded = node.prototype.onAdded;
|
||||
node.prototype.onAdded = function () {
|
||||
const r = onAdded?.apply(this, arguments);
|
||||
|
||||
for (const widget of this.widgets || []) {
|
||||
const bindings = inputs[widget.name][1]?.["vykosx.binding"];
|
||||
if (!bindings) continue;
|
||||
|
||||
for (const binding of bindings) {
|
||||
|
||||
const source = this.widgets.find((w) => w.name === binding.source);
|
||||
if (!source) {
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let lastValue;
|
||||
async function valueChanged() {
|
||||
const state = {
|
||||
$this: widget,
|
||||
$source: source,
|
||||
$node: node,
|
||||
};
|
||||
|
||||
for (const callback of binding.callback) {
|
||||
await invokeCallback(callback, state);
|
||||
}
|
||||
|
||||
app.graph.setDirtyCanvas(true, false);
|
||||
}
|
||||
|
||||
const cb = source.callback;
|
||||
source.callback = function () {
|
||||
const v = cb?.apply(this, arguments) ?? source.value;
|
||||
if (v !== lastValue) {
|
||||
lastValue = v;
|
||||
valueChanged();
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
lastValue = source.value;
|
||||
valueChanged();
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
// code borrowed from save-image-extended-comfyui https://github.com/thedyze/save-image-extended-comfyui
|
||||
|
||||
const categories = ["🐺 VykosX"];
|
||||
app.registerExtension({
|
||||
name: "VykosX.HelpPopup",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
// console.log(`nodeType: ${nodeType}`)
|
||||
// console.log(`nodeType: ${nodeData}`)
|
||||
|
||||
try {
|
||||
categories.forEach(category => {
|
||||
if (nodeData?.category?.startsWith(category)) {
|
||||
addDocumentation(nodeData, nodeType);
|
||||
}
|
||||
else return
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in registering VykosX.HelpPopup", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const create_documentation_stylesheet = () => {
|
||||
const tag = 'sie-documentation-stylesheet'
|
||||
|
||||
let styleTag = document.head.querySelector(tag)
|
||||
|
||||
// color: var(--fg-color); will be black even in dark mode... bug but i can't tell where it comes from
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style')
|
||||
styleTag.type = 'text/css'
|
||||
styleTag.id = tag
|
||||
styleTag.innerHTML = `
|
||||
.sie-documentation-popup {
|
||||
background: var(--comfy-menu-bg);
|
||||
position: absolute;
|
||||
color: white;
|
||||
font: 12px monospace;
|
||||
line-height: 1.5em;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border-style: solid;
|
||||
border-width: medium;
|
||||
border-color: var(--border-color);
|
||||
z-index: 5;
|
||||
overflow: hidden;
|
||||
}
|
||||
.content-wrapper {
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
/* Scrollbar styling for Chrome */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--fg-color);
|
||||
border-radius: 6px;
|
||||
border: 3px solid var(--bg-color);
|
||||
}
|
||||
|
||||
/* Scrollbar styling for Firefox */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--fg-color) var(--bg-color);
|
||||
a {
|
||||
color: yellow;
|
||||
}
|
||||
a:visited {
|
||||
color: orange;
|
||||
}
|
||||
a:hover {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
`
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
}
|
||||
|
||||
/** Add documentation widget to the selected node */
|
||||
export const addDocumentation = (
|
||||
nodeData,
|
||||
nodeType,
|
||||
opts = { icon_size: 14, icon_margin: 4 },) => {
|
||||
|
||||
opts = opts || {}
|
||||
const iconSize = opts.icon_size ? opts.icon_size : 14
|
||||
const iconMargin = opts.icon_margin ? opts.icon_margin : 4
|
||||
let docElement = null
|
||||
let contentWrapper = null
|
||||
//if no description in the node python code, don't do anything
|
||||
if (!nodeData.description) {
|
||||
return
|
||||
}
|
||||
|
||||
const drawFg = nodeType.prototype.onDrawForeground
|
||||
nodeType.prototype.onDrawForeground = function (ctx) {
|
||||
const r = drawFg ? drawFg.apply(this, arguments) : undefined
|
||||
if (this.flags.collapsed) return r
|
||||
|
||||
// icon position
|
||||
const x = this.size[0] - iconSize - iconMargin
|
||||
|
||||
// create the popup
|
||||
if (this.show_doc && docElement === null) {
|
||||
docElement = document.createElement('div')
|
||||
contentWrapper = document.createElement('div');
|
||||
docElement.appendChild(contentWrapper);
|
||||
|
||||
create_documentation_stylesheet()
|
||||
contentWrapper.classList.add('content-wrapper');
|
||||
docElement.classList.add('sie-documentation-popup')
|
||||
|
||||
//parse the string from the python node code to html with marked, and sanitize the html with DOMPurify
|
||||
contentWrapper.innerHTML = DOMPurify.sanitize(marked.parse(nodeData.description,))
|
||||
|
||||
// resize handle
|
||||
const resizeHandle = document.createElement('div');
|
||||
resizeHandle.style.width = '0';
|
||||
resizeHandle.style.height = '0';
|
||||
resizeHandle.style.position = 'absolute';
|
||||
resizeHandle.style.bottom = '0';
|
||||
resizeHandle.style.right = '0';
|
||||
resizeHandle.style.cursor = 'se-resize';
|
||||
|
||||
// Add pseudo-elements to create a triangle shape
|
||||
const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border-color').trim();
|
||||
resizeHandle.style.borderTop = '10px solid transparent';
|
||||
resizeHandle.style.borderLeft = '10px solid transparent';
|
||||
resizeHandle.style.borderBottom = `10px solid ${borderColor}`;
|
||||
resizeHandle.style.borderRight = `10px solid ${borderColor}`;
|
||||
|
||||
docElement.appendChild(resizeHandle)
|
||||
let isResizing = false
|
||||
let startX, startY, startWidth, startHeight
|
||||
|
||||
resizeHandle.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
isResizing = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
startWidth = parseInt(document.defaultView.getComputedStyle(docElement).width, 10);
|
||||
startHeight = parseInt(document.defaultView.getComputedStyle(docElement).height, 10);
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
// close button
|
||||
const closeButton = document.createElement('div');
|
||||
closeButton.textContent = '❌';
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.top = '0';
|
||||
closeButton.style.right = '0';
|
||||
closeButton.style.cursor = 'pointer';
|
||||
closeButton.style.padding = '5px';
|
||||
closeButton.style.color = 'red';
|
||||
closeButton.style.fontSize = '12px';
|
||||
|
||||
docElement.appendChild(closeButton)
|
||||
|
||||
closeButton.addEventListener('mousedown', (e) => {
|
||||
e.stopPropagation();
|
||||
this.show_doc = !this.show_doc
|
||||
docElement.parentNode.removeChild(docElement)
|
||||
docElement = null
|
||||
if (contentWrapper) {
|
||||
contentWrapper.remove()
|
||||
contentWrapper = null
|
||||
}
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (!isResizing) return;
|
||||
const scale = app.canvas.ds.scale;
|
||||
const newWidth = startWidth + (e.clientX - startX) / scale;
|
||||
const newHeight = startHeight + (e.clientY - startY) / scale;;
|
||||
docElement.style.width = `${newWidth}px`;
|
||||
docElement.style.height = `${newHeight}px`;
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
);
|
||||
|
||||
document.addEventListener('mouseup', function () {
|
||||
isResizing = false
|
||||
},
|
||||
{ signal: this.docCtrl.signal },
|
||||
)
|
||||
|
||||
document.body.appendChild(docElement)
|
||||
}
|
||||
// close the popup
|
||||
else if (!this.show_doc && docElement !== null) {
|
||||
docElement.parentNode.removeChild(docElement)
|
||||
docElement = null
|
||||
}
|
||||
// update position of the popup
|
||||
if (this.show_doc && docElement !== null) {
|
||||
const rect = ctx.canvas.getBoundingClientRect()
|
||||
const scaleX = rect.width / ctx.canvas.width
|
||||
const scaleY = rect.height / ctx.canvas.height
|
||||
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(scaleX, scaleY)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(this.size[0] * scaleX * Math.max(1.0,window.devicePixelRatio) , 0)
|
||||
.translateSelf(10, -32)
|
||||
|
||||
const scale = new DOMMatrix()
|
||||
.scaleSelf(transform.a, transform.d);
|
||||
|
||||
const styleObject = {
|
||||
transformOrigin: '0 0',
|
||||
transform: scale,
|
||||
left: `${transform.a + transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
};
|
||||
Object.assign(docElement.style, styleObject);
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(x - 2, iconSize - 34)
|
||||
ctx.scale(iconSize / 32, iconSize / 32)
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.3)'
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.lineWidth = 2.4
|
||||
ctx.font = 'bold 36px monospace'
|
||||
ctx.fillStyle = 'orange';
|
||||
ctx.fillText('?', 0, 24)
|
||||
ctx.restore()
|
||||
return r
|
||||
}
|
||||
// handle clicking of the icon
|
||||
const mouseDown = nodeType.prototype.onMouseDown
|
||||
nodeType.prototype.onMouseDown = function (e, localPos, canvas) {
|
||||
const r = mouseDown ? mouseDown.apply(this, arguments) : undefined
|
||||
const iconX = this.size[0] - iconSize - iconMargin
|
||||
const iconY = iconSize - 34
|
||||
if (
|
||||
localPos[0] > iconX &&
|
||||
localPos[0] < iconX + iconSize &&
|
||||
localPos[1] > iconY &&
|
||||
localPos[1] < iconY + iconSize
|
||||
) {
|
||||
if (this.show_doc === undefined) {
|
||||
this.show_doc = true
|
||||
} else {
|
||||
this.show_doc = !this.show_doc
|
||||
}
|
||||
if (this.show_doc) {
|
||||
this.docCtrl = new AbortController()
|
||||
} else {
|
||||
this.docCtrl.abort()
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
const onRem = nodeType.prototype.onRemoved
|
||||
|
||||
nodeType.prototype.onRemoved = function () {
|
||||
const r = onRem ? onRem.apply(this, []) : undefined
|
||||
|
||||
if (docElement) {
|
||||
docElement.remove()
|
||||
docElement = null
|
||||
}
|
||||
|
||||
if (contentWrapper) {
|
||||
contentWrapper.remove()
|
||||
contentWrapper = null
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "VykosX-ControlFlowUtils"
|
||||
description = "Custom nodes to improve flow control and logic + several utilities to enhance capabilities"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
license = "GPL v3"
|
||||
|
||||
[project.urls]
|
||||
|
||||
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 681 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 333 KiB |