Skip to content

Commit 604cc57

Browse files
dmitriplotnikovcopybara-github
authored andcommitted
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: 966107832
1 parent 2ae52f4 commit 604cc57

12 files changed

Lines changed: 213 additions & 33 deletions

MODULE.bazel

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ module(
33
)
44

55
# https://registry.bazel.build/modules/abseil-cpp
6-
bazel_dep(name = "abseil-cpp", version = "20260107.0", repo_name = "com_google_absl")
6+
bazel_dep(name = "abseil-cpp", version = "20260526.0", repo_name = "com_google_absl")
7+
single_version_override(
8+
module_name = "abseil-cpp",
9+
version = "20260526.0",
10+
)
711

812
# https://registry.bazel.build/modules/abseil-py
913
bazel_dep(name = "abseil-py", version = "2.4.0", repo_name = "com_google_absl_py")
@@ -12,10 +16,10 @@ bazel_dep(name = "abseil-py", version = "2.4.0", repo_name = "com_google_absl_py
1216
bazel_dep(name = "bazel_skylib", version = "1.9.0")
1317

1418
# https://registry.bazel.build/modules/cel-cpp
15-
bazel_dep(name = "cel-cpp", version = "0.15.0", repo_name = "com_google_cel_cpp")
19+
bazel_dep(name = "cel-cpp", version = "0.16.1", repo_name = "com_google_cel_cpp")
1620
git_override(
1721
module_name = "cel-cpp",
18-
commit = "76ae0b3c1768d93a10270f904101de338867bdb1",
22+
commit = "1dcff093b3ca3d575f982e2ca154f9c751e06055",
1923
remote = "https://github.com/cel-expr/cel-cpp",
2024
)
2125

cel_expr_python/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pybind_library(
2020
"py_cel_expression.cc",
2121
"py_cel_function.cc",
2222
"py_cel_function_decl.cc",
23+
"py_cel_options.cc",
2324
"py_cel_overload.cc",
2425
"py_cel_python_extension.cc",
2526
"py_cel_type.cc",
@@ -36,6 +37,7 @@ pybind_library(
3637
"py_cel_expression.h",
3738
"py_cel_function.h",
3839
"py_cel_function_decl.h",
40+
"py_cel_options.h",
3941
"py_cel_overload.h",
4042
"py_cel_python_extension.h",
4143
"py_cel_type.h",

cel_expr_python/cel.pyi

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ class CelExtension(CelExtensionBase):
1212
class CelExtensionBase:
1313
def __init__(self, name: str) -> None: ...
1414

15+
class Options:
16+
enable_pratt_parser: bool
17+
def __init__(self, enable_pratt_parser: bool = ...) -> None: ...
18+
1519
class EnvConfig:
1620
@property
1721
def context_type(self) -> str: ...
@@ -26,6 +30,7 @@ class Env:
2630
def compile(self, expression: str, disable_check: bool = ...) -> Expression: ...
2731
def deserialize(self, serialized: str | bytes) -> Expression: ...
2832
def config(self) -> EnvConfig: ...
33+
def options(self) -> Options: ...
2934

3035
class Expression:
3136
def eval(self, activation: Activation | None = ..., data: Mapping[str, Any] | None = ..., functions=..., arena: _InternalArena = ...) -> Value: ...
@@ -86,6 +91,15 @@ class _InternalArena:
8691

8792
def Arena() -> _InternalArena: ...
8893

89-
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: ...
94+
def NewEnv(
95+
descriptor_pool: proto_descriptor_pool.DescriptorPool | Any | None = ...,
96+
config: EnvConfig | None = ...,
97+
variables: Mapping[str, Type] | None = ...,
98+
extensions: Sequence[CelExtensionBase] | None = ...,
99+
container: str | ExpressionContainer | None = ...,
100+
functions: Sequence[FunctionDecl] | None = ...,
101+
function_impls: Mapping[str, Callable[..., Any]] | None = ...,
102+
options: Options | None = ...,
103+
) -> Env: ...
90104

91105
def NewEnvConfigFromYaml(yaml: str) -> EnvConfig: ...

cel_expr_python/cel_env_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,20 @@ def test_config_functions_deprecated_syntax(self):
724724
res = env.compile("'bad'.is_ok()").eval()
725725
self.assertFalse(res.value())
726726

727+
def test_env_options(self):
728+
options = cel.Options(enable_pratt_parser=True)
729+
self.assertTrue(options.enable_pratt_parser)
730+
self.assertEqual(repr(options), "Options(enable_pratt_parser=True)")
731+
options.enable_pratt_parser = False
732+
self.assertFalse(options.enable_pratt_parser)
733+
self.assertEqual(repr(options), "Options(enable_pratt_parser=False)")
734+
735+
env = cel.NewEnv(options=cel.Options(enable_pratt_parser=True))
736+
self.assertTrue(env.options().enable_pratt_parser)
737+
738+
default_env = cel.NewEnv()
739+
self.assertFalse(default_env.options().enable_pratt_parser)
740+
727741

728742
class TestCelExtension(cel.CelExtension):
729743
"""An example CEL extension for testing."""

cel_expr_python/cel_test.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
from cel.expr.conformance.proto2 import test_all_types_pb2 as test_all_types_pb
2929

3030

31-
class CelTest(absltest.TestCase):
31+
@absltest.skipThisClass("Base class")
32+
class _CelTestBase(absltest.TestCase):
33+
options: cel.Options = cel.Options()
3234

3335
def setUp(self):
3436
super().setUp()
@@ -50,7 +52,8 @@ def setUp(self):
5052
"var_string_map": cel.Type.Map(cel.Type.STRING, cel.Type.BOOL),
5153
"var_dyn_map": cel.Type.MAP,
5254
"var_dyn": cel.Type.DYN,
53-
}
55+
},
56+
options=self.options,
5457
)
5558
self.object_counts_before_test = self._grab_object_counts()
5659

