A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
—
Hierarchical exception system covering syntax errors, evaluation errors, type mismatches, and symbol resolution failures. The error handling system provides detailed error information, suggestions for fixes, and proper exception hierarchies for comprehensive error management.
The foundation exception classes that all rule engine errors inherit from.
class EngineError(Exception):
"""Base exception class for all rule engine errors."""
def __init__(self, message: str = ''):
"""
Initialize the engine error.
Args:
message (str): Description of the error
"""
@property
def message(self) -> str:
"""The error message description."""Usage Example:
import rule_engine
try:
# Any rule engine operation
rule = rule_engine.Rule('invalid syntax ===')
except rule_engine.EngineError as e:
print(f"Rule engine error: {e.message}")
print(f"Error type: {type(e).__name__}")Errors that occur during rule evaluation and expression processing.
class EvaluationError(EngineError):
"""
Errors that occur during rule evaluation, including type mismatches,
function call failures, and expression evaluation issues.
"""Usage Example:
import rule_engine
try:
rule = rule_engine.Rule('name + age') # Can't add string + number
rule.evaluate({'name': 'John', 'age': 25})
except rule_engine.EvaluationError as e:
print(f"Evaluation failed: {e.message}")Errors related to rule expression syntax and parsing.
class RuleSyntaxError(EngineError):
"""Errors in rule expression grammar and syntax."""
def __init__(self, message: str, token=None):
"""
Initialize syntax error.
Args:
message (str): Description of the syntax error
token: PLY token related to the error (if available)
"""
@property
def token(self):
"""The PLY token related to the syntax error."""Usage Example:
import rule_engine
try:
rule = rule_engine.Rule('name == "John" and and age > 18') # Double 'and'
except rule_engine.RuleSyntaxError as e:
print(f"Syntax error: {e.message}")
if e.token:
print(f"Error at line {e.token.lineno}, position {e.token.lexpos}")
# Common syntax errors
syntax_errors = [
'name === "value"', # Invalid operator
'if (condition', # Missing closing parenthesis
'name == "unclosed', # Unclosed string
'3.14.15', # Invalid float format
'regex =~ "[unclosed', # Invalid regex
]
for expr in syntax_errors:
try:
rule_engine.Rule(expr)
except rule_engine.RuleSyntaxError as e:
print(f"'{expr}' -> {e.message}")Errors related to resolving symbols and accessing object members.
Raised when a symbol cannot be resolved to a value.
class SymbolResolutionError(EvaluationError):
"""Error when a symbol name cannot be resolved."""
def __init__(self, symbol_name: str, symbol_scope: str = None,
thing=UNDEFINED, suggestion: str = None):
"""
Initialize symbol resolution error.
Args:
symbol_name (str): Name of the unresolved symbol
symbol_scope (str): Scope where symbol should be valid
thing: Root object used for resolution
suggestion (str): Optional suggestion for correct symbol name
"""
@property
def symbol_name(self) -> str:
"""The name of the symbol that couldn't be resolved."""
@property
def symbol_scope(self) -> str:
"""The scope where the symbol should be valid."""
@property
def thing(self):
"""The root object used for symbol resolution."""
@property
def suggestion(self) -> str:
"""Optional suggestion for a correct symbol name."""Usage Example:
import rule_engine
try:
rule = rule_engine.Rule('unknown_field == "value"')
rule.matches({'known_field': 'value'})
except rule_engine.SymbolResolutionError as e:
print(f"Unknown symbol: '{e.symbol_name}'")
if e.suggestion:
print(f"Did you mean: '{e.suggestion}'")
if e.thing is not rule_engine.UNDEFINED:
available = list(e.thing.keys()) if hasattr(e.thing, 'keys') else dir(e.thing)
print(f"Available symbols: {available}")Raised when an object attribute cannot be accessed.
class AttributeResolutionError(EvaluationError):
"""Error when an attribute cannot be resolved on an object."""
def __init__(self, attribute_name: str, object_, thing=UNDEFINED, suggestion: str = None):
"""
Initialize attribute resolution error.
Args:
attribute_name (str): Name of the unresolved attribute
object_: Object where attribute access was attempted
thing: Root object used for resolution
suggestion (str): Optional suggestion for correct attribute name
"""
@property
def attribute_name(self) -> str:
"""The name of the attribute that couldn't be resolved."""
@property
def object(self):
"""The object where attribute access was attempted."""
@property
def thing(self):
"""The root object used for resolution."""
@property
def suggestion(self) -> str:
"""Optional suggestion for a correct attribute name."""Usage Example:
import rule_engine
class Person:
def __init__(self, name):
self.name = name
self.first_name = name.split()[0]
try:
context = rule_engine.Context(resolver=rule_engine.resolve_attribute)
rule = rule_engine.Rule('firstname', context=context) # Typo: should be 'first_name'
rule.evaluate(Person('John Doe'))
except rule_engine.AttributeResolutionError as e:
print(f"Attribute '{e.attribute_name}' not found")
if e.suggestion:
print(f"Did you mean: '{e.suggestion}'")
print(f"Available attributes: {[attr for attr in dir(e.object) if not attr.startswith('_')]}")Errors related to type mismatches and type validation.
Raised when a symbol resolves to a value of the wrong type.
class SymbolTypeError(EvaluationError):
"""Error when a symbol resolves to an incompatible type."""
def __init__(self, symbol_name: str, is_value, is_type, expected_type):
"""
Initialize symbol type error.
Args:
symbol_name (str): Name of the symbol with wrong type
is_value: The actual Python value
is_type: The actual DataType
expected_type: The expected DataType
"""
@property
def symbol_name(self) -> str:
"""The name of the symbol with incorrect type."""
@property
def is_value(self):
"""The actual Python value of the symbol."""
@property
def is_type(self):
"""The actual DataType of the symbol."""
@property
def expected_type(self):
"""The expected DataType for the symbol."""Raised when an attribute resolves to a value of the wrong type.
class AttributeTypeError(EvaluationError):
"""Error when an attribute resolves to an incompatible type."""
def __init__(self, attribute_name: str, object_type, is_value, is_type, expected_type):
"""
Initialize attribute type error.
Args:
attribute_name (str): Name of the attribute with wrong type
object_type: The object type where attribute was resolved
is_value: The actual Python value
is_type: The actual DataType
expected_type: The expected DataType
"""
@property
def attribute_name(self) -> str:
"""The name of the attribute with incorrect type."""
@property
def object_type(self):
"""The object on which the attribute was resolved."""
@property
def is_value(self):
"""The actual Python value of the attribute."""
@property
def is_type(self):
"""The actual DataType of the attribute."""
@property
def expected_type(self):
"""The expected DataType for the attribute."""Usage Example:
import rule_engine
# Type error example
type_map = {
'age': rule_engine.DataType.FLOAT,
'name': rule_engine.DataType.STRING
}
context = rule_engine.Context(type_resolver=rule_engine.type_resolver_from_dict(type_map))
try:
# This should work
rule = rule_engine.Rule('age > 18', context=context)
result = rule.matches({'age': 25, 'name': 'John'})
# This will cause a type error if age is provided as string
result = rule.matches({'age': "twenty-five", 'name': 'John'})
except rule_engine.SymbolTypeError as e:
print(f"Type error for symbol '{e.symbol_name}':")
print(f" Expected: {e.expected_type.name}")
print(f" Got: {e.is_type.name} ({e.is_value})")Specific syntax errors for different data types and constructs.
class BytesSyntaxError(EngineError):
"""Error in bytes literal syntax."""
def __init__(self, message: str, value: str):
"""
Args:
message (str): Error description
value (str): The malformed bytes value
"""
@property
def value(self) -> str:
"""The bytes value that caused the error."""
class StringSyntaxError(EngineError):
"""Error in string literal syntax."""
def __init__(self, message: str, value: str):
"""
Args:
message (str): Error description
value (str): The malformed string value
"""
@property
def value(self) -> str:
"""The string value that caused the error."""
class DatetimeSyntaxError(EngineError):
"""Error in datetime literal syntax."""
def __init__(self, message: str, value: str):
"""
Args:
message (str): Error description
value (str): The malformed datetime value
"""
@property
def value(self) -> str:
"""The datetime value that caused the error."""
class FloatSyntaxError(EngineError):
"""Error in float literal syntax."""
def __init__(self, message: str, value: str):
"""
Args:
message (str): Error description
value (str): The malformed float value
"""
@property
def value(self) -> str:
"""The float value that caused the error."""
class TimedeltaSyntaxError(EngineError):
"""Error in timedelta literal syntax."""
def __init__(self, message: str, value: str):
"""
Args:
message (str): Error description
value (str): The malformed timedelta value
"""
@property
def value(self) -> str:
"""The timedelta value that caused the error."""
class RegexSyntaxError(EngineError):
"""Error in regular expression syntax."""
def __init__(self, message: str, error, value: str):
"""
Args:
message (str): Error description
error: The original re.error exception
value (str): The malformed regex pattern
"""
@property
def error(self):
"""The original re.error exception."""
@property
def value(self) -> str:
"""The regex pattern that caused the error."""Usage Example:
import rule_engine
# Examples of syntax errors for different data types
syntax_test_cases = [
('dt"2023-13-45"', rule_engine.DatetimeSyntaxError), # Invalid date
('3.14.15.9', rule_engine.FloatSyntaxError), # Invalid float
('td"25:70:80"', rule_engine.TimedeltaSyntaxError), # Invalid timedelta
('b"\\xGH"', rule_engine.BytesSyntaxError), # Invalid bytes escape
('"unclosed string', rule_engine.StringSyntaxError), # Unclosed string
('name =~ "[unclosed"', rule_engine.RegexSyntaxError), # Invalid regex
]
for expr, expected_error in syntax_test_cases:
try:
rule_engine.Rule(expr)
except expected_error as e:
print(f"'{expr}' -> {type(e).__name__}: {e.message}")
if hasattr(e, 'value'):
print(f" Problematic value: '{e.value}'")Errors that occur when calling functions within expressions.
class FunctionCallError(EvaluationError):
"""Error during function execution."""
def __init__(self, message: str, error=None, function_name: str = None):
"""
Args:
message (str): Error description
error: The original exception that caused this error
function_name (str): Name of the function that failed
"""
@property
def error(self):
"""The original exception that caused this error."""
@property
def function_name(self) -> str:
"""The name of the function that failed."""Errors that occur during container lookups (similar to IndexError/KeyError).
class LookupError(EvaluationError):
"""Error when lookup operations fail on containers."""
def __init__(self, container, item):
"""
Args:
container: The container object where lookup failed
item: The key/index that was used for lookup
"""
@property
def container(self):
"""The container where the lookup failed."""
@property
def item(self):
"""The key/index used for the failed lookup."""Usage Example:
import rule_engine
# Function call errors
try:
rule = rule_engine.Rule('unknown_function(42)')
rule.evaluate({})
except rule_engine.FunctionCallError as e:
print(f"Function error: {e.message}")
if e.function_name:
print(f"Failed function: {e.function_name}")
# Lookup errors
try:
rule = rule_engine.Rule('data[missing_key]')
rule.evaluate({'data': {'existing_key': 'value'}})
except rule_engine.LookupError as e:
print(f"Lookup failed: container={type(e.container)}, item={e.item}")import rule_engine
def safe_rule_evaluation(rule_text, data, context=None):
"""Safely evaluate a rule with comprehensive error handling."""
try:
rule = rule_engine.Rule(rule_text, context=context)
return rule.matches(data), None
except rule_engine.RuleSyntaxError as e:
return False, f"Syntax error: {e.message}"
except rule_engine.SymbolResolutionError as e:
error_msg = f"Unknown symbol: {e.symbol_name}"
if e.suggestion:
error_msg += f" (did you mean: {e.suggestion}?)"
return False, error_msg
except rule_engine.EvaluationError as e:
return False, f"Evaluation error: {e.message}"
except rule_engine.EngineError as e:
return False, f"Rule engine error: {e.message}"
# Usage
result, error = safe_rule_evaluation('age > 18', {'age': 25})
if error:
print(f"Error: {error}")
else:
print(f"Result: {result}")import rule_engine
import logging
logger = logging.getLogger(__name__)
def debug_rule_execution(rule_text, data, context=None):
"""Execute rule with detailed error logging."""
try:
logger.info(f"Evaluating rule: {rule_text}")
rule = rule_engine.Rule(rule_text, context=context)
result = rule.matches(data)
logger.info(f"Rule evaluation successful: {result}")
return result
except rule_engine.RuleSyntaxError as e:
logger.error(f"Syntax error in rule '{rule_text}': {e.message}")
if e.token:
logger.error(f"Error location: line {e.token.lineno}, pos {e.token.lexpos}")
raise
except rule_engine.SymbolResolutionError as e:
logger.error(f"Symbol resolution failed: {e.symbol_name}")
if e.suggestion:
logger.info(f"Suggested correction: {e.suggestion}")
available_symbols = list(data.keys()) if hasattr(data, 'keys') else dir(data)
logger.debug(f"Available symbols: {available_symbols}")
raise
except rule_engine.EvaluationError as e:
logger.error(f"Evaluation failed for rule '{rule_text}': {e.message}")
logger.debug(f"Data: {data}")
raiseimport rule_engine
class RuleProcessor:
"""Process rules with fallback strategies."""
def __init__(self, fallback_result=False):
self.fallback_result = fallback_result
self.error_count = 0
def evaluate_with_fallback(self, rule_text, data, context=None):
"""Evaluate rule with fallback on errors."""
try:
rule = rule_engine.Rule(rule_text, context=context)
return rule.matches(data)
except rule_engine.EngineError as e:
self.error_count += 1
logging.warning(f"Rule evaluation failed, using fallback: {e.message}")
return self.fallback_result
def get_error_rate(self, total_evaluations):
"""Calculate error rate for monitoring."""
return self.error_count / total_evaluations if total_evaluations > 0 else 0Install with Tessl CLI
npx tessl i tessl/pypi-rule-engine