Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
run: uv run pytest --verbose --tb=short

lint:
name: Code Quality
name: Code Quality & Type Checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -87,6 +87,19 @@ jobs:

- name: Run Python linting
run: uv run ruff check .

- name: Test type stub files exist
run: |
test -f python/cel/cel.pyi || (echo "❌ Type stub file missing" && exit 1)
test -f python/cel/py.typed || (echo "❌ PEP 561 marker missing" && exit 1)
echo "✅ Type stub files present"

- name: Test ty type checker integration
run: |
echo "from cel import evaluate, Context; evaluate('1+1')" > test_types.py
uvx ty check test_types.py
rm test_types.py
echo "✅ Type checker integration working"
linux:
runs-on: ${{ matrix.platform.runner }}
needs: [test, lint]
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### ✨ Added

- **EvaluationMode enum**: Control type handling behavior in CEL expressions
- `EvaluationMode.PYTHON` (default for Python API): Python-friendly type promotions
- `EvaluationMode.STRICT` (default for CLI): Strict CEL type rules with no coercion
- **Type checking support**: Added complete type stub files (`.pyi`) for PyO3 extension



## [0.5.0] - 2025-08-08

Expand Down
12 changes: 12 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ def add_function(self, name: str, func: Callable) -> None:
"""
```

### Type Checking Support

This package includes manual type stub files (`.pyi`) for the PyO3 Rust extension to support static type checking.

**Type checker verification:**
```bash
uvx ty check your_file.py # ✅ Should pass without "no member" errors
uvx mypy your_file.py # ✅ Works with proper MYPYPATH setup
```

See https://pyo3.rs/main/type-stub to opt-in to the automated types when implemented in Maturin.

## Debugging & Troubleshooting

### Common Issues
Expand Down
6 changes: 6 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ The Common Expression Language (CEL) is a non-Turing complete language designed
# With context
cel 'age >= 21' --context '{"age": 25}' # → true

# Mixed arithmetic with modes
cel '1 + 2.5' # → Error (default: strict mode)
cel '1 + 2.5' --mode python # → 3.5 (python-friendly mode)
cel '1 + 2.5' --mode strict # → Error (strict CEL rules)

# Interactive REPL
cel --interactive
```
Expand Down Expand Up @@ -93,6 +98,7 @@ The Common Expression Language (CEL) is a non-Turing complete language designed
✅ **Safe by design** (Rust core)
✅ **Ready for production**
✅ **No GIL-blocking, safe concurrent evaluation**
✅ **Flexible type handling** (Python-friendly + strict modes)

## Why Python CEL?

Expand Down
21 changes: 21 additions & 0 deletions docs/reference/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ Enable debug mode with detailed error information.
cel --debug 'user.role == "admin"' --context-file user.json
```

#### `--mode`, `-m`
Set the evaluation mode for type handling.

```bash
cel '1 + 2.5' --mode python # → 3.5 (python-friendly type promotions)
cel '1 + 2.5' --mode strict # → Error (default: strict CEL type rules)
cel '1 + 2.5' -m python # → 3.5 (short form)
```

**Values**:
- `python` - Python-friendly type promotions for mixed arithmetic
- `strict` (default) - Strict CEL type rules with no automatic coercion

**Use Cases**:
- `python`: Best for JSON APIs, user-friendly applications, Python integration
- `strict`: Best for CEL spec compliance, type precision, cross-language consistency

### Context Options

#### `--context`, `-c`
Expand Down Expand Up @@ -250,6 +267,10 @@ cel 'expression'
cel 'expression' --context '{"key": "value"}'
cel 'expression' --context-file context.json

# With evaluation mode
cel 'expression' --mode python # Python-friendly mixed arithmetic
cel 'expression' --mode strict # Default: strict CEL type rules

# Interactive mode
cel --interactive
```
Expand Down
126 changes: 125 additions & 1 deletion docs/reference/python-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,97 @@ Complete autogenerated reference for the Python CEL library.

::: cel.evaluate

## Enums

### EvaluationMode

**Controls how CEL expressions handle type compatibility.**

The EvaluationMode enum allows you to choose between Python-friendly type coercion (default) and strict CEL type enforcement:

```python
from cel import evaluate, EvaluationMode

# Python-friendly mode (default) - allows mixed arithmetic
result = evaluate("1 + 2.5") # → 3.5
result = evaluate("1 + 2.5", mode=EvaluationMode.PYTHON) # → 3.5
result = evaluate("1 + 2.5", mode="python") # → 3.5

# Strict mode - enforces strict CEL type rules
try:
evaluate("1 + 2.5", mode=EvaluationMode.STRICT) # TypeError
except TypeError as e:
print(f"Strict mode error: {e}") # → "Unsupported addition operation"

try:
evaluate("1 + 2.5", mode="strict") # TypeError
except TypeError as e:
print(f"Strict mode error: {e}") # → "Unsupported addition operation"
```

#### Values

**EvaluationMode.PYTHON** (or `"python"`)
*Default mode for Python API.* Enables Python-friendly type promotions for better mixed arithmetic compatibility:
- Integer literals are automatically promoted to floats when used with floats
- Context variables are promoted from int to float when mixed arithmetic is detected
- Expression preprocessing converts `1 + 2.5` to `1.0 + 2.5` for compatibility

**EvaluationMode.STRICT** (or `"strict"`)
*Default mode for CLI.* Enforces strict CEL type rules with no automatic coercion:
- Mixed int/float arithmetic raises TypeError
- No type promotions or expression rewriting
- Matches WebAssembly CEL behavior exactly

#### Default Modes

**Note**: The Python API and CLI have different defaults:
- **Python API**: Defaults to `EvaluationMode.PYTHON` for seamless integration with Python code
- **CLI**: Defaults to `EvaluationMode.STRICT` for CEL specification compliance and testing

#### When to Use Each Mode

**Use PYTHON mode (Python API default) when:**
- Integrating with existing Python code that expects mixed numeric types
- Working with data from JSON APIs (which often mix ints and floats)
- Building user-friendly applications where type coercion feels natural
- Migrating from pure Python evaluation logic

**Use STRICT mode (CLI default) when:**
- Building applications that need to match CEL implementations in other languages
- Working with systems where type precision is critical
- Following strict CEL specification compliance
- Debugging type-related issues in expressions

#### Implementation Details

In PYTHON mode, the library:
1. Analyzes context for mixed numeric types (int + float)
2. Promotes integers to floats in both context variables and expression literals
3. Preprocesses expressions like `1 + 2.5` to become `1.0 + 2.5`

In STRICT mode, the library:
1. Performs no type promotions or expression rewriting
2. Passes expressions directly to the CEL evaluator
3. Raises TypeError for incompatible type operations

**Example with Context:**

```python
from cel import evaluate, EvaluationMode

context = {"x": 1, "y": 2.5} # Mixed int/float in context

# Python mode handles mixed types gracefully
result = evaluate("x + y", context, mode=EvaluationMode.PYTHON) # → 3.5

# Strict mode rejects mixed types
try:
evaluate("x + y", context, mode=EvaluationMode.STRICT) # TypeError
except TypeError as e:
print("Mixed types not allowed in strict mode")
```

## Classes

### Context
Expand Down Expand Up @@ -292,12 +383,45 @@ except TypeError as e:
assert "Unsupported multiplication operation" in str(e)
```

#### EvaluationMode-Specific Errors

**Strict mode can produce additional TypeError exceptions:**

```python
from cel import evaluate, EvaluationMode

# Mixed numeric types in strict mode
try:
evaluate("1 + 2.5", mode=EvaluationMode.STRICT)
except TypeError as e:
assert "Unsupported addition operation" in str(e)
print(f"Strict mode type error: {e}")

# Mixed types from context in strict mode
context = {"int_val": 10, "float_val": 2.5}
try:
evaluate("int_val * float_val", context, mode=EvaluationMode.STRICT)
except TypeError as e:
assert "Unsupported multiplication operation" in str(e)
print(f"Context type mixing error: {e}")
```

**Invalid mode strings raise TypeError:**

```python
try:
evaluate("1 + 2", mode="invalid_mode")
except TypeError as e:
assert "Invalid EvaluationMode" in str(e)
print(f"Invalid mode error: {e}")
```

### Production Error Handling

For comprehensive error handling patterns, safety guidelines, and production best practices, see the **[Error Handling How-To Guide](../how-to-guides/error-handling.md)** which covers:

- Safe handling of malformed expressions and untrusted input
- Safe evaluation wrappers
- Safe evaluation wrappers with EvaluationMode considerations
- Context validation patterns
- Defensive expression techniques
- Logging and monitoring
Expand Down
12 changes: 2 additions & 10 deletions python/cel/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
# Import the Rust extension
# Import CLI functionality

from . import cli
from .cel import *

__doc__ = cel.__doc__
if hasattr(cel, "__all__"):
__all__ = cel.__all__
else:
__all__ = [
"evaluate",
"Context",
]
from .evaluation_modes import EvaluationMode
53 changes: 53 additions & 0 deletions python/cel/cel.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Type stubs for the CEL Rust extension module.
"""

