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_