Skip to content

Commit 828f4e8

Browse files
dmitriplotnikovcopybara-github
authored andcommitted
Add extension version and alias support
PiperOrigin-RevId: 893216967
1 parent 1ec5a73 commit 828f4e8

20 files changed

Lines changed: 416 additions & 204 deletions

MODULE.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ bazel_dep(name = "bazel_skylib", version = "1.8.2")
1414
# https://registry.bazel.build/modules/cel-cpp
1515
bazel_dep(name = "cel-cpp", version = "0.14.0", repo_name = "com_google_cel_cpp")
1616

17-
# 03/30/2026
17+
# 04/01/2026
1818
_CEL_CPP_COMMIT = "5a3463337cf2a9b90b53833af2bbc1f35da90d64"
1919

2020
_CEL_CPP_SHA256 = "299be398d1495340eb92da31f9a0667e1351479752e8a567ac31c385e4aea73c"

README.md

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -234,36 +234,34 @@ expr = cel_env.compile("my_func(1)")
234234

235235
To define a custom extension in C++, define a class extending
236236
`cel_python::CelExtension`. There are two methods you will need to implement:
237-
`ConfigureCompiler` and `ConfigureRuntime`. The implementations of these methods
238-
use the same API as extensions written for the C++ CEL runtime. In fact,
237+
`GetCompilerLibrary` and `ConfigureRuntime`. The implementations of these
238+
methods use the same API as extensions written for the C++ CEL runtime. In fact,
239239
extensions written for the C++ runtime can be used unchanged with
240240
cel-expr-python - you would just need to write a trivial wrapper class invoking
241241
the registration functions defined by the C++ extension.
242242

243-
```cpp
244-
absl::Status ConfigureCompiler(
245-
cel::CompilerBuilder& compiler_builder,
246-
const proto2::DescriptorPool& descriptor_pool);
247-
```
248243
This method adds extension function definitions to the provided
249244
`CompilerBuilder`, for example:
250245

251246
```cpp
252-
absl::Status ConfigureCompiler(
253-
cel::CompilerBuilder& compiler_builder,
254-
const proto2::DescriptorPool& descriptor_pool) {
255-
CEL_PYTHON_ASSIGN_OR_RETURN(
256-
auto func_translate,
257-
cel::MakeFunctionDecl("translate",
258-
cel::MakeMemberOverloadDecl("translate_inst",
259-
/*return_type=*/cel::StringType(),
260-
/*target=*/cel::StringType(),
261-
/*from_lang=*/cel::StringType(),
262-
/*to_lang=*/cel::StringType())));
263-
CEL_PYTHON_RETURN_IF_ERROR(
264-
compiler_builder.GetCheckerBuilder().AddFunction(func_translate));
265-
return absl::OkStatus();
266-
}
247+
cel::CompilerLibrary GetCompilerLibrary() {
248+
return cel::CompilerLibrary(
249+
"translate-ext",
250+
[](cel::TypeCheckerBuilder& checker_builder) -> absl::Status {
251+
CEL_PYTHON_ASSIGN_OR_RETURN(
252+
auto func_translate,
253+
cel::MakeFunctionDecl(
254+
"translate",
255+
cel::MakeMemberOverloadDecl("translate_inst",
256+
/*return_type=*/cel::StringType(),
257+
/*target=*/cel::StringType(),
258+
/*from_lang=*/cel::StringType(),
259+
/*to_lang=*/cel::StringType())));
260+
CEL_PYTHON_RETURN_IF_ERROR(
261+
checker_builder.AddFunction(func_translate));
262+
return absl::OkStatus();
263+
});
264+
}
267265
```
268266
269267
The other method registers the actual implementation

