@@ -16,6 +16,25 @@ use std::error::Error;
1616use std:: fmt;
1717use std:: sync:: Arc ;
1818
19+ #[ derive( Clone , Debug , PartialEq ) ]
20+ pub enum EvaluationMode {
21+ PythonCompatible ,
22+ Strict ,
23+ }
24+
25+ impl < ' py > FromPyObject < ' py > for EvaluationMode {
26+ fn extract_bound ( ob : & Bound < ' py , PyAny > ) -> PyResult < Self > {
27+ let s: String = ob. extract ( ) ?;
28+ match s. as_str ( ) {
29+ "python" => Ok ( EvaluationMode :: PythonCompatible ) ,
30+ "strict" => Ok ( EvaluationMode :: Strict ) ,
31+ _ => Err ( PyTypeError :: new_err ( format ! (
32+ "Invalid EvaluationMode: expected 'python' or 'strict', got '{s}'"
33+ ) ) ) ,
34+ }
35+ }
36+ }
37+
1938#[ derive( Debug ) ]
2039struct RustyCelType ( Value ) ;
2140
@@ -424,6 +443,12 @@ impl TryIntoValue for RustyPyType<'_> {
424443/// - A `cel.Context` object (recommended for reusable contexts)
425444/// - A standard Python dictionary containing variables and functions
426445/// - None (for expressions that don't require external variables)
446+ /// mode (Union[str, cel.EvaluationMode]): The evaluation mode to use.
447+ /// Defaults to "python". Can be:
448+ /// - "python" or EvaluationMode.PYTHON: Enables Python-friendly type
449+ /// promotions (e.g., int -> float) for better mixed arithmetic compatibility
450+ /// - "strict" or EvaluationMode.STRICT: Enforces strict CEL type rules
451+ /// with no automatic coercion to match WebAssembly behavior
427452///
428453/// Returns:
429454/// Union[bool, int, float, str, list, dict, datetime.datetime, bytes, None]:
@@ -508,7 +533,7 @@ impl TryIntoValue for RustyPyType<'_> {
508533///
509534/// Using Context object for reusable evaluations:
510535///
511- /// >>> from cel import Context
536+ /// >>> from cel import Context, EvaluationMode
512537/// >>> context = Context(
513538/// ... variables={"base_url": "https://api.example.com"},
514539/// ... functions={"len": len}
@@ -518,19 +543,32 @@ impl TryIntoValue for RustyPyType<'_> {
518543/// >>> evaluate("len('hello world')", context)
519544/// 11
520545///
546+ /// Using different evaluation modes:
547+ ///
548+ /// >>> # Python mode (default) - allows mixed arithmetic
549+ /// >>> evaluate("1 + 2.5")
550+ /// 3.5
551+ /// >>> evaluate("1 + 2.5", mode=EvaluationMode.PYTHON)
552+ /// 3.5
553+ /// >>> # Strict mode - enforces type matching
554+ /// >>> try:
555+ /// ... evaluate("1 + 2.5", mode=EvaluationMode.STRICT)
556+ /// ... except TypeError as e:
557+ /// ... print("Strict mode error:", e)
558+ /// Strict mode error: Unsupported addition operation: Int + Double...
559+ ///
521560/// See Also:
522561/// - cel.Context: For managing reusable evaluation contexts
523562/// - CEL Language Guide: For comprehensive language documentation
524563/// - Python API Reference: For detailed API documentation
525- #[ pyfunction( signature = ( src, evaluation_context=None ) ) ]
526- fn evaluate ( src : String , evaluation_context : Option < & Bound < ' _ , PyAny > > ) -> PyResult < RustyCelType > {
527- // Preprocess expression for better mixed int/float arithmetic compatibility
528- // First check if expression itself has mixed literals
529- let mut processed_src = if expression_has_mixed_numeric_literals ( & src) {
530- preprocess_expression_for_mixed_arithmetic_always ( & src)
531- } else {
532- src. clone ( )
533- } ;
564+ #[ pyfunction( signature = ( src, evaluation_context=None , mode=None ) ) ]
565+ fn evaluate (
566+ src : String ,
567+ evaluation_context : Option < & Bound < ' _ , PyAny > > ,
568+ mode : Option < EvaluationMode > ,
569+ ) -> PyResult < RustyCelType > {
570+ // Use PythonCompatible as default if mode is not provided
571+ let mode = mode. unwrap_or ( EvaluationMode :: PythonCompatible ) ;
534572
535573 let mut environment = CelContext :: default ( ) ;
536574 let mut ctx = context:: Context :: new ( None , None ) ?;
@@ -539,7 +577,7 @@ fn evaluate(src: String, evaluation_context: Option<&Bound<'_, PyAny>>) -> PyRes
539577 // Custom Rust functions can also be added to the environment...
540578 //environment.add_function("add", |a: i64, b: i64| a + b);
541579
542- // Process the evaluation context if provided first to determine if we need preprocessing
580+ // Process the evaluation context if provided
543581 if let Some ( evaluation_context) = evaluation_context {
544582 // Attempt to extract directly as a Context object
545583 if let Ok ( py_context_ref) = evaluation_context. extract :: < PyRef < context:: Context > > ( ) {
@@ -555,21 +593,35 @@ fn evaluate(src: String, evaluation_context: Option<&Bound<'_, PyAny>>) -> PyRes
555593 ) ) ;
556594 } ;
557595
558- // Smart numeric coercion for mixed int/float arithmetic compatibility
559596 variables_for_env = ctx. variables . clone ( ) ;
597+ }
560598
561- // Check if we should promote integers to floats for better compatibility
562- let should_promote = should_promote_integers_to_floats ( & variables_for_env)
563- || expression_has_mixed_numeric_literals ( & src) ;
564-
565- if should_promote {
566- promote_integers_in_context ( & mut variables_for_env) ;
567-
568- // Always preprocess the expression when we're promoting types
569- // This handles cases where context has floats but expression has integer literals
570- processed_src = preprocess_expression_for_mixed_arithmetic_always ( & src) ;
599+ // Apply type promotion logic based on evaluation mode (consolidated)
600+ let processed_src = match mode {
601+ EvaluationMode :: PythonCompatible => {
602+ // Check if we should promote integers to floats for better compatibility
603+ let should_promote = should_promote_integers_to_floats ( & variables_for_env)
604+ || expression_has_mixed_numeric_literals ( & src) ;
605+
606+ if should_promote {
607+ // Promote integers in context if we have one
608+ if !variables_for_env. is_empty ( ) {
609+ promote_integers_in_context ( & mut variables_for_env) ;
610+ }
611+ // Always preprocess the expression when promoting types
612+ preprocess_expression_for_mixed_arithmetic_always ( & src)
613+ } else if expression_has_mixed_numeric_literals ( & src) {
614+ // Preprocess expression even without context if it has mixed literals
615+ preprocess_expression_for_mixed_arithmetic_always ( & src)
616+ } else {
617+ src. clone ( )
618+ }
571619 }
572- }
620+ EvaluationMode :: Strict => {
621+ // Do nothing - preserve strict type behavior with no promotions or rewriting
622+ src. clone ( )
623+ }
624+ } ;
573625
574626 // Use panic::catch_unwind to handle parser panics gracefully
575627 let program = panic:: catch_unwind ( || Program :: compile ( & processed_src) )
0 commit comments