@@ -615,10 +618,13 @@ def testDynType(self):
615618
self.assertIn("out of range for 'var_dyn'", res.value())
616619

617620
def testDynType_nonCelType(self):
618-
res = self._eval("var_dyn", {"var_dyn": self})
621+
class NonCelValue:
622+
pass
623+
624+
res = self._eval("var_dyn", {"var_dyn": NonCelValue()})
619625
self.assertEqual(res.type(), cel.Type.ERROR)
620626
self.assertIn(
621-
"Non-CEL value type for 'var_dyn': CelTest",
627+
"Non-CEL value type for 'var_dyn': NonCelValue",
622628
res.value(),
623629
)
624630

@@ -768,18 +774,26 @@ def testCompilationErrorHandling(self):
768774
# Check parser error.
769775
with self.assertRaises(Exception) as e:
770776
self.env.compile("'Hello,' # 'World!'", disable_check=True)
771-
self.assertIn(
772-
"1:10: Syntax error: token recognition error at: '#'\n "
773-
"| 'Hello,' # 'World!'\n "
774-
"| .........^",
775-
str(e.exception),
776-
)
777-
self.assertIn(
778-
"1:12: Syntax error: extraneous input ''World!'' expecting <EOF>\n "
779-
"| 'Hello,' # 'World!'\n "
780-
"| ...........^",
781-
str(e.exception),
782-
)
777+
if self.options.enable_pratt_parser:
778+
self.assertIn(
779+
"1:10: unexpected character\n"
780+
" | 'Hello,' # 'World!'\n"
781+
" | .........^",
782+
str(e.exception),
783+
)
784+
else:
785+
self.assertIn(
786+
"1:10: Syntax error: token recognition error at: '#'\n "
787+
"| 'Hello,' # 'World!'\n "
788+
"| .........^",
789+
str(e.exception),
790+
)
791+
self.assertIn(
792+
"1:12: Syntax error: extraneous input ''World!'' expecting <EOF>\n "
793+
"| 'Hello,' # 'World!'\n "
794+
"| ...........^",
795+
str(e.exception),
796+
)
783797