cel_expr_python/BUILD

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pybind_extension(
6363
"@com_google_absl//absl/types:optional",
6464
"@com_google_absl//absl/types:span",
6565
"@com_google_cel_cpp//checker:type_checker_builder",
66+
"@com_google_cel_cpp//checker:type_checker_builder_factory",
6667
"@com_google_cel_cpp//checker:validation_result",
6768
"@com_google_cel_cpp//common:ast",
6869
"@com_google_cel_cpp//common:ast_proto",
@@ -78,8 +79,13 @@ pybind_extension(
7879
"@com_google_cel_cpp//compiler",
7980
"@com_google_cel_cpp//env",
8081
"@com_google_cel_cpp//env:config",
82+
"@com_google_cel_cpp//env:env_runtime",
83+
"@com_google_cel_cpp//env:env_std_extensions",
8184
"@com_google_cel_cpp//env:env_yaml",
85+
"@com_google_cel_cpp//env:runtime_std_extensions",
8286
"@com_google_cel_cpp//extensions/protobuf:runtime_adapter",
87+
"@com_google_cel_cpp//parser",
88+
"@com_google_cel_cpp//parser:options",
8389
"@com_google_cel_cpp//parser:parser_interface",
8490
"@com_google_cel_cpp//runtime",
8591
"@com_google_cel_cpp//runtime:activation",
@@ -88,7 +94,6 @@ pybind_extension(
8894
"@com_google_cel_cpp//runtime:reference_resolver",
8995
"@com_google_cel_cpp//runtime:runtime_builder",
9096
"@com_google_cel_cpp//runtime:runtime_options",
91-
"@com_google_cel_cpp//runtime:standard_runtime_builder_factory",
9297
"@com_google_cel_spec//proto/cel/expr:checked_cc_proto",
9398
"@com_google_cel_spec//proto/cel/expr:syntax_cc_proto",
9499
"@com_google_protobuf//:protobuf",
@@ -99,14 +104,25 @@ pybind_extension(
99104
# For pybind11-based CEL extensions.
100105
pybind_library(
101106
name = "cel_extension",
102-
hdrs = ["cel_extension.h"],
107+
srcs = [
108+
"py_error_status.cc",
109+
],
110+
hdrs = [
111+
"cel_extension.h",
112+
"py_error_status.h",
113+
],
103114
visibility = ["//visibility:public"],
104115
deps = [
116+
":status_macros",
117+
"@com_google_absl//absl/base:no_destructor",
118+
"@com_google_absl//absl/container:flat_hash_map",
119+
"@com_google_absl//absl/log:absl_check",
120+
"@com_google_absl//absl/log:absl_log",
105121
"@com_google_absl//absl/status",
122+
"@com_google_absl//absl/status:statusor",
106123
"@com_google_cel_cpp//compiler",
107124
"@com_google_cel_cpp//runtime:runtime_builder",
108125
"@com_google_cel_cpp//runtime:runtime_options",
109-
"@com_google_protobuf//:protobuf",
110126
],
111127
)
112128

@@ -135,6 +151,10 @@ py_test(
135151
srcs = ["cel_env_test.py"],
136152
deps = [
137153
":cel",
154+
"//cel_expr_python/ext:ext_bindings",
155+
"//cel_expr_python/ext:ext_math",
156+
"//cel_expr_python/ext:ext_optional",
157+
"//cel_expr_python/ext:ext_strings",
138158
"//testing:proto2_test_all_types_py_pb2",
139159
"@com_google_absl_py//absl/testing:absltest",
140160
],

cel_expr_python/cel_env_test.py

Lines changed: 156 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@
1818
ability to be created from and serialized to YAML format.
1919
"""
2020

21+
import textwrap
22+
2123
from absl.testing import absltest
2224
from cel_expr_python import cel
25+
from cel_expr_python.ext import ext_bindings
26+
from cel_expr_python.ext import ext_math
27+
from cel_expr_python.ext import ext_optional
28+
from cel_expr_python.ext import ext_strings
2329
from cel.expr.conformance.proto2 import test_all_types_pb2 as test_all_types_pb
2430

2531

@@ -91,6 +97,16 @@ def test_invalid_yaml(self):
9197
str(e.exception),
9298
)
9399

100+
def test_config_export_container(self):
101+
env = cel.NewEnv(container="test.container")
102+
yaml = env.config().to_yaml()
103+
self.assertEqual(
104+
normalize_yaml(yaml),
105+
normalize_yaml("""
106+
container: "test.container"
107+
"""),
108+
)
109+
94110
def test_config_export_variables(self):
95111
config = cel.NewEnv(
96112
variables={
@@ -236,27 +252,148 @@ def test_config_variable_types(self):
236252
self.assertEqual(res.type(), cel.Type.INT)
237253
self.assertEqual(res.value(), 42)
238254

255+
def test_config_export_extension_version(self):
256+
env = cel.NewEnv(
257+
extensions=[
258+
ext_math.ExtMath(0),
259+
ext_optional.ExtOptional(1),
260+
ext_strings.ExtStrings(2),
261+
ext_bindings.ExtBindings(),
262+
],
263+
)
264+
yaml = env.config().to_yaml()
265+
self.assertEqual(
266+
normalize_yaml(yaml),
267+
normalize_yaml("""
268+
extensions:
269+
- name: "bindings"
270+
- name: "math"
271+
version: 0
272+
- name: "optional"
273+
version: 1
274+
- name: "strings"
275+
version: 2
276+
"""),
277+
)
278+
279+
def test_config_extension_version_out_of_range(self):
280+
cases = [
281+
[
282+
lambda: ext_math.ExtMath(42),
283+
r"'math' extension version: 42 not in range \[0, \d+\]",
284+
],
285+
[
286+
lambda: ext_optional.ExtOptional(6),
287+
r"'optional' extension version: 6 not in range \[0, \d+\]",
288+
],
289+
[
290+
lambda: ext_strings.ExtStrings(18),
291+
r"'strings' extension version: 18 not in range \[0, \d+\]",
292+
],
293+
]
294+
for test_case in cases:
295+
with self.assertRaises(Exception) as e:
296+
cel.NewEnv(
297+
extensions=[test_case[0]()],
298+
)
299+
self.assertRegex(str(e.exception), test_case[1])
300+
301+
def test_config_extensions(self):
302+
config = cel.NewEnvConfigFromYaml("""
303+
extensions:
304+
- name: math
305+
- name: strings
306+
""")
307+
env = cel.NewEnv(
308+
config=config,
309+
extensions=[TestCelExtension()],
310+
)
311+
yaml = env.config().to_yaml()
312+
self.assertEqual(
313+
normalize_yaml(yaml),
314+
normalize_yaml("""
315+
extensions:
316+
- name: "math"
317+
- name: "strings"
318+
- name: "test_cel_extension"
319+
"""),
320+
)
321+
res = env.compile("'%.4f'.format([math.sqrt(2)])").eval()
322+
self.assertEqual(res.value(), "1.4142")
323+
res = env.compile("hello('World')").eval()
324+
self.assertEqual(res.value(), "Hello, World!")
325+
326+
def test_config_extension_override_same_version(self):
327+
config = cel.NewEnvConfigFromYaml("""
328+
extensions:
329+
- name: cel.lib.ext.math
330+
version: 1
331+
- name: strings
332+
version: 2
333+
""")
334+
env = cel.NewEnv(
335+
config=config,
336+
extensions=[ext_math.ExtMath(1), ext_strings.ExtStrings(2)],
337+
)
338+
res = env.compile("'%.3f'.format([math.floor(3.14)])").eval()
339+
self.assertEqual(res.value(), "3.000")
340+
341+
def test_config_extension_override_different_version(self):
342+
config = cel.NewEnvConfigFromYaml("""
343+
extensions:
344+
- name: math
345+
version: 0
346+
- name: cel.lib.ext.strings
347+
version: 2
348+
""")
349+
with self.assertRaises(Exception) as e:
350+
cel.NewEnv(
351+
config=config,
352+
extensions=[ext_math.ExtMath()],
353+
)
354+
self.assertIn(
355+
"Extension 'math' version 0 is already included. Cannot"
356+
" also include version 2",
357+
str(e.exception),
358+
)
359+
with self.assertRaises(Exception) as e:
360+
cel.NewEnv(
361+
config=config,
362+
extensions=[ext_strings.ExtStrings(1)],
363+
)
364+
self.assertIn(
365+
"Extension 'cel.lib.ext.strings' version 2 is already included. Cannot"
366+
" also include version 1",
367+
str(e.exception),
368+
)
369+
370+
371+
class TestCelExtension(cel.CelExtension):
372+
"""An example CEL extension for testing."""
373+
374+
def __init__(self):
375+
super().__init__(
376+
"test_cel_extension",
377+
functions=[
378+
cel.FunctionDecl(
379+
"hello",
380+
[
381+
cel.Overload(
382+
"hello(string)",
383+
return_type=cel.Type.STRING,
384+
parameters=[
385+
cel.Type.STRING,
386+
],
387+
impl=lambda arg: f"Hello, {arg}!",
388+
)
389+
],
390+
),
391+
],
392+
)
393+
239394

240395
def normalize_yaml(yaml: str) -> str:
241-
lines = yaml.split("\n")
242-
indent = -1
243-
unindented_lines = []
244-
for line in lines:
245-
pos = -1
246-
for i, char in enumerate(line):
247-
if char != " " and char != "\t":
248-
pos = i
249-
break
250-
if pos == -1:
251-
# Skip blank lines.
252-
continue
253-
if indent == -1:
254-
indent = pos
255-
if pos >= indent:
256-
unindented_lines.append(line[indent:])
257-
else:
258-
unindented_lines.append(line)
259-
return "\n".join(unindented_lines)
396+
return textwrap.dedent(yaml).strip()
260397

261398

262399
if __name__ == "__main__":

0 commit comments

Comments
 (0)