Skip to content

Commit ce854de

Browse files
committed
Test evaluation mode and slight refactor
1 parent 79ad64f commit ce854de

File tree

7 files changed

+180
-39
lines changed

7 files changed

+180
-39
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,7 @@ jobs:
9393
test -f python/cel/cel.pyi || (echo "❌ Type stub file missing" && exit 1)
9494
test -f python/cel/py.typed || (echo "❌ PEP 561 marker missing" && exit 1)
9595
echo "✅ Type stub files present"
96-
97-
- name: Run type checking tests
98-
run: uv run pytest tests/test_type_stubs.py -v
99-
96+
10097
- name: Test ty type checker integration
10198
run: |
10299
echo "from cel import evaluate, Context; evaluate('1+1')" > test_types.py

docs/reference/python-api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ In STRICT mode, the library:
8181
3. Raises TypeError for incompatible type operations
8282

8383
**Example with Context:**
84+
8485
```python
8586
from cel import evaluate, EvaluationMode
8687

python/cel/__init__.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,5 @@
11
# Import the Rust extension
2-
# Import CLI functionality
3-
from enum import Enum
42

53
from . import cli
64
from .cel import *
7-
8-
9-
class EvaluationMode(str, Enum):
10-
"""
11-
Defines the evaluation dialect for a CEL expression.
12-
"""
13-
14-
PYTHON = "python"
15-
"""
16-
Enables Python-friendly type promotions (e.g., int -> float). (Default)
17-
"""
18-
STRICT = "strict"
19-
"""
20-
Enforces strict cel-rust type rules with no automatic coercion to match Wasm behavior.
21-
"""
5+
from .evaluation_modes import EvaluationMode

python/cel/cel.pyi

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
Type stubs for the CEL Rust extension module.
33
"""
44

5-
from typing import TYPE_CHECKING, Any, Callable, Dict, Literal, Optional, Union, overload
5+
from typing import Any, Callable, Dict, Literal, Optional, Union, overload
66

7-
if TYPE_CHECKING:
8-
from . import EvaluationMode
7+
from .evaluation_modes import EvaluationMode
98

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

python/cel/cli.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,9 @@
3737
from rich.table import Table
3838
from typing_extensions import Annotated
3939

40-
try:
41-
from . import EvaluationMode, cel
42-
except ImportError:
43-
# Fallback for running as standalone script
44-
try:
45-
import cel
46-
from cel import EvaluationMode
47-
except ImportError:
48-
console = Console()
49-
console.print(
50-
"[red]Error: 'cel' package not found. Please install with: pip install common-expression-language[/red]"
51-
)
52-
sys.exit(1)
40+
# Import directly from relative modules to avoid circular imports
41+
from .cel import Context, evaluate
42+
from .evaluation_modes import EvaluationMode
5343

5444
# Initialize Rich console
5545
console = Console()
@@ -202,15 +192,15 @@ def __init__(self, context: Optional[Dict[str, Any]] = None, mode: str = "strict
202192
def _update_cel_context(self):
203193
"""Update the internal CEL context object."""
204194
if self.context:
205-
self._cel_context = cel.Context(self.context)
195+
self._cel_context = Context(self.context)
206196
else:
207197
self._cel_context = None
208198

209199
def evaluate(self, expression: str) -> Any:
210200
"""Evaluate a CEL expression."""
211201
if not expression.strip():
212202
raise ValueError("Empty expression")
213-
return cel.evaluate(expression, self._cel_context, mode=self.mode)
203+
return evaluate(expression, self._cel_context, mode=self.mode)
214204

215205
def update_context(self, new_context: Dict[str, Any]):
216206
"""Update the evaluation context."""

python/cel/evaluation_modes.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from enum import Enum
2+
3+
4+
class EvaluationMode(str, Enum):
5+
"""
6+
Defines the evaluation dialect for a CEL expression.
7+
"""
8+
9+
PYTHON = "python"
10+
"""
11+
Enables Python-friendly type promotions (e.g., int -> float). (Default)
12+
"""
13+
STRICT = "strict"
14+
"""
15+
Enforces strict cel-rust type rules with no automatic coercion to match Wasm behavior.
16+
"""

tests/test_evaluation_mode.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Tests for EvaluationMode functionality."""
2+
3+
import pytest
4+
from cel import Context, EvaluationMode, evaluate
5+
6+
7+
def test_evaluation_mode_enum_values():
8+
"""Test that EvaluationMode enum has expected values."""
9+
assert EvaluationMode.PYTHON == "python"
10+
assert EvaluationMode.STRICT == "strict"
11+
# String representation shows enum name, but equality works with values
12+
assert str(EvaluationMode.PYTHON) == "EvaluationMode.PYTHON"
13+
assert str(EvaluationMode.STRICT) == "EvaluationMode.STRICT"
14+
15+
16+
def test_default_mode_is_python_compatible():
17+
"""Test that the default evaluation mode allows mixed arithmetic."""
18+
# Without explicit mode, should use python by default
19+
result = evaluate("1 + 2.5")
20+
assert result == 3.5
21+
22+
23+
def test_python_mode_explicit():
24+
"""Test explicit python mode allows mixed arithmetic."""
25+
# Using enum
26+
result = evaluate("1 + 2.5", mode=EvaluationMode.PYTHON)
27+
assert result == 3.5
28+
29+
# Using string
30+
result = evaluate("1 + 2.5", mode="python")
31+
assert result == 3.5
32+
33+
34+
def test_strict_mode_rejects_mixed_arithmetic():
35+
"""Test that strict mode rejects mixed int/float arithmetic."""
36+
# Using enum
37+
with pytest.raises(TypeError, match="Unsupported addition operation"):
38+
evaluate("1 + 2.5", mode=EvaluationMode.STRICT)
39+
40+
# Using string
41+
with pytest.raises(TypeError, match="Unsupported addition operation"):
42+
evaluate("1 + 2.5", mode="strict")
43+
44+
45+
def test_same_type_arithmetic_works_in_both_modes():
46+
"""Test that same-type arithmetic works in both modes."""
47+
# Integer arithmetic
48+
assert evaluate("1 + 2", mode=EvaluationMode.PYTHON) == 3
49+
assert evaluate("1 + 2", mode=EvaluationMode.STRICT) == 3
50+
51+
# Float arithmetic
52+
assert evaluate("1.5 + 2.5", mode=EvaluationMode.PYTHON) == 4.0
53+
assert evaluate("1.5 + 2.5", mode=EvaluationMode.STRICT) == 4.0
54+
55+
56+
def test_context_with_mixed_types():
57+
"""Test evaluation modes with context containing mixed types."""
58+
context = {"x": 1, "y": 2.5}
59+
60+
# Python mode should promote and work
61+
result = evaluate("x + y", context, mode=EvaluationMode.PYTHON)
62+
assert result == 3.5
63+
64+
# Strict should fail
65+
with pytest.raises(TypeError, match="Unsupported addition operation"):
66+
evaluate("x + y", context, mode=EvaluationMode.STRICT)
67+
68+
69+
def test_context_object_with_mixed_types():
70+
"""Test evaluation modes with Context object containing mixed types."""
71+
context = Context(variables={"a": 5, "b": 3.0})
72+
73+
# Python mode should work
74+
result = evaluate("a + b", context, mode=EvaluationMode.PYTHON)
75+
assert result == 8.0
76+
77+
# Strict should fail
78+
with pytest.raises(TypeError, match="Unsupported addition operation"):
79+
evaluate("a + b", context, mode=EvaluationMode.STRICT)
80+
81+
82+
def test_invalid_mode_string():
83+
"""Test that invalid mode strings raise appropriate errors."""
84+
with pytest.raises(TypeError, match="Invalid EvaluationMode"):
85+
evaluate("1 + 2", mode="InvalidMode")
86+
87+
88+
def test_context_type_promotion_in_python_mode():
89+
"""Test that python mode promotes context integers when floats are present."""
90+
context = {"int_val": 10, "float_val": 2.5}
91+
92+
# This should work in python mode due to type promotion
93+
result = evaluate("int_val * float_val", context, mode=EvaluationMode.PYTHON)
94+
assert result == 25.0
95+
96+
97+
def test_expression_preprocessing_in_python_mode():
98+
"""Test that python mode preprocesses integer literals when mixed with floats."""
99+
# This expression has mixed literals, should work in python mode
100+
result = evaluate("10 + 2.5", mode=EvaluationMode.PYTHON)
101+
assert result == 12.5
102+
103+
# Should fail in strict mode
104+
with pytest.raises(TypeError):
105+
evaluate("10 + 2.5", mode=EvaluationMode.STRICT)
106+
107+
108+
def test_non_arithmetic_expressions_work_in_both_modes():
109+
"""Test that non-arithmetic expressions work the same in both modes."""
110+
# String operations
111+
assert evaluate('"hello" + " world"', mode=EvaluationMode.PYTHON) == "hello world"
112+
assert evaluate('"hello" + " world"', mode=EvaluationMode.STRICT) == "hello world"
113+
114+
# Boolean operations
115+
assert evaluate("true && false", mode=EvaluationMode.PYTHON) is False
116+
assert evaluate("true && false", mode=EvaluationMode.STRICT) is False
117+
118+
# List operations
119+
assert evaluate("[1, 2, 3].size()", mode=EvaluationMode.PYTHON) == 3
120+
assert evaluate("[1, 2, 3].size()", mode=EvaluationMode.STRICT) == 3
121+
122+
123+
def test_mode_with_custom_functions():
124+
"""Test that evaluation modes work with custom functions."""
125+
126+
def add_numbers(a, b):
127+
return a + b
128+
129+
context = Context(variables={"x": 1, "y": 2.5}, functions={"add": add_numbers})
130+
131+
# The Python function itself will handle mixed arithmetic
132+
result = evaluate("add(x, y)", context, mode=EvaluationMode.STRICT)
133+
assert result == 3.5 # Python function can handle mixed types
134+
135+
# But CEL arithmetic still fails in strict mode
136+
with pytest.raises(TypeError):
137+
evaluate("x + y", context, mode=EvaluationMode.STRICT)
138+
139+
140+
def test_mode_parameter_positions():
141+
"""Test that mode parameter works in different positions."""
142+
context = {"a": 1, "b": 2}
143+
144+
# Mode as third parameter
145+
result1 = evaluate("a + b", context, EvaluationMode.PYTHON)
146+
assert result1 == 3
147+
148+
# Mode as keyword argument
149+
result2 = evaluate("a + b", context, mode=EvaluationMode.PYTHON)
150+
assert result2 == 3
151+
152+
# Mode without context
153+
result3 = evaluate("1 + 2", mode=EvaluationMode.PYTHON)
154+
assert result3 == 3

0 commit comments

Comments
 (0)