784798
# Check type-checker error.
785799
with self.assertRaises(Exception) as e:
@@ -793,7 +807,11 @@ def testCompilationErrorHandling(self):
793807
)
794808

795809
def testErrorHandling(self):
796-
bad_env = cel.NewEnv(_BadDescriptorPool(), variables={})
810+
bad_env = cel.NewEnv(
811+
_BadDescriptorPool(),
812+
variables={},
813+
options=self.options,
814+
)
797815
with self.assertRaises(Exception) as e:
798816
bad_env.compile("cel.expr.conformance.proto2.TestSomeTypes{}")
799817
self.assertRegex(
@@ -929,5 +947,14 @@ def testErrorOnProtoCreation(self):
929947
)
930948

931949

950+
class CelTest(_CelTestBase):
951+
# Default options.
952+
pass
953+
954+
955+
class CelPrattParserTest(_CelTestBase):
956+
options = cel.Options(enable_pratt_parser=True)
957+
958+
932959
if __name__ == "__main__":
933960
absltest.main()

cel_expr_python/py_cel_env.cc

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "cel_expr_python/py_cel_env_internal.h"
3333
#include "cel_expr_python/py_cel_expression.h"
3434
#include "cel_expr_python/py_cel_function_decl.h"
35+
#include "cel_expr_python/py_cel_options.h"
3536
#include "cel_expr_python/py_cel_type.h"
3637
#include "cel_expr_python/py_error_status.h"
3738
#include <pybind11/pybind11.h>
@@ -81,7 +82,8 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) {
8182
std::optional<std::vector<std::shared_ptr<PyCelFunctionDecl>>>&
8283
functions,
8384
std::optional<std::unordered_map<std::string, py::object>>&
84-
function_impls) {
85+
function_impls,
86+
std::optional<PyCelOptions>& options) {
8587
PyObject* pool_ptr;
8688
if (descriptor_pool.is_none()) {
8789
// Replicates python's `descriptor_pool.Default()`
@@ -119,7 +121,10 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) {
119121
}
120122
}
121123

122-
return PyCelEnv(config.value_or(PyCelEnvConfig()), pool_ptr,
124+
PyCelOptions env_options = options.value_or(PyCelOptions());
125+
126+
return PyCelEnv(config.value_or(PyCelEnvConfig()), env_options,
127+
pool_ptr,
123128
std::move(variables).value_or(
124129
std::unordered_map<std::string, PyCelType>{}),
125130
ext_ptrs, std::move(expr_container),
@@ -131,10 +136,12 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) {
131136
py::arg("descriptor_pool") = py::none(), py::arg("config") = py::none(),
132137
py::arg("variables") = py::none(), py::arg("extensions") = py::none(),
133138
py::arg("container") = py::none(), py::arg("functions") = py::none(),
134-
py::arg("function_impls") = py::none());
139+
py::arg("function_impls") = py::none(), py::arg("options") = py::none());
135140
cel_class
136141
.def("config",
137142
[](PyCelEnv& self) { return self.GetEnv()->GetEnvConfig(); })
143+
.def("options",
144+
[](PyCelEnv& self) { return self.GetEnv()->GetOptions(); })
138145
.def("compile", &PyCelEnv::Compile, py::arg("expression"),
139146
py::arg("disable_check") = false)
140147
.def("deserialize", &PyCelEnv::Deserialize, py::arg("serialized"))
@@ -165,14 +172,15 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) {
165172
}
166173

167174
PyCelEnv::PyCelEnv(
168-
const PyCelEnvConfig& config, PyObject* descriptor_pool,
175+
const PyCelEnvConfig& config, const PyCelOptions& options,
176+
PyObject* descriptor_pool,
169177
const std::unordered_map<std::string, PyCelType>& variable_types,
170178
const std::vector<PyObject*>& extensions,
171179
cel::ExpressionContainer container,
172180
const std::vector<std::shared_ptr<PyCelFunctionDecl>>& functions,
173181
const std::unordered_map<std::string, py::object>& function_impls) {
174182
env_ = ThrowIfError(PyCelEnvInternal::NewCelEnvInternal(
175-
config, descriptor_pool, std::move(variable_types), extensions,
183+
config, options, descriptor_pool, std::move(variable_types), extensions,
176184
std::move(container), std::move(functions), std::move(function_impls)));
177185
ABSL_CHECK(PyGILState_Check());
178186
}

cel_expr_python/py_cel_env.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "cel_expr_python/py_cel_expression.h"
3131
#include "cel_expr_python/py_cel_function.h"
3232
#include "cel_expr_python/py_cel_function_decl.h"
33+
#include "cel_expr_python/py_cel_options.h"
3334
#include "cel_expr_python/py_cel_type.h"
3435
#include <pybind11/pybind11.h>
3536

