diff --git a/.gitignore b/.gitignore index 01c98cd..519f255 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ htmlcov/ # Rust target/ +# UV +uv.lock + # Project specific site/ *.so \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c6150b9..174142a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,16 +3,16 @@ version: 2 build: os: ubuntu-24.04 tools: - python: "3.11" + python: "3.12" jobs: - pre_build: - - curl -LsSf https://astral.sh/uv/install.sh | sh - - source $HOME/.cargo/env - - uv sync + pre_create_environment: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + create_environment: + - uv venv "${READTHEDOCS_VIRTUALENV_PATH}" + install: + - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --group docs mkdocs: - configuration: mkdocs.yml - -python: - install: - - requirements: docs/requirements.txt \ No newline at end of file + configuration: mkdocs.yml \ No newline at end of file diff --git a/docs/how-to-guides/cli-recipes.md b/docs/how-to-guides/cli-recipes.md index b051c44..4efbca1 100644 --- a/docs/how-to-guides/cli-recipes.md +++ b/docs/how-to-guides/cli-recipes.md @@ -126,7 +126,7 @@ fi cel -i # Example session: -CEL> :context user='{"name": "Alice", "role": "admin", "verified": true}' +CEL> context {"user": {"name": "Alice", "role": "admin", "verified": true}} Context updated: user CEL> user.name @@ -138,27 +138,29 @@ true CEL> user.verified && user.role in ["admin", "moderator"] true -CEL> :load-context permissions.json -Context loaded from permissions.json +CEL> load permissions.json +Loaded context from permissions.json CEL> "write" in permissions true -CEL> :history +CEL> history 1: user.name 2: user.role == "admin" 3: user.verified && user.role in ["admin", "moderator"] 4: "write" in permissions -CEL> :exit +CEL> exit ``` ### Rapid Prototyping ```bash -# Test expressions quickly +# Test expressions quickly: cel -i -CEL> :context data='{"users": [{"name": "Alice", "active": true}, {"name": "Bob", "active": false}]}' +CEL> context {"data": {"users": [{"name": "Alice", "active": true}, {"name": "Bob", "active": false}]}} +Context updated: data + CEL> data.users.filter(u, u.active).map(u, u.name) ["Alice"] @@ -455,9 +457,11 @@ cel --verbose 'expression' --context-file context.json # Validate JSON files before using them cat context.json | python -m json.tool -# Test expressions step by step in interactive mode +# Test expressions step by step in interactive mode: cel -i -CEL> :context test_data='{"user": {"name": "Alice"}}' +CEL> context {"user": {"name": "Alice"}} +Context updated: user + CEL> has(user) true CEL> has(user.name) @@ -500,8 +504,11 @@ cel 'expression' --context-file "$(pwd)/context.json" # 4. Test context loading in interactive mode cel -i -CEL> :load-context context.json -CEL> :show-context +CEL> load context.json +Loaded context from context.json + +CEL> context +{"user": {"name": "Alice"}} ``` #### Type conversion issues @@ -517,9 +524,11 @@ cel 'string(age) + " years"' --context '{"age": 30}' # Instead of: int_string > 10 cel 'int(int_string) > 10' --context '{"int_string": "15"}' -# Check types in interactive mode +# Check types in interactive mode: cel -i -CEL> :context data='{"value": "123"}' +CEL> context {"data": {"value": "123"}} +Context updated: data + CEL> typeof(data.value) string CEL> int(data.value) diff --git a/docs/how-to-guides/production-patterns-best-practices.md b/docs/how-to-guides/production-patterns-best-practices.md index f755afb..11e2ca6 100644 --- a/docs/how-to-guides/production-patterns-best-practices.md +++ b/docs/how-to-guides/production-patterns-best-practices.md @@ -462,36 +462,98 @@ import time from cel import evaluate def benchmark_cel_performance(): - # Simple expressions - simple_expr = "x + y * 2" - context = {"x": 10, "y": 20} + """Comprehensive CEL performance benchmark matching documented claims.""" - iterations = 1000 # Reduced for testing - start_time = time.perf_counter() + # Test scenarios matching the performance table + test_cases = [ + { + "name": "Simple expressions", + "expression": "x + y * 2", + "context": {"x": 10, "y": 20}, + "expected": 50, + "iterations": 10000 + }, + { + "name": "Complex expressions", + "expression": "user.active && user.role in ['admin', 'editor'] && has(user.permissions) && user.permissions.size() > 0", + "context": { + "user": { + "active": True, + "role": "admin", + "permissions": ["read", "write", "delete"] + } + }, + "expected": True, + "iterations": 5000 + }, + { + "name": "Function calls", + "expression": "double(x) + square(y)", + "context": { + "x": 5, + "y": 3, + "double": lambda x: x * 2, + "square": lambda x: x * x + }, + "expected": 19, # double(5) + square(3) = 10 + 9 + "iterations": 3000 + } + ] - for _ in range(iterations): - result = evaluate(simple_expr, context) + results = [] - end_time = time.perf_counter() - avg_time_us = ((end_time - start_time) / iterations) * 1_000_000 - throughput = iterations / (end_time - start_time) - - # Verify the benchmark ran correctly - assert avg_time_us > 0 - assert throughput > 0 + for test_case in test_cases: + print(f"\nBenchmarking: {test_case['name']}") + + # Verify the expression works correctly + result = evaluate(test_case["expression"], test_case["context"]) + assert result == test_case["expected"], f"Expected {test_case['expected']}, got {result}" + + # Warmup + for _ in range(100): + evaluate(test_case["expression"], test_case["context"]) + + # Benchmark + start_time = time.perf_counter() + for _ in range(test_case["iterations"]): + evaluate(test_case["expression"], test_case["context"]) + end_time = time.perf_counter() + + # Calculate metrics + total_time = end_time - start_time + avg_time_us = (total_time / test_case["iterations"]) * 1_000_000 + throughput = test_case["iterations"] / total_time + + result_data = { + "name": test_case["name"], + "avg_time_us": avg_time_us, + "throughput": throughput, + "iterations": test_case["iterations"] + } + results.append(result_data) + + print(f" Average time: {avg_time_us:.1f} μs") + print(f" Throughput: {throughput:,.0f} ops/sec") - # Test that the expression actually works - result = evaluate(simple_expr, context) - assert result == 50 # 10 + 20 * 2 + return results -# Run the benchmark -benchmark_cel_performance() +# Run the benchmark and display results +if __name__ == "__main__": + print("CEL Performance Benchmark") + print("=" * 40) + results = benchmark_cel_performance() + + print("\nSummary:") + print("-" * 40) + for result in results: + print(f"{result['name']:20} | {result['avg_time_us']:6.1f} μs | {result['throughput']:8,.0f} ops/sec") ``` **Expected Results**: -- **Modern hardware**: 5-50 μs per evaluation -- **Simple expressions**: 50,000+ ops/sec -- **Complex expressions**: 10,000+ ops/sec + +- **Simple expressions**: 5-15 μs per evaluation, 50,000+ ops/sec +- **Complex expressions**: 15-40 μs per evaluation, 25,000+ ops/sec +- **Function calls**: 20-50 μs per evaluation, 20,000+ ops/sec **Learn More**: See [Performance Benchmarking Examples](https://github.com/hardbyte/python-common-expression-language/tree/main/examples/performance) for comprehensive benchmarking scripts. diff --git a/docs/reference/cli-reference.md b/docs/reference/cli-reference.md index faa7d7f..d38d05a 100644 --- a/docs/reference/cli-reference.md +++ b/docs/reference/cli-reference.md @@ -87,7 +87,7 @@ cel -i In interactive mode, you can: - Enter expressions directly -- Use built-in commands (`:help`, `:context`, etc.) +- Use built-in commands (`help`, `context`, etc.) - Load context from files - View command history @@ -158,134 +158,85 @@ When in interactive mode (`cel -i`), these commands are available: ### Context Management -#### `:context =` -Set a context variable. +#### `context` +Display current context variables in a formatted table. ``` -CEL> :context name="Alice" -Context updated: name - -CEL> :context age=30 -Context updated: age - -CEL> name + " is " + string(age) -Alice is 30 +CEL> context + Context Variables +┏━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ +┃ Variable ┃ Type ┃ Value ┃ +┡━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ +│ user │ dict │ {'name': 'Alice'} │ +│ debug │ bool │ False │ +└──────────┴──────┴───────────────────┘ ``` -#### `:context ` -Set multiple context variables from JSON. +#### `context ` +Set context variables from JSON object. ``` -CEL> :context {"user": {"name": "Bob", "role": "admin"}, "debug": true} +CEL> context {"user": {"name": "Alice", "role": "admin"}, "debug": false} Context updated: user, debug -CEL> user.role -admin -``` - -#### `:show-context` -Display current context. - -``` -CEL> :show-context -{ - "name": "Alice", - "age": 30, - "user": { - "name": "Bob", - "role": "admin" - }, - "debug": true -} -``` - -#### `:clear-context` -Clear all context variables. - -``` -CEL> :clear-context -Context cleared - -CEL> :show-context -{} +CEL> context {"name": "Bob", "age": 30} +Context updated: name, age ``` -#### `:load-context ` +#### `load ` Load context from JSON file. ``` -CEL> :load-context user.json -Context loaded from user.json +CEL> load user.json +Loaded context from user.json -CEL> :load-context /path/to/config.json -Context loaded from /path/to/config.json +CEL> load /path/to/config.json +Loaded context from /path/to/config.json ``` ### History Management -#### `:history` +#### `history` Show command history. ``` -CEL> :history +CEL> history 1: 1 + 2 2: "hello".size() 3: user.name 4: user.role == "admin" ``` -#### `:replay ` -Replay command number n from history. - -``` -CEL> :replay 2 -4 - -CEL> :replay -1 -true -``` - -**Special values**: -- `` - Replay specific command number -- `-1` - Replay last command -- `-2` - Replay second-to-last command, etc. - -#### `:clear-history` -Clear command history. - -``` -CEL> :clear-history -History cleared -``` - ### Utility Commands -#### `:help` +#### `help` Show help message. ``` -CEL> :help -Available commands: - :context = - Set context variable - :show-context - Show current context - :clear-context - Clear all context - :load-context - Load context from file - :history - Show command history - :replay - Replay command n - :clear-history - Clear history - :help - Show this help - :exit - Exit REPL +CEL> help + REPL Commands +┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ Command ┃ Description ┃ +┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ +│ help │ Show this help message │ +│ context │ Show current context variables │ +│ context │ Set context variables from JSON │ +│ history │ Show expression history │ +│ load │ Load JSON context from file │ +│ exit/quit │ Exit the REPL │ +│ Ctrl-C │ Exit the REPL │ +└────────────────┴─────────────────────────────────┘ ``` -#### `:exit` +#### `exit` / `quit` Exit the interactive REPL. ``` -CEL> :exit +CEL> exit Goodbye! ``` -**Aliases**: `:quit`, `:q`, `Ctrl+D` +**Aliases**: `quit`, `Ctrl+D` 📚 **For practical usage examples, recipes, and integration patterns, see the [CLI Usage Recipes](../how-to-guides/cli-recipes.md) guide.** diff --git a/python/cel/cli.py b/python/cel/cli.py index 8de30d7..a87fe2a 100644 --- a/python/cel/cli.py +++ b/python/cel/cli.py @@ -203,7 +203,7 @@ def get_context_vars(self) -> Dict[str, Any]: return self.context.copy() -class EnhancedCELREPL: +class InteractiveCELREPL: """Enhanced REPL with prompt_toolkit features.""" def __init__(self, evaluator: CELEvaluator, history_limit: int = 10): @@ -239,7 +239,6 @@ def __init__(self, evaluator: CELEvaluator, history_limit: int = 10): # Command dispatch dictionary for cleaner organization self.commands = { "help": self._show_help, - "context": self._show_context, "history": self._show_history, } @@ -282,12 +281,21 @@ def run(self): continue # Handle REPL commands - command_parts = expression.strip().lower().split() - command = command_parts[0] if command_parts else "" + command_parts = expression.strip().split() + command = command_parts[0].lower() if command_parts else "" if command in ["exit", "quit"]: console.print("[yellow]Goodbye![/yellow]") break + elif command == "context": + if len(command_parts) > 1: + # Setting context: context {"key": "value"} + context_json = " ".join(command_parts[1:]) + self._set_context(context_json) + else: + # Showing context: context + self._show_context() + continue elif command in self.commands: self.commands[command]() continue @@ -329,6 +337,7 @@ def _show_help(self): help_table.add_row("help", "Show this help message") help_table.add_row("context", "Show current context variables") + help_table.add_row("context ", "Set context variables from JSON") help_table.add_row("history", "Show expression history") help_table.add_row("load ", "Load JSON context from file") help_table.add_row("exit/quit", "Exit the REPL") @@ -365,6 +374,34 @@ def _show_context(self): console.print(table) + def _set_context(self, context_json: str): + """Set context variables from JSON string.""" + try: + new_context = json.loads(context_json) + if not isinstance(new_context, dict): + console.print("[red]Error: Context must be a JSON object (dictionary)[/red]") + return + + # Update the context + self.evaluator.update_context(new_context) + + # Update completer with new context variables + self._update_completer() + + # Show what was updated + context_keys = list(new_context.keys()) + if len(context_keys) == 1: + console.print(f"[green]Context updated: {context_keys[0]}[/green]") + elif len(context_keys) <= 3: + console.print(f"[green]Context updated: {', '.join(context_keys)}[/green]") + else: + console.print(f"[green]Context updated: {len(context_keys)} variables[/green]") + + except json.JSONDecodeError as e: + console.print(f"[red]Error: Invalid JSON - {e}[/red]") + except Exception as e: + console.print(f"[red]Error updating context: {e}[/red]") + def _show_history(self): """Show expression history with Rich formatting.""" if not self.history: @@ -545,7 +582,7 @@ def main( # Interactive mode if interactive: - repl = EnhancedCELREPL(evaluator) + repl = InteractiveCELREPL(evaluator) repl.run() return diff --git a/tests/test_cli.py b/tests/test_cli.py index 2e25137..b8c5ecb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -21,7 +21,7 @@ from cel.cli import ( CELEvaluator, CELFormatter, - EnhancedCELREPL, + InteractiveCELREPL, evaluate_expressions_from_file, load_context_from_file, ) @@ -184,13 +184,13 @@ def test_evaluate_empty_expression_error(self): evaluator.evaluate(" ") -class TestEnhancedCELREPL: +class TestInteractiveCELREPL: """Test the Enhanced REPL functionality.""" def test_repl_initialization(self): """Test REPL initializes correctly.""" evaluator = CELEvaluator({"test": 42}) - repl = EnhancedCELREPL(evaluator) + repl = InteractiveCELREPL(evaluator) assert repl.evaluator == evaluator assert repl.history == [] @@ -203,27 +203,25 @@ def test_repl_initialization(self): # Test command dispatch dictionary assert "help" in repl.commands - assert "context" in repl.commands assert "history" in repl.commands def test_repl_command_dispatch(self): """Test that commands are properly mapped.""" evaluator = CELEvaluator() - repl = EnhancedCELREPL(evaluator) + repl = InteractiveCELREPL(evaluator) # Test that command methods exist assert callable(repl.commands["help"]) - assert callable(repl.commands["context"]) assert callable(repl.commands["history"]) assert repl.commands["help"] == repl._show_help - assert repl.commands["context"] == repl._show_context + assert repl.commands["history"] == repl._show_history def test_update_completer(self): """Test that completer updates with context variables.""" evaluator = CELEvaluator({"var1": 1, "var2": 2}) - repl = EnhancedCELREPL(evaluator) + repl = InteractiveCELREPL(evaluator) # Test initial completer setup completer_words = repl.session.completer.words @@ -235,7 +233,7 @@ def test_update_completer(self): def test_load_context_success(self): """Test successful context loading from file.""" evaluator = CELEvaluator() - repl = EnhancedCELREPL(evaluator) + repl = InteractiveCELREPL(evaluator) # Create temporary JSON file test_context = {"name": "test", "value": 42} @@ -261,7 +259,7 @@ def test_load_context_success(self): def test_load_context_file_not_found(self): """Test context loading with non-existent file.""" evaluator = CELEvaluator() - repl = EnhancedCELREPL(evaluator) + repl = InteractiveCELREPL(evaluator) with patch("cel.cli.console") as mock_console: repl._load_context("nonexistent.json") @@ -274,7 +272,7 @@ def test_load_context_file_not_found(self): def test_history_limit_enforcement(self): """Test that history is limited to prevent memory growth.""" evaluator = CELEvaluator() - repl = EnhancedCELREPL(evaluator, history_limit=3) + repl = InteractiveCELREPL(evaluator, history_limit=3) # Manually add items to history to test limit for i in range(5): @@ -440,7 +438,7 @@ def test_enhanced_formatter_architecture(self): def test_repl_command_parsing_with_spaces(self): """Test that REPL can handle commands with spaces in arguments.""" evaluator = CELEvaluator() - repl = EnhancedCELREPL(evaluator) + repl = InteractiveCELREPL(evaluator) # Create a temporary file with spaces in the name with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: