CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-rule-engine

A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.

Pending
Overview
Eval results
Files

error-handling.mddocs/

Error Handling

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.

Capabilities

Base Exception Classes

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__}")

Evaluation Errors

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}")

Syntax Errors

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}")

Symbol Resolution Errors

Errors related to resolving symbols and accessing object members.

Symbol Resolution Error

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}")

Attribute Resolution Error

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('_')]}")

Type-Related Errors

Errors related to type mismatches and type validation.

Symbol Type Error

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."""

Attribute Type Error

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})")

Specialized Syntax Errors

Specific syntax errors for different data types and constructs.

Data Type Syntax Errors

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}'")

Other Specialized Errors

Function Call Error

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."""

Lookup Error

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}")

Error Handling Best Practices

Comprehensive Error Handling

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}")

Error Logging and Debugging

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}")
        raise

Graceful Degradation

import 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 0

Install with Tessl CLI

npx tessl i tessl/pypi-rule-engine

docs

context-management.md

core-operations.md

error-handling.md

index.md

type-system.md

tile.json