from typing import Any, Callable, Dict, Literal, Optional, Union, overload

from .evaluation_modes import EvaluationMode

class Context:
"""CEL evaluation context for variables and functions."""

@overload
def __init__(self) -> None: ...
@overload
def __init__(self, variables: Dict[str, Any]) -> None: ...
@overload
def __init__(
self,
variables: Optional[Dict[str, Any]] = None,
*,
functions: Optional[Dict[str, Callable[..., Any]]] = None,
) -> None: ...
def add_variable(self, name: str, value: Any) -> None:
"""Add a variable to the context."""
...

def add_function(self, name: str, func: Callable[..., Any]) -> None:
"""Add a function to the context."""
...

def update(self, variables: Dict[str, Any]) -> None:
"""Update context with variables from a dictionary."""
...

def evaluate(
expression: str,
context: Optional[Union[Dict[str, Any], Context]] = None,
*,
mode: Union[Literal["python", "strict"], "EvaluationMode", str] = "python",
) -> Any:
"""
Evaluate a CEL expression.

Args:
expression: The CEL expression to evaluate
context: Optional context with variables and functions
mode: Evaluation mode - either "python" (default) for mixed arithmetic
or "strict" for strict type matching

Returns:
The result of evaluating the expression
"""
...
Loading
Loading