Merge pull request #14 from StableLlama/enhance_completeness
Enhance completeness
This commit is contained in:
@@ -4,9 +4,6 @@ Basic Python functions for manipulating data that every programmer is used to.
|
||||
|
||||
These nodes are very lightweight and require no additional dependencies.
|
||||
|
||||
> [!NOTE]
|
||||
> This projected is in early development—do not use it in production, yet!
|
||||
|
||||
## Quickstart
|
||||
|
||||
1. Install [ComfyUI](https://docs.comfy.org/get_started).
|
||||
@@ -158,6 +155,7 @@ These nodes are very lightweight and require no additional dependencies.
|
||||
- is_absolute - Checks if a path is absolute (begins at root directory)
|
||||
- get_size - Returns the size of a file in bytes
|
||||
- get_extension - Extracts the file extension from a path (including the dot)
|
||||
- set_extension - Replaces or adds a file extension to a path
|
||||
- Directory operations:
|
||||
- list_dir - Lists files and directories in a specified path with filtering options
|
||||
- get_cwd - Returns the current working directory
|
||||
|
||||
+3
-4
@@ -4,10 +4,8 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "basic_data_handling"
|
||||
version = "0.3.5"
|
||||
description = """NOTE: Still in development! Expect breaking changes!
|
||||
|
||||
Basic Python functions for manipulating data that every programmer is used to.
|
||||
version = "0.4.0"
|
||||
description = """Basic Python functions for manipulating data that every programmer is used to.
|
||||
Comprehensive node collection for data manipulation in ComfyUI workflows.
|
||||
|
||||
Supported data types:
|
||||
@@ -21,6 +19,7 @@ Feature categories:
|
||||
- Data structures manipulation (data lists, LIST, DICT, SET)
|
||||
- Flow control (conditionals, branching, execution order)
|
||||
- Mathematical operations (arithmetic, trigonometry, logarithmic functions)
|
||||
- Mathematical formula node in a safe implementation
|
||||
- String manipulation (case conversion, formatting, splitting, validation)
|
||||
- File system path handling (path operations, information, searching)
|
||||
- SET operations (creation, modification, comparison, mathematical set theory)
|
||||
|
||||
@@ -64,7 +64,6 @@ class IfElifElse(ComfyNodeABC):
|
||||
When none is true, the value of the else is returned.
|
||||
This allows conditional data flow in ComfyUI workflows.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -149,7 +148,6 @@ class SwitchCase(ComfyNodeABC):
|
||||
|
||||
NOTE: This version of the node will most likely be deprecated in the future.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
@@ -52,7 +52,6 @@ class DataListCreateFromBoolean(ComfyNodeABC):
|
||||
This node creates and returns a Data List. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -80,7 +79,6 @@ class DataListCreateFromFloat(ComfyNodeABC):
|
||||
This node creates and returns a Data List. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -108,7 +106,6 @@ class DataListCreateFromInt(ComfyNodeABC):
|
||||
This node creates and returns a Data List. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -136,7 +133,6 @@ class DataListCreateFromString(ComfyNodeABC):
|
||||
This node creates and returns a Data List. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
|
||||
@@ -22,7 +22,6 @@ class DictCreate(ComfyNodeABC):
|
||||
|
||||
This node creates and returns a new empty dictionary object.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -54,7 +53,6 @@ class DictCreateFromBoolean(ComfyNodeABC):
|
||||
|
||||
This node creates and returns a new empty dictionary object.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -86,7 +84,6 @@ class DictCreateFromFloat(ComfyNodeABC):
|
||||
|
||||
This node creates and returns a new empty dictionary object.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -118,7 +115,6 @@ class DictCreateFromInt(ComfyNodeABC):
|
||||
|
||||
This node creates and returns a new empty dictionary object.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -150,7 +146,6 @@ class DictCreateFromString(ComfyNodeABC):
|
||||
|
||||
This node creates and returns a new empty dictionary object.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
|
||||
@@ -50,7 +50,6 @@ class ListCreateFromBoolean(ComfyNodeABC):
|
||||
This node creates and returns a LIST. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -76,7 +75,6 @@ class ListCreateFromFloat(ComfyNodeABC):
|
||||
This node creates and returns a LIST. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -102,7 +100,6 @@ class ListCreateFromInt(ComfyNodeABC):
|
||||
This node creates and returns a LIST. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -128,7 +125,6 @@ class ListCreateFromString(ComfyNodeABC):
|
||||
This node creates and returns a LIST. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import re
|
||||
import math
|
||||
from inspect import cleandoc
|
||||
|
||||
try:
|
||||
from comfy.comfy_types.node_typing import IO, ComfyNodeABC
|
||||
except:
|
||||
except ImportError:
|
||||
# Mock classes for standalone testing
|
||||
class IO:
|
||||
BOOLEAN = "BOOLEAN"
|
||||
INT = "INT"
|
||||
@@ -18,260 +21,235 @@ class MathFormula(ComfyNodeABC):
|
||||
"""
|
||||
A node that evaluates a mathematical formula provided as a string without using eval.
|
||||
|
||||
This node takes up to 4 numerical inputs (`a`, `b`, `c`, and `d`) and safely evaluates the formula
|
||||
with supported operations: +, -, *, /, //, %, **, parentheses, and mathematical functions like abs(),
|
||||
floor(), ceil(), and round(). It treats one-letter variables as inputs and multi-letter words as functions.
|
||||
This node takes an arbitrary number of numerical inputs (single letters like `a`, `b`, `c`, etc.) and safely
|
||||
evaluates the formula with supported operations: +, -, *, /, //, %, **, parentheses, and mathematical
|
||||
functions.
|
||||
|
||||
Example: If the formula is "a + abs(b * 4 + c)" and inputs are:
|
||||
a = 2, b = -3, c = 5,
|
||||
The calculation would be:
|
||||
result = 2 + abs(-3 * 4 + 5) = 2 + abs(-7) = 2 + 7 = 9
|
||||
NOTE on Operator Precedence: This parser uses standard precedence rules. Unary minus binds tightly,
|
||||
so expressions like `-a ** 2` are interpreted as `(-a) ** 2`. To calculate `-(a ** 2)`,
|
||||
use parentheses: `-(a ** 2)`.
|
||||
|
||||
The identifiers `e` and `pi` are special. When used as a function call (`e()`, `pi()`), they return their
|
||||
respective mathematical constants. When used as a variable (`e`), they expect a corresponding input.
|
||||
|
||||
Supported functions:
|
||||
- Basic: abs, floor, ceil, round, min(a,b), max(a,b)
|
||||
- Trigonometric: sin, cos, tan, asin, acos, atan, atan2(y,x), degrees, radians
|
||||
- Hyperbolic: sinh, cosh, tanh, asinh, acosh, atanh
|
||||
- Exponential & Logarithmic: exp, log, log10, log2, sqrt, pow(base,exp)
|
||||
- Constants (must be called with empty parentheses): pi(), e()
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": ContainsDynamicDict({
|
||||
"formula": (IO.STRING, {"default": "a + b"}),
|
||||
"a": (IO.NUMBER, {"default": 0.0, "_dynamic": "letter"}),
|
||||
"formula": (IO.STRING, {"default": "-e() ** -2"}),
|
||||
"a": (IO.NUMBER, {"default": 3.0, "_dynamic": "letter"}),
|
||||
}),
|
||||
}
|
||||
|
||||
RETURN_TYPES = (IO.FLOAT,)
|
||||
CATEGORY = "Basic/maths"
|
||||
CATEGORY = "math"
|
||||
DESCRIPTION = cleandoc(__doc__ or "")
|
||||
FUNCTION = "evaluate"
|
||||
|
||||
OPERATORS = {"**", "//", "/", "*", "-", "+", "%"}
|
||||
SUPPORTED_FUNCTIONS = {"abs", "floor", "ceil", "round"}
|
||||
VALID_CHARS = set("0123456789.+-*/%(), ")
|
||||
FUNC_ARITIES = {
|
||||
"pi": 0, "e": 0, "abs": 1, "floor": 1, "ceil": 1, "round": 1, "sin": 1, "cos": 1,
|
||||
"tan": 1, "asin": 1, "acos": 1, "atan": 1, "degrees": 1, "radians": 1, "sinh": 1,
|
||||
"cosh": 1, "tanh": 1, "asinh": 1, "acosh": 1, "atanh": 1, "exp": 1, "log": 1,
|
||||
"log10": 1, "log2": 1, "sqrt": 1, "pow": 2, "atan2": 2, "min": 2, "max": 2,
|
||||
}
|
||||
SUPPORTED_FUNCTIONS = set(FUNC_ARITIES.keys())
|
||||
|
||||
def evaluate(self, formula: str, a: float, b: float = 0.0, c: float = 0.0, d: float = 0.0) -> tuple[float]:
|
||||
# Tokenize the formula without replacement
|
||||
print(f'formula: "{formula}"')
|
||||
OPERATOR_PROPS = {
|
||||
# token: (precedence, associativity, arity)
|
||||
"+": (2, "LEFT", 2),
|
||||
"-": (2, "LEFT", 2),
|
||||
"*": (3, "LEFT", 2),
|
||||
"/": (3, "LEFT", 2),
|
||||
"//": (3, "LEFT", 2),
|
||||
"%": (3, "LEFT", 2),
|
||||
"**": (4, "RIGHT", 2),
|
||||
"_NEG": (5, "RIGHT", 1), # Unary has higher precedence than exponentiation
|
||||
}
|
||||
OPERATORS = set(OPERATOR_PROPS.keys())
|
||||
|
||||
TOKEN_REGEX = re.compile(
|
||||
r"([a-zA-Z_][a-zA-Z0-9_]*)"
|
||||
r"|(\d+(?:\.\d*)?|\.\d+)"
|
||||
r"|(\*\*|//|[+\-*/%(),])"
|
||||
)
|
||||
|
||||
def evaluate(self, formula: str, **kwargs) -> tuple[float]:
|
||||
tokens = self.tokenize_formula(formula)
|
||||
print(f'ev tokens: "{tokens}"')
|
||||
|
||||
# Replace variables in the tokenized formula
|
||||
replaced_tokens = []
|
||||
for token in tokens:
|
||||
if token == "a":
|
||||
replaced_tokens.append(str(a))
|
||||
elif token == "b":
|
||||
replaced_tokens.append(str(b))
|
||||
elif token == "c":
|
||||
replaced_tokens.append(str(c))
|
||||
elif token == "d":
|
||||
replaced_tokens.append(str(d))
|
||||
else:
|
||||
replaced_tokens.append(token)
|
||||
|
||||
# Join the replaced tokens back into a formula
|
||||
updated_formula = " ".join(replaced_tokens)
|
||||
print(f'updated_formula: "{updated_formula}"')
|
||||
|
||||
# Ensure formula is valid
|
||||
self.validate_formula(updated_formula)
|
||||
|
||||
# Parse formula into postfix form and evaluate
|
||||
postfix = self.infix_to_postfix(replaced_tokens)
|
||||
print(f'postfix: "{postfix}"')
|
||||
result = self.evaluate_postfix(postfix)
|
||||
print(f'result: "{result}"')
|
||||
|
||||
postfix_tokens = self.infix_to_postfix(tokens)
|
||||
result = self.evaluate_postfix(postfix_tokens, kwargs)
|
||||
return (result,)
|
||||
|
||||
def validate_formula(self, formula: str):
|
||||
"""Validates the formula for allowed operators, functions, and characters."""
|
||||
import re
|
||||
|
||||
# Validate supported characters
|
||||
def tokenize_formula(self, formula: str) -> list[str]:
|
||||
allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.,+-*/%() \t\n\r"
|
||||
for char in formula:
|
||||
if char not in self.VALID_CHARS and not char.isalpha():
|
||||
if char not in allowed_chars:
|
||||
raise ValueError(f"Invalid character in formula: '{char}'")
|
||||
return [token for group in self.TOKEN_REGEX.findall(formula) for token in group if token]
|
||||
|
||||
# Validate that all functions are supported
|
||||
functions = re.findall(r"[a-zA-Z_]\w*(?=\()", formula) # Matches functions immediately before "("
|
||||
print('formula:')
|
||||
print(formula)
|
||||
print('functions:')
|
||||
print(functions)
|
||||
for func in functions:
|
||||
if func not in self.SUPPORTED_FUNCTIONS:
|
||||
raise ValueError(f"Unsupported function in formula: '{func}'")
|
||||
def infix_to_postfix(self, tokens: list[str]) -> list:
|
||||
output_queue = []
|
||||
op_stack = []
|
||||
prev_token = None
|
||||
binary_operators = {op for op, props in self.OPERATOR_PROPS.items() if props[2] == 2}
|
||||
|
||||
def tokenize_formula(self, formula: str) -> list:
|
||||
"""
|
||||
Tokenizes a formula into numbers, variables, functions, operators, and parentheses.
|
||||
|
||||
Rules:
|
||||
- A single-letter token (e.g., 'a') is treated as a variable.
|
||||
- A multi-letter token (e.g., 'abs') is treated as a function.
|
||||
- Numbers can be integers, floating point, or hexadecimal.
|
||||
"""
|
||||
import re
|
||||
|
||||
# Regex pattern for tokenizing
|
||||
pattern = (
|
||||
r"(?P<number>-?\d+(\.\d+)?([eE][+-]?\d+)?|-?0x[0-9a-fA-F]+)" # Matches float, int, hex, scientific notation
|
||||
r"|(?P<function>[a-zA-Z]{2,})" # Matches multi-letter functions like 'abs'
|
||||
r"|(?P<variable>[a-zA-Z])" # Matches single-letter variables (e.g., 'a', 'b')
|
||||
r"|(?P<operator>\*\*|//|[+\-*/%()])" # Matches operators and parentheses
|
||||
)
|
||||
# Apply regex to extract matches
|
||||
tokens = re.finditer(pattern, formula)
|
||||
|
||||
# Collect and clean the matched tokens
|
||||
result = [match.group(0) for match in tokens]
|
||||
print(f'Tokens: {result}') # Debug print
|
||||
return result
|
||||
|
||||
def infix_to_postfix(self, tokens: list) -> list:
|
||||
"""
|
||||
Converts a list of tokens in infix notation to postfix notation (RPN).
|
||||
Correctly handles unary negation (e.g., -b) and operator precedence.
|
||||
"""
|
||||
precedence = {"**": 4, "*": 3, "/": 3, "//": 3, "%": 3, "+": 2, "-": 2, "(": 1}
|
||||
stack = [] # Operator stack
|
||||
postfix = [] # Output list
|
||||
previous_token = None # Tracks the previous token to distinguish unary vs binary operators
|
||||
|
||||
# Track function-to-parenthesis relationships
|
||||
function_stack = [] # Stack to track pending functions
|
||||
|
||||
for token in tokens:
|
||||
if self.is_number(token): # Numbers
|
||||
postfix.append(float(token))
|
||||
elif token in self.SUPPORTED_FUNCTIONS: # Functions (e.g., abs)
|
||||
# Push function onto stack - will be processed when matching parenthesis is found
|
||||
function_stack.append(len(stack)) # Record stack depth for this function
|
||||
stack.append(token)
|
||||
elif token == "(": # Left parentheses
|
||||
stack.append(token)
|
||||
elif token == ")": # Right parentheses
|
||||
# Pop operators until left parenthesis
|
||||
while stack and stack[-1] != "(":
|
||||
postfix.append(stack.pop())
|
||||
|
||||
if not stack:
|
||||
raise ValueError("Mismatched parentheses in expression.")
|
||||
|
||||
stack.pop() # Discard the left parenthesis
|
||||
|
||||
# Check if this closing parenthesis matches a function call
|
||||
if function_stack and len(stack) == function_stack[-1]:
|
||||
# We've found our function, add it to postfix
|
||||
if stack and stack[-1] in self.SUPPORTED_FUNCTIONS:
|
||||
postfix.append(stack.pop())
|
||||
function_stack.pop() # Remove the function marker
|
||||
elif token in self.OPERATORS: # Operators (+, -, *, etc.)
|
||||
# Check for unary negation
|
||||
if token == "-" and (previous_token is None or previous_token in self.OPERATORS or previous_token == "("):
|
||||
# Handle negative numbers directly - place negative value in postfix
|
||||
if tokens.index(token) + 1 < len(tokens) and self.is_number(tokens[tokens.index(token) + 1]):
|
||||
next_token = tokens[tokens.index(token) + 1]
|
||||
# Skip this token and the next number, add negative number to postfix
|
||||
postfix.append(-float(next_token))
|
||||
previous_token = next_token # Skip to after the number
|
||||
continue
|
||||
else:
|
||||
# Traditional unary minus using 0 - operand
|
||||
postfix.append(0.0)
|
||||
stack.append(token)
|
||||
for i, token in enumerate(tokens):
|
||||
if self.is_number(token):
|
||||
output_queue.append(float(token))
|
||||
elif token.isalnum() and not token.isdigit():
|
||||
is_function_call = (i + 1 < len(tokens) and tokens[i+1] == '(')
|
||||
if is_function_call:
|
||||
if token not in self.SUPPORTED_FUNCTIONS:
|
||||
raise ValueError(f"Unknown function: '{token}'")
|
||||
op_stack.append(token)
|
||||
else:
|
||||
# Handle regular operators
|
||||
while stack and stack[-1] not in ["("] and precedence.get(stack[-1], 0) >= precedence[token]:
|
||||
postfix.append(stack.pop())
|
||||
stack.append(token)
|
||||
if len(token) == 1 and token.isalpha():
|
||||
output_queue.append(('VAR', token))
|
||||
else:
|
||||
if token in self.SUPPORTED_FUNCTIONS:
|
||||
raise ValueError(f"Function '{token}' must be called with parentheses, e.g., {token}()")
|
||||
else:
|
||||
raise ValueError(f"Unknown identifier: '{token}'. Only single-letter variables are allowed.")
|
||||
|
||||
elif token == "(":
|
||||
op_stack.append(token)
|
||||
elif token == ",":
|
||||
while op_stack and op_stack[-1] != "(":
|
||||
output_queue.append(op_stack.pop())
|
||||
if not op_stack or op_stack[-1] != "(":
|
||||
raise ValueError("Misplaced comma or mismatched parentheses.")
|
||||
elif token == ")":
|
||||
while op_stack and op_stack[-1] != "(":
|
||||
output_queue.append(op_stack.pop())
|
||||
if not op_stack:
|
||||
raise ValueError("Mismatched parentheses.")
|
||||
op_stack.pop()
|
||||
if op_stack and op_stack[-1] in self.SUPPORTED_FUNCTIONS:
|
||||
output_queue.append(op_stack.pop())
|
||||
elif token in self.OPERATORS:
|
||||
is_unary = token == '-' and (prev_token is None or prev_token in binary_operators or prev_token in ['(', ','])
|
||||
op_to_push = "_NEG" if is_unary else token
|
||||
props = self.OPERATOR_PROPS[op_to_push]
|
||||
prec = props[0]
|
||||
|
||||
while (op_stack and op_stack[-1] != '(' and op_stack[-1] in self.OPERATORS):
|
||||
stack_op_props = self.OPERATOR_PROPS[op_stack[-1]]
|
||||
stack_op_prec = stack_op_props[0]
|
||||
stack_op_assoc = stack_op_props[1]
|
||||
if (stack_op_prec > prec) or (stack_op_prec == prec and stack_op_assoc == "LEFT"):
|
||||
output_queue.append(op_stack.pop())
|
||||
else:
|
||||
break
|
||||
op_stack.append(op_to_push)
|
||||
else:
|
||||
raise ValueError(f"Unexpected token in infix expression: {token}")
|
||||
raise ValueError(f"Unknown identifier or invalid expression: '{token}'")
|
||||
prev_token = token
|
||||
|
||||
# Update previous token
|
||||
previous_token = token
|
||||
while op_stack:
|
||||
op = op_stack.pop()
|
||||
if op == "(":
|
||||
raise ValueError("Mismatched parentheses.")
|
||||
output_queue.append(op)
|
||||
|
||||
# Pop any remaining operators from the stack
|
||||
while stack:
|
||||
top = stack.pop()
|
||||
if top in ["(", ")"]:
|
||||
raise ValueError("Mismatched parentheses in expression.")
|
||||
postfix.append(top)
|
||||
return output_queue
|
||||
|
||||
return postfix
|
||||
|
||||
def evaluate_postfix(self, postfix: list) -> float:
|
||||
"""
|
||||
Evaluates a postfix (RPN) expression and returns the result.
|
||||
Handles numbers, operators, and functions, including unary negation.
|
||||
"""
|
||||
def evaluate_postfix(self, postfix: list, variables: dict) -> float:
|
||||
stack = []
|
||||
|
||||
for token in postfix:
|
||||
if isinstance(token, float): # Numbers
|
||||
if isinstance(token, float):
|
||||
stack.append(token)
|
||||
elif token in self.SUPPORTED_FUNCTIONS: # Functions (e.g., abs)
|
||||
if not stack:
|
||||
raise ValueError(f"Invalid formula: function '{token}' requires an argument but the stack is empty.")
|
||||
arg = stack.pop()
|
||||
stack.append(self.apply_function(token, arg))
|
||||
elif token in self.OPERATORS: # Operators (+, -, *, /, etc.)
|
||||
if len(stack) < 2:
|
||||
raise ValueError(f"Invalid formula: operator '{token}' requires two arguments but the stack has {len(stack)}.")
|
||||
b = stack.pop()
|
||||
a = stack.pop()
|
||||
stack.append(self.apply_operator(a, b, token))
|
||||
elif isinstance(token, tuple) and token[0] == 'VAR':
|
||||
var_name = token[1]
|
||||
if var_name not in variables:
|
||||
raise ValueError(f"Variable '{var_name}' was not provided.")
|
||||
stack.append(float(variables[var_name]))
|
||||
elif token in self.OPERATORS:
|
||||
arity = self.OPERATOR_PROPS[token][2]
|
||||
if len(stack) < arity:
|
||||
raise ValueError(f"Operator '{token}' needs {arity} operand(s).")
|
||||
|
||||
operands = [stack.pop() for _ in range(arity)]
|
||||
operands.reverse()
|
||||
|
||||
if arity == 1:
|
||||
stack.append(-operands[0])
|
||||
else:
|
||||
stack.append(self.apply_operator(operands[0], operands[1], token))
|
||||
elif token in self.SUPPORTED_FUNCTIONS:
|
||||
arity = self.FUNC_ARITIES[token]
|
||||
if len(stack) < arity:
|
||||
raise ValueError(f"Function '{token}' needs {arity} argument(s).")
|
||||
|
||||
args = [stack.pop() for _ in range(arity)]
|
||||
args.reverse()
|
||||
stack.append(self.apply_function(token, args))
|
||||
else:
|
||||
raise ValueError(f"Unexpected token in postfix expression: {token}")
|
||||
raise ValueError(f"Internal error: Unknown token in postfix queue: {token}")
|
||||
|
||||
if len(stack) != 1:
|
||||
raise ValueError(f"Invalid postfix expression: stack contains excess items: {stack}")
|
||||
return stack.pop()
|
||||
raise ValueError("Invalid expression. The formula may be incomplete or have extra values.")
|
||||
return stack[0]
|
||||
|
||||
def apply_operator(self, a: float, b: float, operator: str) -> float:
|
||||
"""Applies the given operator to two operands."""
|
||||
if operator == "+":
|
||||
return a + b
|
||||
elif operator == "-":
|
||||
return a - b
|
||||
elif operator == "*":
|
||||
return a * b
|
||||
elif operator == "/":
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("Division by zero.")
|
||||
return a / b
|
||||
elif operator == "//":
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("Division by zero.")
|
||||
return a // b
|
||||
elif operator == "%":
|
||||
if b == 0:
|
||||
raise ZeroDivisionError("Modulo by zero.")
|
||||
return a % b
|
||||
elif operator == "**":
|
||||
return a ** b
|
||||
else:
|
||||
raise ValueError(f"Unsupported operator: {operator}")
|
||||
if operator == "+": return a + b
|
||||
if operator == "-": return a - b
|
||||
if operator == "*": return a * b
|
||||
if operator == "**": return a ** b
|
||||
if b == 0 and operator in ['/', '//', '%']:
|
||||
raise ZeroDivisionError(f"Division by zero in operator '{operator}'.")
|
||||
if operator == "/": return a / b
|
||||
if operator == "//": return a // b
|
||||
if operator == "%": return a % b
|
||||
raise ValueError(f"Unsupported operator: {operator}")
|
||||
|
||||
def apply_function(self, func: str, arg: float) -> float:
|
||||
"""Applies a mathematical function."""
|
||||
import math
|
||||
if func == "abs":
|
||||
return abs(arg)
|
||||
elif func == "floor":
|
||||
return math.floor(arg)
|
||||
elif func == "ceil":
|
||||
return math.ceil(arg)
|
||||
elif func == "round":
|
||||
return round(arg)
|
||||
else:
|
||||
raise ValueError(f"Unsupported function: {func}")
|
||||
def apply_function(self, func: str, args: list) -> float:
|
||||
if func == "pi": return math.pi
|
||||
if func == "e": return math.e
|
||||
if len(args) == 1:
|
||||
arg = args[0]
|
||||
if func == "abs": return abs(arg)
|
||||
if func == "floor": return math.floor(arg)
|
||||
if func == "ceil": return math.ceil(arg)
|
||||
if func == "round": return round(arg)
|
||||
if func == "sin": return math.sin(arg)
|
||||
if func == "cos": return math.cos(arg)
|
||||
if func == "tan": return math.tan(arg)
|
||||
if func == "asin": return math.asin(arg)
|
||||
if func == "acos": return math.acos(arg)
|
||||
if func == "atan": return math.atan(arg)
|
||||
if func == "degrees": return math.degrees(arg)
|
||||
if func == "radians": return math.radians(arg)
|
||||
if func == "sinh": return math.sinh(arg)
|
||||
if func == "cosh": return math.cosh(arg)
|
||||
if func == "tanh": return math.tanh(arg)
|
||||
if func == "asinh": return math.asinh(arg)
|
||||
if func == "acosh": return math.acosh(arg)
|
||||
if func == "atanh": return math.atanh(arg)
|
||||
if func == "exp": return math.exp(arg)
|
||||
if func == "log": return math.log(arg)
|
||||
if func == "log10": return math.log10(arg)
|
||||
if func == "log2": return math.log2(arg)
|
||||
if func == "sqrt": return math.sqrt(arg)
|
||||
if len(args) == 2:
|
||||
a, b = args[0], args[1]
|
||||
if func == "pow": return math.pow(a, b)
|
||||
if func == "atan2": return math.atan2(a, b)
|
||||
if func == "min": return min(a, b)
|
||||
if func == "max": return max(a, b)
|
||||
raise ValueError(f"Internal error: apply_function called with wrong number of args for '{func}'")
|
||||
|
||||
def is_number(self, value: str) -> bool:
|
||||
"""Checks if a token is a valid number."""
|
||||
try:
|
||||
float.fromhex(value) if value.startswith("0x") else float(value)
|
||||
float(value)
|
||||
return True
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
|
||||
@@ -453,6 +453,37 @@ class PathNormalize(ComfyNodeABC):
|
||||
return (os.path.normpath(path),)
|
||||
|
||||
|
||||
class PathSetExtension(ComfyNodeABC):
|
||||
"""
|
||||
Sets the file extension for a path.
|
||||
|
||||
This node replaces the current extension in a path with a new one.
|
||||
The extension should include the dot (e.g., '.jpg').
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"path": (IO.STRING, {}),
|
||||
"extension": (IO.STRING, {"default": ".txt"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (IO.STRING,)
|
||||
RETURN_NAMES = ("path",)
|
||||
CATEGORY = "Basic/Path"
|
||||
DESCRIPTION = cleandoc(__doc__ or "")
|
||||
FUNCTION = "set_extension"
|
||||
|
||||
def set_extension(self, path: str, extension: str) -> tuple[str]:
|
||||
# Make sure extension starts with a dot
|
||||
if not extension.startswith('.') and extension:
|
||||
extension = '.' + extension
|
||||
|
||||
root, _ = os.path.splitext(path)
|
||||
return (root + extension,)
|
||||
|
||||
|
||||
class PathRelative(ComfyNodeABC):
|
||||
"""
|
||||
Returns a relative path.
|
||||
@@ -542,6 +573,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Basic data handling: PathExpandVars": PathExpandVars,
|
||||
"Basic data handling: PathGetCwd": PathGetCwd,
|
||||
"Basic data handling: PathGetExtension": PathGetExtension,
|
||||
"Basic data handling: PathSetExtension": PathSetExtension,
|
||||
"Basic data handling: PathGetSize": PathGetSize,
|
||||
"Basic data handling: PathGlob": PathGlob,
|
||||
"Basic data handling: PathIsAbsolute": PathIsAbsolute,
|
||||
@@ -564,6 +596,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Basic data handling: PathExpandVars": "expand vars",
|
||||
"Basic data handling: PathGetCwd": "get current working directory",
|
||||
"Basic data handling: PathGetExtension": "get extension",
|
||||
"Basic data handling: PathSetExtension": "set extension",
|
||||
"Basic data handling: PathGetSize": "get size",
|
||||
"Basic data handling: PathGlob": "glob",
|
||||
"Basic data handling: PathIsAbsolute": "is absolute",
|
||||
|
||||
@@ -48,7 +48,6 @@ class SetCreateFromBoolean(ComfyNodeABC):
|
||||
This node creates and returns a SET. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -74,7 +73,6 @@ class SetCreateFromFloat(ComfyNodeABC):
|
||||
This node creates and returns a SET. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -100,7 +98,6 @@ class SetCreateFromInt(ComfyNodeABC):
|
||||
This node creates and returns a SET. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -126,7 +123,6 @@ class SetCreateFromString(ComfyNodeABC):
|
||||
This node creates and returns a SET. The list of items is dynamically
|
||||
extended based on the number of inputs provided.
|
||||
"""
|
||||
EXPERIMENTAL = True
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import pytest
|
||||
import math
|
||||
from math import sin, cos
|
||||
from src.basic_data_handling.math_formula_node import MathFormula
|
||||
|
||||
def test_basic_formula_evaluation():
|
||||
"""Test basic formula evaluation with simple operations."""
|
||||
node = MathFormula()
|
||||
|
||||
# Simple arithmetic
|
||||
assert node.evaluate("a + b", a=3, b=4)[0] == pytest.approx(7.0)
|
||||
assert node.evaluate("a - b", a=10, b=6)[0] == pytest.approx(4.0)
|
||||
assert node.evaluate("a * b", a=5, b=3)[0] == pytest.approx(15.0)
|
||||
assert node.evaluate("a / b", a=10, b=2)[0] == pytest.approx(5.0)
|
||||
|
||||
# Combined operations
|
||||
assert node.evaluate("a + b * c", a=2, b=3, c=4)[0] == pytest.approx(14.0)
|
||||
assert node.evaluate("(a + b) * c", a=2, b=3, c=4)[0] == pytest.approx(20.0)
|
||||
assert node.evaluate("a ** b", a=2, b=3)[0] == pytest.approx(8.0)
|
||||
assert node.evaluate("a // b", a=7, b=2)[0] == pytest.approx(3.0)
|
||||
assert node.evaluate("a % b", a=7, b=4)[0] == pytest.approx(3.0)
|
||||
|
||||
# Unary minus
|
||||
assert node.evaluate("-a", a=5)[0] == pytest.approx(-5.0)
|
||||
assert node.evaluate("a * -b", a=3, b=4)[0] == pytest.approx(-12.0)
|
||||
|
||||
def test_function_calls():
|
||||
"""Test formula evaluation with math functions."""
|
||||
node = MathFormula()
|
||||
|
||||
# Single-argument functions
|
||||
assert node.evaluate("sin(a)", a=math.pi/6)[0] == pytest.approx(0.5)
|
||||
assert node.evaluate("cos(a)", a=0)[0] == pytest.approx(1.0)
|
||||
|
||||
# Unary minus with function calls
|
||||
assert node.evaluate("sin(-a)", a=math.pi/6)[0] == pytest.approx(-0.5)
|
||||
assert node.evaluate("-sin(a)", a=math.pi/6)[0] == pytest.approx(-0.5)
|
||||
assert node.evaluate("-sin(-a)", a=math.pi/6)[0] == pytest.approx(0.5)
|
||||
|
||||
# Unary minus with nested function calls
|
||||
assert node.evaluate("sin(cos(-a))", a=math.pi)[0] == pytest.approx(sin(cos(-math.pi)))
|
||||
assert node.evaluate("-sin(cos(-a))", a=math.pi)[0] == pytest.approx(-sin(cos(-math.pi)))
|
||||
assert node.evaluate("tan(a)", a=math.pi/4)[0] == pytest.approx(1.0)
|
||||
assert node.evaluate("sqrt(a)", a=16)[0] == pytest.approx(4.0)
|
||||
assert node.evaluate("log(a)", a=math.e)[0] == pytest.approx(1.0)
|
||||
assert node.evaluate("exp(a)", a=2)[0] == pytest.approx(math.exp(2))
|
||||
|
||||
# Two-argument functions
|
||||
assert node.evaluate("min(a, b)", a=5, b=10)[0] == pytest.approx(5.0)
|
||||
assert node.evaluate("max(a, b)", a=5, b=10)[0] == pytest.approx(10.0)
|
||||
assert node.evaluate("pow(a, b)", a=2, b=3)[0] == pytest.approx(8.0)
|
||||
|
||||
def test_constants():
|
||||
"""Test formula evaluation with constants."""
|
||||
node = MathFormula()
|
||||
|
||||
# Constants called with empty parentheses
|
||||
assert node.evaluate("pi()")[0] == pytest.approx(math.pi)
|
||||
assert node.evaluate("e()")[0] == pytest.approx(math.e)
|
||||
|
||||
# Using constants in expressions
|
||||
assert node.evaluate("2 * pi()")[0] == pytest.approx(2 * math.pi)
|
||||
assert node.evaluate("e() ** 2")[0] == pytest.approx(math.e ** 2)
|
||||
assert node.evaluate("sin(pi() / 2)")[0] == pytest.approx(1.0)
|
||||
assert node.evaluate("log(e())")[0] == pytest.approx(1.0)
|
||||
|
||||
# More complex expressions with constants
|
||||
assert node.evaluate("a * pi() + b", a=2, b=1)[0] == pytest.approx(2 * math.pi + 1)
|
||||
assert node.evaluate("e() ** a", a=3)[0] == pytest.approx(math.e ** 3)
|
||||
assert node.evaluate("sin(a * pi())", a=0.5)[0] == pytest.approx(1.0, abs=1e-10)
|
||||
|
||||
def test_variable_constant_distinction():
|
||||
"""Test that variables and constants can coexist without conflict."""
|
||||
node = MathFormula()
|
||||
|
||||
# Using 'e' as a variable while also using 'e()' as a constant
|
||||
assert node.evaluate("e + e()", e=5)[0] == pytest.approx(5 + math.e)
|
||||
|
||||
# Using 'pi' as a variable while also using 'pi()' as a constant
|
||||
with pytest.raises(ValueError, match=r"Function 'pi' must be called with parentheses, e.g., pi()"):
|
||||
node.evaluate("pi * pi()", pi=3)
|
||||
|
||||
# More complex expressions
|
||||
assert node.evaluate("e * log(e())", e=2)[0] == pytest.approx(2.0)
|
||||
assert node.evaluate("a * e() + b * e", a=2, b=3, e=4)[0] == pytest.approx(2 * math.e + 3 * 4)
|
||||
|
||||
def test_unary_minus():
|
||||
"""Test that unary minus (negation) is handled correctly in all contexts."""
|
||||
node = MathFormula()
|
||||
|
||||
# Simple variable negation
|
||||
assert node.evaluate("-a", a=5)[0] == pytest.approx(-5.0)
|
||||
assert node.evaluate("-a", a=-3)[0] == pytest.approx(3.0)
|
||||
|
||||
# Constant negation
|
||||
assert node.evaluate("-pi()")[0] == pytest.approx(-math.pi)
|
||||
assert node.evaluate("-e()")[0] == pytest.approx(-math.e)
|
||||
|
||||
# Function result negation
|
||||
assert node.evaluate("-sin(a)", a=math.pi/2)[0] == pytest.approx(-1.0)
|
||||
assert node.evaluate("-sqrt(a)", a=4)[0] == pytest.approx(-2.0)
|
||||
|
||||
# Nested unary minus
|
||||
assert node.evaluate("--a", a=5)[0] == pytest.approx(5.0)
|
||||
assert node.evaluate("---a", a=5)[0] == pytest.approx(-5.0)
|
||||
|
||||
# Unary minus with binary operators
|
||||
assert node.evaluate("a + -b", a=5, b=3)[0] == pytest.approx(2.0)
|
||||
assert node.evaluate("a * -b", a=4, b=3)[0] == pytest.approx(-12.0)
|
||||
assert node.evaluate("a / -b", a=6, b=2)[0] == pytest.approx(-3.0)
|
||||
assert node.evaluate("a - -b", a=5, b=3)[0] == pytest.approx(8.0)
|
||||
|
||||
# Unary minus with higher precedence operators
|
||||
assert node.evaluate("-a * b", a=2, b=3)[0] == pytest.approx(-6.0)
|
||||
assert node.evaluate("-(a * b)", a=2, b=3)[0] == pytest.approx(-6.0)
|
||||
assert node.evaluate("-a ** 2", a=3)[0] == pytest.approx(9.0) # see documentation about this special case
|
||||
assert node.evaluate("(-a) ** 2", a=3)[0] == pytest.approx(9.0) # (-a)^2
|
||||
|
||||
# Unary minus with parentheses
|
||||
assert node.evaluate("-(a + b)", a=2, b=3)[0] == pytest.approx(-5.0)
|
||||
assert node.evaluate("-(a + b) * c", a=2, b=3, c=4)[0] == pytest.approx(-20.0)
|
||||
assert node.evaluate("a * -(b + c)", a=2, b=3, c=4)[0] == pytest.approx(-14.0)
|
||||
|
||||
# Unary minus at start of parenthesized expressions
|
||||
assert node.evaluate("(-a + b)", a=5, b=3)[0] == pytest.approx(-2.0)
|
||||
assert node.evaluate("(-a - b)", a=5, b=3)[0] == pytest.approx(-8.0)
|
||||
|
||||
# Unary minus with constants and functions in complex expressions
|
||||
assert node.evaluate("-pi() * sin(-a)", a=math.pi/2)[0] == pytest.approx(math.pi)
|
||||
assert node.evaluate("-e() ** -2")[0] == pytest.approx(1/(math.e**2)) # see documentation about this special case
|
||||
|
||||
def test_error_conditions():
|
||||
"""Test error conditions in formula evaluation."""
|
||||
node = MathFormula()
|
||||
|
||||
# Constants without parentheses should raise ValueError
|
||||
with pytest.raises(ValueError, match=r"Function 'pi' must be called with parentheses, e.g., pi()"):
|
||||
node.evaluate("pi")
|
||||
|
||||
with pytest.raises(ValueError, match=r"Variable 'e' was not provided."):
|
||||
node.evaluate("2 * e")
|
||||
|
||||
# Test unary minus with errors
|
||||
with pytest.raises(ValueError):
|
||||
node.evaluate("-unknown_var")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
node.evaluate("-unknown_func(x)", x=5)
|
||||
|
||||
# Unary minus with domain errors
|
||||
with pytest.raises(ValueError):
|
||||
node.evaluate("sqrt(-a)", a=4) # Negative under square root
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
node.evaluate("log(-a)", a=2) # Negative in logarithm
|
||||
|
||||
# Missing variables
|
||||
with pytest.raises(ValueError, match=r"Variable 'x' was not provided"):
|
||||
node.evaluate("x + y", y=5)
|
||||
|
||||
# Invalid function calls
|
||||
with pytest.raises(ValueError):
|
||||
node.evaluate("unknown_func(x)", x=5)
|
||||
|
||||
# Division by zero
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
node.evaluate("a / b", a=5, b=0)
|
||||
|
||||
# Function domain errors
|
||||
with pytest.raises(ValueError):
|
||||
node.evaluate("sqrt(x)", x=-1) # Negative square root
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
node.evaluate("log(x)", x=0) # Log of zero
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
node.evaluate("asin(x)", x=2) # Out of domain [-1, 1]
|
||||
|
||||
def test_complex_expressions():
|
||||
"""Test evaluation of complex mathematical expressions."""
|
||||
node = MathFormula()
|
||||
|
||||
# Complex expression with variables, functions, and constants
|
||||
formula = "a * sin(b * pi()) + c * sqrt(d) + e() ** 2"
|
||||
result = node.evaluate(formula, a=2, b=0.5, c=3, d=9)[0]
|
||||
expected = 2 * math.sin(0.5 * math.pi) + 3 * math.sqrt(9) + math.e ** 2
|
||||
assert result == pytest.approx(expected)
|
||||
|
||||
# Expression with nested functions
|
||||
formula = "sqrt(pow(a, 2) + pow(b, 2))"
|
||||
assert node.evaluate(formula, a=3, b=4)[0] == pytest.approx(5.0)
|
||||
|
||||
# Expression with multiple operations and precedence
|
||||
formula = "a + b * c - d / e + f ** g"
|
||||
result = node.evaluate(formula, a=1, b=2, c=3, d=8, e=4, f=2, g=3)[0]
|
||||
expected = 1 + 2 * 3 - 8 / 4 + 2 ** 3
|
||||
assert result == pytest.approx(expected)
|
||||
|
||||
# Complex expressions with unary minus
|
||||
formula = "a + -b * c - -(d / e) + -f ** g"
|
||||
result = node.evaluate(formula, a=1, b=2, c=3, d=8, e=4, f=2, g=3)[0]
|
||||
expected = 1 + (-2) * 3 - (-(8 / 4)) + (-(2 ** 3))
|
||||
assert result == pytest.approx(expected)
|
||||
|
||||
# Expression with multiple unary minuses and parentheses
|
||||
formula = "-a * (b + -c) / (-d)"
|
||||
result = node.evaluate(formula, a=2, b=5, c=3, d=4)[0]
|
||||
expected = -2 * (5 + (-3)) / (-4)
|
||||
assert result == pytest.approx(expected)
|
||||
|
||||
def test_formula_with_e_variable():
|
||||
"""Test formulas with 'e' as a variable to ensure it doesn't clash with the constant."""
|
||||
node = MathFormula()
|
||||
|
||||
# Using 'e' as a variable
|
||||
assert node.evaluate("e * 2", e=5)[0] == pytest.approx(10.0)
|
||||
|
||||
# Using both 'e' as a variable and 'e()' as a constant in the same expression
|
||||
assert node.evaluate("e + e()", e=3)[0] == pytest.approx(3 + math.e)
|
||||
|
||||
# More complex formula with both uses
|
||||
formula = "a * e + b * e()"
|
||||
result = node.evaluate(formula, a=2, e=3, b=4)[0]
|
||||
expected = 2 * 3 + 4 * math.e
|
||||
assert result == pytest.approx(expected)
|
||||
|
||||
# Formula using 'e' in different contexts
|
||||
formula = "e * sin(e() * pi())"
|
||||
result = node.evaluate(formula, e=2)[0]
|
||||
expected = 2 * math.sin(math.e * math.pi)
|
||||
assert result == pytest.approx(expected)
|
||||
|
||||
def test_whitespace_handling():
|
||||
"""Test that the parser correctly handles different whitespace patterns."""
|
||||
node = MathFormula()
|
||||
|
||||
# No spaces
|
||||
assert node.evaluate("a+b*c", a=1, b=2, c=3)[0] == pytest.approx(7.0)
|
||||
|
||||
# Lots of spaces
|
||||
assert node.evaluate("a + b * c", a=1, b=2, c=3)[0] == pytest.approx(7.0)
|
||||
|
||||
# Whitespace around unary minus
|
||||
assert node.evaluate("- a", a=5)[0] == pytest.approx(-5.0)
|
||||
assert node.evaluate("- a", a=5)[0] == pytest.approx(-5.0)
|
||||
assert node.evaluate("a * - b", a=3, b=4)[0] == pytest.approx(-12.0)
|
||||
assert node.evaluate("a * - b", a=3, b=4)[0] == pytest.approx(-12.0)
|
||||
assert node.evaluate("- (a + b)", a=2, b=3)[0] == pytest.approx(-5.0)
|
||||
assert node.evaluate("- (a + b)", a=2, b=3)[0] == pytest.approx(-5.0)
|
||||
|
||||
# Mixed spacing around functions
|
||||
assert node.evaluate("sin( a )+cos(b)", a=math.pi/2, b=0)[0] == pytest.approx(2.0)
|
||||
|
||||
# Spacing around constants
|
||||
assert node.evaluate("2*pi( )", a=1)[0] == pytest.approx(2 * math.pi)
|
||||
assert node.evaluate("e( )**2")[0] == pytest.approx(math.e ** 2)
|
||||
|
||||
def test_tokenizing():
|
||||
"""Test the tokenization functionality."""
|
||||
node = MathFormula()
|
||||
|
||||
# Test basic tokenizing
|
||||
tokens = node.tokenize_formula("a + b * c")
|
||||
assert "a" in tokens
|
||||
assert "+" in tokens
|
||||
assert "b" in tokens
|
||||
assert "*" in tokens
|
||||
assert "c" in tokens
|
||||
|
||||
# Test tokenizing functions
|
||||
tokens = node.tokenize_formula("sin(a) + cos(b)")
|
||||
assert "sin" in tokens
|
||||
assert "(" in tokens
|
||||
assert "a" in tokens
|
||||
assert ")" in tokens
|
||||
assert "+" in tokens
|
||||
assert "cos" in tokens
|
||||
assert "b" in tokens
|
||||
|
||||
# Test tokenizing constants with parentheses
|
||||
tokens = node.tokenize_formula("pi() + e()")
|
||||
assert "pi" in tokens
|
||||
assert "(" in tokens
|
||||
assert ")" in tokens
|
||||
assert "+" in tokens
|
||||
assert "e" in tokens
|
||||
|
||||
# Test complex expression
|
||||
tokens = node.tokenize_formula("a + b * (c - d) / e() ** 2")
|
||||
assert all(token in tokens for token in ["a", "+", "b", "*", "(", "c", "-", "d", ")", "/", "e", "(", ")", "**", "2"])
|
||||
|
||||
def test_infix_to_postfix():
|
||||
"""Test the infix to postfix conversion."""
|
||||
node = MathFormula()
|
||||
|
||||
# Test operator precedence
|
||||
formula = "a + b * c"
|
||||
# We can't test exact postfix because it contains internal representations,
|
||||
# but we can test the evaluation result
|
||||
assert node.evaluate(formula, a=1, b=2, c=3)[0] == pytest.approx(7.0)
|
||||
|
||||
# Test parentheses
|
||||
formula = "(a + b) * c"
|
||||
assert node.evaluate(formula, a=1, b=2, c=3)[0] == pytest.approx(9.0)
|
||||
|
||||
# Test function calls
|
||||
formula = "sin(a) + cos(b)"
|
||||
assert node.evaluate(formula, a=math.pi/2, b=0)[0] == pytest.approx(2.0)
|
||||
|
||||
# Test constant function calls
|
||||
formula = "pi() + e()"
|
||||
assert node.evaluate(formula)[0] == pytest.approx(math.pi + math.e)
|
||||
@@ -5,7 +5,7 @@ import platform
|
||||
from src.basic_data_handling.path_nodes import (
|
||||
PathJoin, PathAbspath, PathExists, PathIsFile, PathIsDir, PathGetSize,
|
||||
PathSplit, PathSplitExt, PathBasename, PathDirname, PathGetExtension,
|
||||
PathNormalize, PathRelative, PathGlob, PathExpandVars, PathGetCwd,
|
||||
PathSetExtension, PathNormalize, PathRelative, PathGlob, PathExpandVars, PathGetCwd,
|
||||
PathListDir, PathIsAbsolute, PathCommonPrefix,
|
||||
)
|
||||
|
||||
@@ -199,6 +199,24 @@ def test_path_get_extension():
|
||||
# Test with empty string
|
||||
assert node.get_extension("") == ("",)
|
||||
|
||||
def test_path_set_extension():
|
||||
node = PathSetExtension()
|
||||
# Test basic extension replacement
|
||||
assert node.set_extension("file.txt", ".jpg") == ("file.jpg",)
|
||||
# Test adding extension to file without extension
|
||||
assert node.set_extension("file", ".png") == ("file.png",)
|
||||
# Test with extension without dot prefix
|
||||
assert node.set_extension("document.docx", "pdf") == ("document.pdf",)
|
||||
# Test with path containing directory
|
||||
assert node.set_extension("folder/image.jpg", ".png") == ("folder/image.png",)
|
||||
# Test with multiple dots in filename
|
||||
assert node.set_extension("archive.tar.gz", ".zip") == ("archive.tar.zip",)
|
||||
# Test with empty extension (removes extension)
|
||||
assert node.set_extension("config.ini", "") == ("config",)
|
||||
# Test with empty string as path
|
||||
assert node.set_extension("", ".txt") == (".txt",)
|
||||
|
||||
|
||||
|
||||
def test_path_normalize():
|
||||
node = PathNormalize()
|
||||
|
||||
Reference in New Issue
Block a user