@@ -68,7 +69,8 @@ class PyCelEnv {
6869

6970
private:
7071
// Private constructor. Use `py_cel.NewEnv()` in python to obtain an instance.
71-
PyCelEnv(const PyCelEnvConfig& config, PyObject* descriptor_pool,
72+
PyCelEnv(const PyCelEnvConfig& config, const PyCelOptions& options,
73+
PyObject* descriptor_pool,
7274
const std::unordered_map<std::string, PyCelType>& variable_types,
7375
const std::vector<PyObject*>& extensions,
7476
cel::ExpressionContainer container,

cel_expr_python/py_cel_env_internal.cc

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "cel_expr_python/py_cel_env_config.h"
4646
#include "cel_expr_python/py_cel_function.h"
4747
#include "cel_expr_python/py_cel_function_decl.h"
48+
#include "cel_expr_python/py_cel_options.h"
4849
#include "cel_expr_python/py_cel_overload.h"
4950
#include "cel_expr_python/py_cel_python_extension.h"
5051
#include "cel_expr_python/py_cel_type.h"
@@ -66,10 +67,12 @@ static const cel::FunctionDescriptorOptions kFunctionDescriptorOptions = {
6667
} // namespace
6768

6869
PyCelEnvInternal::PyCelEnvInternal(
69-
const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool,
70+
const PyCelEnvConfig& env_config, const PyCelOptions& options,
71+
PyObject* py_descriptor_pool,
7072
std::vector<CelExtensionHandle> extension_handles,
7173
absl::flat_hash_map<std::string, py::object>& function_impls)
7274
: env_config_(env_config),
75+
options_(options),
7376
py_descriptor_database_(py_descriptor_pool),
7477
descriptor_pool_(
7578
std::make_shared<google::protobuf::DescriptorPool>(&py_descriptor_database_)),
@@ -78,6 +81,9 @@ PyCelEnvInternal::PyCelEnvInternal(
7881
std::make_shared<PyMessageFactory>(py_descriptor_pool)),
7982
extensions_(std::move(extension_handles)),
8083
function_impls_(std::move(function_impls)) {
84+
cel_env_.GetCompilerOptions().parser_options.enable_pratt_parser =
85+
options.enable_pratt_parser;
86+
8187
cel_env_.SetDescriptorPool(descriptor_pool_);
8288
cel_env_.SetConfig(env_config_.GetConfig());
8389
cel::RegisterStandardExtensions(cel_env_);
@@ -105,7 +111,8 @@ PyCelEnvInternal::PyCelEnvInternal(
105111

106112
absl::StatusOr<std::shared_ptr<PyCelEnvInternal>>
107113
PyCelEnvInternal::NewCelEnvInternal(
108-
const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool,
114+
const PyCelEnvConfig& env_config, const PyCelOptions& options,
115+
PyObject* py_descriptor_pool,
109116
const std::unordered_map<std::string, PyCelType>& variable_types,
110117
const std::vector<PyObject*>& extensions,
111118
cel::ExpressionContainer container,
@@ -219,7 +226,7 @@ PyCelEnvInternal::NewCelEnvInternal(
219226
}
220227
}
221228
return std::shared_ptr<PyCelEnvInternal>(
222-
new PyCelEnvInternal(PyCelEnvConfig(config), py_descriptor_pool,
229+
new PyCelEnvInternal(PyCelEnvConfig(config), options, py_descriptor_pool,
223230
std::move(extension_handles), impls));
224231
}
225232

cel_expr_python/py_cel_env_internal.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "cel_expr_python/py_cel_env_config.h"
3737
#include "cel_expr_python/py_cel_function.h"
3838
#include "cel_expr_python/py_cel_function_decl.h"
39+
#include "cel_expr_python/py_cel_options.h"
3940
#include "cel_expr_python/py_cel_type.h"
4041
#include "cel_expr_python/py_descriptor_database.h"
4142
#include "cel_expr_python/py_message_factory.h"
@@ -73,14 +74,16 @@ class PyCelEnvInternal {
7374
public:
7475
~PyCelEnvInternal() = default;
7576
static absl::StatusOr<std::shared_ptr<PyCelEnvInternal>> NewCelEnvInternal(
76-
const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool,
77+
const PyCelEnvConfig& env_config, const PyCelOptions& options,
78+
PyObject* py_descriptor_pool,
7779
const std::unordered_map<std::string, PyCelType>& variable_types,
7880
const std::vector<PyObject*>& extensions,
7981
cel::ExpressionContainer container,
8082
const std::vector<std::shared_ptr<PyCelFunctionDecl>>& functions,
8183
const std::unordered_map<std::string, py::object>& function_impls);
8284

8385
const PyCelEnvConfig& GetEnvConfig() const { return env_config_; }
86+
const PyCelOptions& GetOptions() const { return options_; }
8487

8588
static absl::StatusOr<const cel::Compiler*> GetCompiler(
8689
const std::shared_ptr<PyCelEnvInternal>& env);
@@ -113,8 +116,8 @@ class PyCelEnvInternal {
113116
private:
114117
// Use NewCelEnvInternal() to create an instance.
115118
PyCelEnvInternal(
116-
const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool,
117-
std::vector<CelExtensionHandle> extensions,
119+
const PyCelEnvConfig& env_config, const PyCelOptions& options,
120+
PyObject* py_descriptor_pool, std::vector<CelExtensionHandle> extensions,
118121
absl::flat_hash_map<std::string, py::object>& function_impls);
119122

120123
absl::Status ConfigureStandardExtension(
@@ -128,6 +131,7 @@ class PyCelEnvInternal {
128131
cel::Env cel_env_;
129132
cel::EnvRuntime cel_env_runtime_;
130133
PyCelEnvConfig env_config_;
134+
PyCelOptions options_;
131135
PyDescriptorDatabase py_descriptor_database_;
132136
std::shared_ptr<google::protobuf::DescriptorPool> descriptor_pool_;
133137
google::protobuf::DynamicMessageFactory message_factory_;

cel_expr_python/py_cel_module.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "cel_expr_python/py_cel_expression.h"
2020
#include "cel_expr_python/py_cel_function.h"
2121
#include "cel_expr_python/py_cel_function_decl.h"
22+
#include "cel_expr_python/py_cel_options.h"
2223
#include "cel_expr_python/py_cel_overload.h"
2324
#include "cel_expr_python/py_cel_python_extension.h"
2425
#include "cel_expr_python/py_cel_type.h"
@@ -39,6 +40,7 @@ PYBIND11_MODULE(cel, m) {
3940
PyCelFunctionDecl::DefinePythonBindings(m);
4041
PyCelPythonExtension::DefinePythonBindings(m);
4142
PyCelFunction::DefinePythonBindings(m);
43+
PyCelOptions::DefinePythonBindings(m);
4244
PyCelEnvConfig::DefinePythonBindings(m);
4345
PyCelEnv::DefinePythonBindings(m);
4446
}

0 commit comments

Comments
 (0)