Skip to content

Commit 52e95d8

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: 964845782
1 parent 2ae52f4 commit 52e95d8

12 files changed

Lines changed: 208 additions & 32 deletions

MODULE.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ bazel_dep(name = "abseil-py", version = "2.4.0", repo_name = "com_google_absl_py
1212
bazel_dep(name = "bazel_skylib", version = "1.9.0")
1313

1414
# https://registry.bazel.build/modules/cel-cpp
15-
bazel_dep(name = "cel-cpp", version = "0.15.0", repo_name = "com_google_cel_cpp")
15+
bazel_dep(name = "cel-cpp", version = "0.16.1", repo_name = "com_google_cel_cpp")
1616
git_override(
1717
module_name = "cel-cpp",
18-
commit = "76ae0b3c1768d93a10270f904101de338867bdb1",
18+
commit = "e6485ab94a6a4f1a9aead2b53e64ce2389917c1f",
1919
remote = "https://github.com/cel-expr/cel-cpp",
2020
)
2121

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_)),
@@ -105,7 +108,8 @@ PyCelEnvInternal::PyCelEnvInternal(
105108

106109
absl::StatusOr<std::shared_ptr<PyCelEnvInternal>>
107110
PyCelEnvInternal::NewCelEnvInternal(
108-
const PyCelEnvConfig& env_config, PyObject* py_descriptor_pool,
111+
const PyCelEnvConfig& env_config, const PyCelOptions& options,
112+
PyObject* py_descriptor_pool,
109113
const std::unordered_map<std::string, PyCelType>& variable_types,
110114
const std::vector<PyObject*>& extensions,
111115
cel::ExpressionContainer container,
@@ -219,7 +223,7 @@ PyCelEnvInternal::NewCelEnvInternal(
219223
}
220224
}
221225
return std::shared_ptr<PyCelEnvInternal>(
222-
new PyCelEnvInternal(PyCelEnvConfig(config), py_descriptor_pool,
226+
new PyCelEnvInternal(PyCelEnvConfig(config), options, py_descriptor_pool,
223227
std::move(extension_handles), impls));
224228
}
225229

@@ -237,6 +241,9 @@ absl::StatusOr<const cel::Compiler*> PyCelEnvInternal::GetCompiler(
237241
std::unique_ptr<cel::CompilerBuilder> compiler_builder,
238242
env->cel_env_.NewCompilerBuilder());
239243

244+
compiler_builder->GetParserBuilder().GetOptions().enable_pratt_parser =
245+
env->options_.enable_pratt_parser;
246+
240247
cel::TypeCheckerBuilder& checker_builder =
241248
compiler_builder->GetCheckerBuilder();
242249

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)