From 52e95d8435b6ec0f422399ced425f79a50a466a5 Mon Sep 17 00:00:00 2001 From: Dmitri Plotnikov Date: Fri, 14 Aug 2026 12:59:56 -0700 Subject: [PATCH] Add support for environment options in Python CEL. This change allows passing an optional dictionary of options to EnvConfig and NewEnv. It exposes an options property on EnvConfig and uses these options to configure the underlying CEL compiler builder, specifically supporting the "enable_pratt_parser" option. PiperOrigin-RevId: 964845782 --- MODULE.bazel | 4 +- cel_expr_python/BUILD | 2 + cel_expr_python/cel.pyi | 16 ++++++- cel_expr_python/cel_env_test.py | 14 ++++++ cel_expr_python/cel_test.py | 61 +++++++++++++++++------- cel_expr_python/py_cel_env.cc | 18 ++++++-- cel_expr_python/py_cel_env.h | 4 +- cel_expr_python/py_cel_env_internal.cc | 13 ++++-- cel_expr_python/py_cel_env_internal.h | 10 ++-- cel_expr_python/py_cel_module.cc | 2 + cel_expr_python/py_cel_options.cc | 64 ++++++++++++++++++++++++++ cel_expr_python/py_cel_options.h | 32 +++++++++++++ 12 files changed, 208 insertions(+), 32 deletions(-) create mode 100644 cel_expr_python/py_cel_options.cc create mode 100644 cel_expr_python/py_cel_options.h diff --git a/MODULE.bazel b/MODULE.bazel index 089d1b1..b35d47a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -12,10 +12,10 @@ bazel_dep(name = "abseil-py", version = "2.4.0", repo_name = "com_google_absl_py bazel_dep(name = "bazel_skylib", version = "1.9.0") # https://registry.bazel.build/modules/cel-cpp -bazel_dep(name = "cel-cpp", version = "0.15.0", repo_name = "com_google_cel_cpp") +bazel_dep(name = "cel-cpp", version = "0.16.1", repo_name = "com_google_cel_cpp") git_override( module_name = "cel-cpp", - commit = "76ae0b3c1768d93a10270f904101de338867bdb1", + commit = "e6485ab94a6a4f1a9aead2b53e64ce2389917c1f", remote = "https://github.com/cel-expr/cel-cpp", ) diff --git a/cel_expr_python/BUILD b/cel_expr_python/BUILD index 190c18a..acb75ab 100644 --- a/cel_expr_python/BUILD +++ b/cel_expr_python/BUILD @@ -20,6 +20,7 @@ pybind_library( "py_cel_expression.cc", "py_cel_function.cc", "py_cel_function_decl.cc", + "py_cel_options.cc", "py_cel_overload.cc", "py_cel_python_extension.cc", "py_cel_type.cc", @@ -36,6 +37,7 @@ pybind_library( "py_cel_expression.h", "py_cel_function.h", "py_cel_function_decl.h", + "py_cel_options.h", "py_cel_overload.h", "py_cel_python_extension.h", "py_cel_type.h", diff --git a/cel_expr_python/cel.pyi b/cel_expr_python/cel.pyi index f5cc7c8..ab151bb 100644 --- a/cel_expr_python/cel.pyi +++ b/cel_expr_python/cel.pyi @@ -12,6 +12,10 @@ class CelExtension(CelExtensionBase): class CelExtensionBase: def __init__(self, name: str) -> None: ... +class Options: + enable_pratt_parser: bool + def __init__(self, enable_pratt_parser: bool = ...) -> None: ... + class EnvConfig: @property def context_type(self) -> str: ... @@ -26,6 +30,7 @@ class Env: def compile(self, expression: str, disable_check: bool = ...) -> Expression: ... def deserialize(self, serialized: str | bytes) -> Expression: ... def config(self) -> EnvConfig: ... + def options(self) -> Options: ... class Expression: def eval(self, activation: Activation | None = ..., data: Mapping[str, Any] | None = ..., functions=..., arena: _InternalArena = ...) -> Value: ... @@ -86,6 +91,15 @@ class _InternalArena: def Arena() -> _InternalArena: ... -def NewEnv(descriptor_pool: proto_descriptor_pool.DescriptorPool | Any | None = ..., config: EnvConfig | None = ..., variables: Mapping[str, Type] | None = ..., extensions: Sequence[CelExtensionBase] | None = ..., container: str | ExpressionContainer | None = ..., functions: Sequence[FunctionDecl] | None = ..., function_impls: Mapping[str, Callable[..., Any]] | None = ...) -> Env: ... +def NewEnv( + descriptor_pool: proto_descriptor_pool.DescriptorPool | Any | None = ..., + config: EnvConfig | None = ..., + variables: Mapping[str, Type] | None = ..., + extensions: Sequence[CelExtensionBase] | None = ..., + container: str | ExpressionContainer | None = ..., + functions: Sequence[FunctionDecl] | None = ..., + function_impls: Mapping[str, Callable[..., Any]] | None = ..., + options: Options | None = ..., +) -> Env: ... def NewEnvConfigFromYaml(yaml: str) -> EnvConfig: ... diff --git a/cel_expr_python/cel_env_test.py b/cel_expr_python/cel_env_test.py index e0cb597..eabe446 100644 --- a/cel_expr_python/cel_env_test.py +++ b/cel_expr_python/cel_env_test.py @@ -724,6 +724,20 @@ def test_config_functions_deprecated_syntax(self): res = env.compile("'bad'.is_ok()").eval() self.assertFalse(res.value()) + def test_env_options(self): + options = cel.Options(enable_pratt_parser=True) + self.assertTrue(options.enable_pratt_parser) + self.assertEqual(repr(options), "Options(enable_pratt_parser=True)") + options.enable_pratt_parser = False + self.assertFalse(options.enable_pratt_parser) + self.assertEqual(repr(options), "Options(enable_pratt_parser=False)") + + env = cel.NewEnv(options=cel.Options(enable_pratt_parser=True)) + self.assertTrue(env.options().enable_pratt_parser) + + default_env = cel.NewEnv() + self.assertFalse(default_env.options().enable_pratt_parser) + class TestCelExtension(cel.CelExtension): """An example CEL extension for testing.""" diff --git a/cel_expr_python/cel_test.py b/cel_expr_python/cel_test.py index e8f6e47..2add977 100644 --- a/cel_expr_python/cel_test.py +++ b/cel_expr_python/cel_test.py @@ -28,7 +28,9 @@ from cel.expr.conformance.proto2 import test_all_types_pb2 as test_all_types_pb -class CelTest(absltest.TestCase): +@absltest.skipThisClass("Base class") +class _CelTestBase(absltest.TestCase): + options: cel.Options = cel.Options() def setUp(self): super().setUp() @@ -50,7 +52,8 @@ def setUp(self): "var_string_map": cel.Type.Map(cel.Type.STRING, cel.Type.BOOL), "var_dyn_map": cel.Type.MAP, "var_dyn": cel.Type.DYN, - } + }, + options=self.options, ) self.object_counts_before_test = self._grab_object_counts() @@ -615,10 +618,13 @@ def testDynType(self): self.assertIn("out of range for 'var_dyn'", res.value()) def testDynType_nonCelType(self): - res = self._eval("var_dyn", {"var_dyn": self}) + class NonCelValue: + pass + + res = self._eval("var_dyn", {"var_dyn": NonCelValue()}) self.assertEqual(res.type(), cel.Type.ERROR) self.assertIn( - "Non-CEL value type for 'var_dyn': CelTest", + "Non-CEL value type for 'var_dyn': NonCelValue", res.value(), ) @@ -768,18 +774,26 @@ def testCompilationErrorHandling(self): # Check parser error. with self.assertRaises(Exception) as e: self.env.compile("'Hello,' # 'World!'", disable_check=True) - self.assertIn( - "1:10: Syntax error: token recognition error at: '#'\n " - "| 'Hello,' # 'World!'\n " - "| .........^", - str(e.exception), - ) - self.assertIn( - "1:12: Syntax error: extraneous input ''World!'' expecting \n " - "| 'Hello,' # 'World!'\n " - "| ...........^", - str(e.exception), - ) + if self.options.enable_pratt_parser: + self.assertIn( + "1:10: unexpected character\n" + " | 'Hello,' # 'World!'\n" + " | .........^", + str(e.exception), + ) + else: + self.assertIn( + "1:10: Syntax error: token recognition error at: '#'\n " + "| 'Hello,' # 'World!'\n " + "| .........^", + str(e.exception), + ) + self.assertIn( + "1:12: Syntax error: extraneous input ''World!'' expecting \n " + "| 'Hello,' # 'World!'\n " + "| ...........^", + str(e.exception), + ) # Check type-checker error. with self.assertRaises(Exception) as e: @@ -793,7 +807,11 @@ def testCompilationErrorHandling(self): ) def testErrorHandling(self): - bad_env = cel.NewEnv(_BadDescriptorPool(), variables={}) + bad_env = cel.NewEnv( + _BadDescriptorPool(), + variables={}, + options=self.options, + ) with self.assertRaises(Exception) as e: bad_env.compile("cel.expr.conformance.proto2.TestSomeTypes{}") self.assertRegex( @@ -929,5 +947,14 @@ def testErrorOnProtoCreation(self): ) +class CelTest(_CelTestBase): + # Default options. + pass + + +class CelPrattParserTest(_CelTestBase): + options = cel.Options(enable_pratt_parser=True) + + if __name__ == "__main__": absltest.main() diff --git a/cel_expr_python/py_cel_env.cc b/cel_expr_python/py_cel_env.cc index fb0fd69..30758cf 100644 --- a/cel_expr_python/py_cel_env.cc +++ b/cel_expr_python/py_cel_env.cc @@ -32,6 +32,7 @@ #include "cel_expr_python/py_cel_env_internal.h" #include "cel_expr_python/py_cel_expression.h" #include "cel_expr_python/py_cel_function_decl.h" +#include "cel_expr_python/py_cel_options.h" #include "cel_expr_python/py_cel_type.h" #include "cel_expr_python/py_error_status.h" #include @@ -81,7 +82,8 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) { std::optional>>& functions, std::optional>& - function_impls) { + function_impls, + std::optional& options) { PyObject* pool_ptr; if (descriptor_pool.is_none()) { // Replicates python's `descriptor_pool.Default()` @@ -119,7 +121,10 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) { } } - return PyCelEnv(config.value_or(PyCelEnvConfig()), pool_ptr, + PyCelOptions env_options = options.value_or(PyCelOptions()); + + return PyCelEnv(config.value_or(PyCelEnvConfig()), env_options, + pool_ptr, std::move(variables).value_or( std::unordered_map{}), ext_ptrs, std::move(expr_container), @@ -131,10 +136,12 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) { py::arg("descriptor_pool") = py::none(), py::arg("config") = py::none(), py::arg("variables") = py::none(), py::arg("extensions") = py::none(), py::arg("container") = py::none(), py::arg("functions") = py::none(), - py::arg("function_impls") = py::none()); + py::arg("function_impls") = py::none(), py::arg("options") = py::none()); cel_class .def("config", [](PyCelEnv& self) { return self.GetEnv()->GetEnvConfig(); }) + .def("options", + [](PyCelEnv& self) { return self.GetEnv()->GetOptions(); }) .def("compile", &PyCelEnv::Compile, py::arg("expression"), py::arg("disable_check") = false) .def("deserialize", &PyCelEnv::Deserialize, py::arg("serialized")) @@ -165,14 +172,15 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) { } PyCelEnv::PyCelEnv( - const PyCelEnvConfig& config, PyObject* descriptor_pool, + const PyCelEnvConfig& config, const PyCelOptions& options, + PyObject* descriptor_pool, const std::unordered_map& variable_types, const std::vector& extensions, cel::ExpressionContainer container, const std::vector>& functions, const std::unordered_map& function_impls) { env_ = ThrowIfError(PyCelEnvInternal::NewCelEnvInternal( - config, descriptor_pool, std::move(variable_types), extensions, + config, options, descriptor_pool, std::move(variable_types), extensions, std::move(container), std::move(functions), std::move(function_impls))); ABSL_CHECK(PyGILState_Check()); } diff --git a/cel_expr_python/py_cel_env.h b/cel_expr_python/py_cel_env.h index 2eac651..b6fc4a0 100644 --- a/cel_expr_python/py_cel_env.h +++ b/cel_expr_python/py_cel_env.h @@ -30,6 +30,7 @@ #include "cel_expr_python/py_cel_expression.h" #include "cel_expr_python/py_cel_function.h" #include "cel_expr_python/py_cel_function_decl.h" +#include "cel_expr_python/py_cel_options.h" #include "cel_expr_python/py_cel_type.h" #include @@ -68,7 +69,8 @@ class PyCelEnv { private: // Private constructor. Use `py_cel.NewEnv()` in python to obtain an instance. - PyCelEnv(const PyCelEnvConfig& config, PyObject* descriptor_pool, + PyCelEnv(const PyCelEnvConfig& config, const PyCelOptions& options, + PyObject* descriptor_pool, const std::unordered_map& variable_types, const std::vector& extensions, cel::ExpressionContainer container, diff --git a/cel_expr_python/py_cel_env_internal.cc b/cel_expr_python/py_cel_env_internal.cc index af0391b..6b77107 100644 --- a/cel_expr_python/py_cel_env_internal.cc +++ b/cel_expr_python/py_cel_env_internal.cc @@ -45,6 +45,7 @@ #include "cel_expr_python/py_cel_env_config.h" #include "cel_expr_python/py_cel_function.h" #include "cel_expr_python/py_cel_function_decl.h" +#include "cel_expr_python/py_cel_options.h" #include "cel_expr_python/py_cel_overload.h" #include "cel_expr_python/py_cel_python_extension.h" #include "cel_expr_python/py_cel_type.h" @@ -66,10 +67,12 @@ static const cel::FunctionDescriptorOptions kFunctionDescriptorOptions = { } // namespace PyCelEnvInternal::PyCelEnvInternal( - const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool, + const PyCelEnvConfig& env_config, const PyCelOptions& options, + PyObject* py_descriptor_pool, std::vector extension_handles, absl::flat_hash_map& function_impls) : env_config_(env_config), + options_(options), py_descriptor_database_(py_descriptor_pool), descriptor_pool_( std::make_shared(&py_descriptor_database_)), @@ -105,7 +108,8 @@ PyCelEnvInternal::PyCelEnvInternal( absl::StatusOr> PyCelEnvInternal::NewCelEnvInternal( - const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool, + const PyCelEnvConfig& env_config, const PyCelOptions& options, + PyObject* py_descriptor_pool, const std::unordered_map& variable_types, const std::vector& extensions, cel::ExpressionContainer container, @@ -219,7 +223,7 @@ PyCelEnvInternal::NewCelEnvInternal( } } return std::shared_ptr( - new PyCelEnvInternal(PyCelEnvConfig(config), py_descriptor_pool, + new PyCelEnvInternal(PyCelEnvConfig(config), options, py_descriptor_pool, std::move(extension_handles), impls)); } @@ -237,6 +241,9 @@ absl::StatusOr PyCelEnvInternal::GetCompiler( std::unique_ptr compiler_builder, env->cel_env_.NewCompilerBuilder()); + compiler_builder->GetParserBuilder().GetOptions().enable_pratt_parser = + env->options_.enable_pratt_parser; + cel::TypeCheckerBuilder& checker_builder = compiler_builder->GetCheckerBuilder(); diff --git a/cel_expr_python/py_cel_env_internal.h b/cel_expr_python/py_cel_env_internal.h index ff82e4f..03e3c13 100644 --- a/cel_expr_python/py_cel_env_internal.h +++ b/cel_expr_python/py_cel_env_internal.h @@ -36,6 +36,7 @@ #include "cel_expr_python/py_cel_env_config.h" #include "cel_expr_python/py_cel_function.h" #include "cel_expr_python/py_cel_function_decl.h" +#include "cel_expr_python/py_cel_options.h" #include "cel_expr_python/py_cel_type.h" #include "cel_expr_python/py_descriptor_database.h" #include "cel_expr_python/py_message_factory.h" @@ -73,7 +74,8 @@ class PyCelEnvInternal { public: ~PyCelEnvInternal() = default; static absl::StatusOr> NewCelEnvInternal( - const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool, + const PyCelEnvConfig& env_config, const PyCelOptions& options, + PyObject* py_descriptor_pool, const std::unordered_map& variable_types, const std::vector& extensions, cel::ExpressionContainer container, @@ -81,6 +83,7 @@ class PyCelEnvInternal { const std::unordered_map& function_impls); const PyCelEnvConfig& GetEnvConfig() const { return env_config_; } + const PyCelOptions& GetOptions() const { return options_; } static absl::StatusOr GetCompiler( const std::shared_ptr& env); @@ -113,8 +116,8 @@ class PyCelEnvInternal { private: // Use NewCelEnvInternal() to create an instance. PyCelEnvInternal( - const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool, - std::vector extensions, + const PyCelEnvConfig& env_config, const PyCelOptions& options, + PyObject* py_descriptor_pool, std::vector extensions, absl::flat_hash_map& function_impls); absl::Status ConfigureStandardExtension( @@ -128,6 +131,7 @@ class PyCelEnvInternal { cel::Env cel_env_; cel::EnvRuntime cel_env_runtime_; PyCelEnvConfig env_config_; + PyCelOptions options_; PyDescriptorDatabase py_descriptor_database_; std::shared_ptr descriptor_pool_; google::protobuf::DynamicMessageFactory message_factory_; diff --git a/cel_expr_python/py_cel_module.cc b/cel_expr_python/py_cel_module.cc index d6766fe..aca47fd 100644 --- a/cel_expr_python/py_cel_module.cc +++ b/cel_expr_python/py_cel_module.cc @@ -19,6 +19,7 @@ #include "cel_expr_python/py_cel_expression.h" #include "cel_expr_python/py_cel_function.h" #include "cel_expr_python/py_cel_function_decl.h" +#include "cel_expr_python/py_cel_options.h" #include "cel_expr_python/py_cel_overload.h" #include "cel_expr_python/py_cel_python_extension.h" #include "cel_expr_python/py_cel_type.h" @@ -39,6 +40,7 @@ PYBIND11_MODULE(cel, m) { PyCelFunctionDecl::DefinePythonBindings(m); PyCelPythonExtension::DefinePythonBindings(m); PyCelFunction::DefinePythonBindings(m); + PyCelOptions::DefinePythonBindings(m); PyCelEnvConfig::DefinePythonBindings(m); PyCelEnv::DefinePythonBindings(m); } diff --git a/cel_expr_python/py_cel_options.cc b/cel_expr_python/py_cel_options.cc new file mode 100644 index 0000000..a0fd272 --- /dev/null +++ b/cel_expr_python/py_cel_options.cc @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cel_expr_python/py_cel_options.h" + +#include +#include +#include +#include + +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include + +namespace cel_python { + +namespace py = ::pybind11; + +namespace { + +std::string GenericRepr(py::handle self) { + std::vector parts; + py::handle cls = self.get_type(); + std::string class_name = cls.attr("__name__").cast(); + py::dict dict = cls.attr("__dict__"); + for (const auto& item : dict) { + std::string name = item.first.cast(); + if (name.starts_with('_')) { + continue; + } + py::object val = self.attr(item.first); + parts.push_back( + absl::StrFormat("%s=%s", name, py::repr(val).cast())); + } + std::sort(parts.begin(), parts.end()); + return absl::StrFormat("%s(%s)", class_name, absl::StrJoin(parts, ", ")); +} + +} // namespace + +void PyCelOptions::DefinePythonBindings(pybind11::module& m) { + py::class_>(m, "Options") + .def(py::init([](bool enable_pratt_parser) { + return PyCelOptions{.enable_pratt_parser = enable_pratt_parser}; + }), + py::arg("enable_pratt_parser") = false) + .def_readwrite("enable_pratt_parser", &PyCelOptions::enable_pratt_parser) + .def("__repr__", &GenericRepr); +} + +} // namespace cel_python diff --git a/cel_expr_python/py_cel_options.h b/cel_expr_python/py_cel_options.h new file mode 100644 index 0000000..5aac31c --- /dev/null +++ b/cel_expr_python/py_cel_options.h @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef THIRD_PARTY_CEL_PYTHON_PY_CEL_OPTIONS_H_ +#define THIRD_PARTY_CEL_PYTHON_PY_CEL_OPTIONS_H_ + +#include + +namespace cel_python { + +struct PyCelOptions { + static void DefinePythonBindings(pybind11::module& m); + + bool enable_pratt_parser = false; +}; + +} // namespace cel_python + +#endif // THIRD_PARTY_CEL_PYTHON_PY_CEL_OPTIONS_H_