v0.0.2 Alpha

Tons of new nodes and functionality implemented, lots of bugs fixed. Should be mostly stable-ish now.
This commit is contained in:
VykosX
2024-09-05 22:37:59 -03:00
committed by GitHub
parent 178716d005
commit 217b995d7b
17 changed files with 3952 additions and 1152 deletions
+2373 -1086
View File
File diff suppressed because it is too large Load Diff
+676
View File
@@ -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'
"""
+261
View File
@@ -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
+48 -38
View File
@@ -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",
}
+311 -27
View File
@@ -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;
};
}
},
});
+282
View File
@@ -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 -1
View File
@@ -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]
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB