Skip to content

Commit 57777ad

Browse files
dmitriplotnikovcopybara-github
authored andcommitted
Add explicit declaration of functions as NewEnv(functions=[...])
PiperOrigin-RevId: 896175602
1 parent 07a04e7 commit 57777ad

10 files changed

Lines changed: 328 additions & 27 deletions

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,76 @@ Resulting message type: <class '...TestAllTypes'>
188188
Resulting message value: 123
189189
```
190190

191+
### Custom functions
192+
193+
When configuring `cel.Env` you can supply custom functions. For each function
194+
there needs to be a declaration and an implementation. The implementation, which
195+
is a Python function, can be provided either as part of the declaration
196+
or separately.
197+
198+
Let's say we want to be able to invoke this function from CEL expressions:
199+
```python
200+
def good_time_of_day(ampm, arg):
201+
if ampm == 'am':
202+
time_of_day = 'morning'
203+
else:
204+
time_of_day = 'afternoon'
205+
return f"Good {time_of_day}, {arg}"
206+
```
207+
208+
The implementation can be supplied along with the declaration:
209+
```python
210+
cel_env = cel.NewEnv(functions=[
211+
cel.FunctionDecl(
212+
"hello",
213+
[
214+
cel.Overload(
215+
"hello(string,string)",
216+
return_type=cel.Type.STRING,
217+
parameters=[
218+
cel.Type.STRING,
219+
cel.Type.STRING,
220+
],
221+
impl=good_time_of_day,
222+
)
223+
],
224+
)
225+
])
226+
```
227+
228+
It can also be provided separately in a dictionary that maps overload IDs to
229+
their respective implementations:
230+
231+
```python
232+
cel_env = cel.NewEnv(functions=[
233+
cel.FunctionDecl(
234+
"hello",
235+
[
236+
cel.Overload(
237+
"hello(string,string)",
238+
return_type=cel.Type.STRING,
239+
parameters=[
240+
cel.Type.STRING,
241+
cel.Type.STRING,
242+
],
243+
)
244+
],
245+
)
246+
],
247+
function_impls={
248+
"hello(string,string)": good_time_of_day,
249+
})
250+
```
251+
252+
Now that the function implementation is bound to the CEL environment,
253+
we can invoke it from CEL like this:
254+
```python
255+
result = env.compile("hello('am', 'breakfast is ready!')").eval()
256+
print(result.value()) # Good morning, breakfast is ready!
257+
result = env.compile("hello('pm', 'tea is served.')").eval()
258+
print(result.value()) # Good afternoon, tea is served.
259+
```
260+
191261
### Extensions
192262

193263
#### Standard extensions

cel_expr_python/cel_env_test.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,105 @@ def test_config_extension_override_different_version(self):
367367
str(e.exception),
368368
)
369369

370+
def test_config_functions(self):
371+
config = cel.NewEnvConfigFromYaml("""
372+
functions:
373+
- name: is_ok
374+
overloads:
375+
- id: "is_ok_string"
376+
target:
377+
type_name: string
378+
return:
379+
type_name: bool
380+
""")
381+
env = cel.NewEnv(
382+
config=config,
383+
functions=[
384+
cel.FunctionDecl(
385+
"hello",
386+
[
387+
cel.Overload(
388+
"good_time_of_day",
389+
return_type=cel.Type.STRING,
390+
parameters=[
391+
cel.Type.STRING,
392+
cel.Type.STRING,
393+
],
394+
impl=lambda ampm, arg: (
395+
"Good"
396+
f" {'morning' if ampm == 'am' else 'afternoon'},"
397+
f" {arg}!"
398+
),
399+
)
400+
],
401+
)
402+
],
403+
function_impls={
404+
"is_ok_string": lambda arg: arg in ["excellent", "good", "fair"],
405+
},
406+
)
407+
yaml = env.config().to_yaml()
408+
self.assertEqual(
409+
normalize_yaml(yaml),
410+
normalize_yaml("""
411+
functions:
412+
- name: "hello"
413+
overloads:
414+
- id: "good_time_of_day"
415+
args:
416+
- type_name: "string"
417+
- type_name: "string"
418+
return:
419+
type_name: "string"
420+
- name: "is_ok"
421+
overloads:
422+
- id: "is_ok_string"
423+
target:
424+
type_name: "string"
425+
return:
426+
type_name: "bool"
427+
"""),
428+
)
429+
res = env.compile("hello('am', 'Sunshine')").eval()
430+
self.assertEqual(res.value(), "Good morning, Sunshine!")
431+
res = env.compile("hello('pm', 'tea is served')").eval()
432+
self.assertEqual(res.value(), "Good afternoon, tea is served!")
433+
res = env.compile("'good'.is_ok()").eval()
434+
self.assertTrue(res.value())
435+
res = env.compile("'bad'.is_ok()").eval()
436+
self.assertFalse(res.value())
437+
438+
def test_config_function_override(self):
439+
config = cel.NewEnvConfigFromYaml("""
440+
functions:
441+
- name: foo
442+
overloads:
443+
- id: "unique_id"
444+
""")
445+
with self.assertRaises(Exception) as e:
446+
cel.NewEnv(
447+
config=config,
448+
functions=[
449+
cel.FunctionDecl(
450+
"bar",
451+
[
452+
cel.Overload(
453+
"unique_id",
454+
impl=lambda: "hello",
455+
)
456+
],
457+
)
458+
],
459+
function_impls={
460+
"unique_id": lambda: "goodbye",
461+
},
462+
)
463+
self.assertIn(
464+
"An implementation for function overload id 'unique_id' already"
465+
" exists.",
466+
str(e.exception),
467+
)
468+
370469

371470
class TestCelExtension(cel.CelExtension):
372471
"""An example CEL extension for testing."""

cel_expr_python/py_cel_env.cc

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "cel_expr_python/py_cel_env_config.h"
3232
#include "cel_expr_python/py_cel_env_internal.h"
3333
#include "cel_expr_python/py_cel_expression.h"
34+
#include "cel_expr_python/py_cel_function_decl.h"
3435
#include "cel_expr_python/py_cel_type.h"
3536
#include "cel_expr_python/py_error_status.h"
3637
#include <pybind11/pybind11.h>
@@ -44,10 +45,14 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) {
4445
py::class_<PyCelEnv, std::shared_ptr<PyCelEnv>> cel_class(m, "Env");
4546
m.def(
4647
"NewEnv",
47-
[](py::object descriptor_pool, std::optional<PyCelEnvConfig> config,
48-
std::optional<std::unordered_map<std::string, PyCelType>> variables,
49-
std::optional<std::vector<py::object>> extensions,
50-
const std::optional<std::string>& container) {
48+
[](py::object descriptor_pool, std::optional<PyCelEnvConfig>& config,
49+
std::optional<std::unordered_map<std::string, PyCelType>>& variables,
50+
std::optional<std::vector<py::object>>& extensions,
51+
const std::optional<std::string>& container,
52+
std::optional<std::vector<std::shared_ptr<PyCelFunctionDecl>>>&
53+
functions,
54+
std::optional<std::unordered_map<std::string, py::object>>&
55+
function_impls) {
5156
PyObject* pool_ptr;
5257
if (descriptor_pool.is_none()) {
5358
// Replicates python's `descriptor_pool.Default()`
@@ -75,11 +80,16 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) {
7580
return PyCelEnv(config.value_or(PyCelEnvConfig()), pool_ptr,
7681
std::move(variables).value_or(
7782
std::unordered_map<std::string, PyCelType>{}),
78-
ext_ptrs, container.value_or(""));
83+
ext_ptrs, container.value_or(""),
84+
functions.value_or(
85+
std::vector<std::shared_ptr<PyCelFunctionDecl>>{}),
86+
function_impls.value_or(
87+
std::unordered_map<std::string, py::object>{}));
7988
},
8089
py::arg("descriptor_pool") = py::none(), py::arg("config") = py::none(),
8190
py::arg("variables") = py::none(), py::arg("extensions") = py::none(),
82-
py::arg("container") = py::none());
91+
py::arg("container") = py::none(), py::arg("functions") = py::none(),
92+
py::arg("function_impls") = py::none());
8393
cel_class
8494
.def("config",
8595
[](PyCelEnv& self) { return self.GetEnv()->GetEnvConfig(); })
@@ -112,13 +122,15 @@ void PyCelEnv::DefinePythonBindings(pybind11::module& m) {
112122
py::arg("arena") = nullptr);
113123
}
114124

115-
PyCelEnv::PyCelEnv(const PyCelEnvConfig& config, PyObject* descriptor_pool,
116-
std::unordered_map<std::string, PyCelType> variable_types,
117-
const std::vector<PyObject*>& extensions,
118-
std::string container) {
125+
PyCelEnv::PyCelEnv(
126+
const PyCelEnvConfig& config, PyObject* descriptor_pool,
127+
const std::unordered_map<std::string, PyCelType>& variable_types,
128+
const std::vector<PyObject*>& extensions, const std::string& container,
129+
const std::vector<std::shared_ptr<PyCelFunctionDecl>>& functions,
130+
const std::unordered_map<std::string, py::object>& function_impls) {
119131
env_ = ThrowIfError(PyCelEnvInternal::NewCelEnvInternal(
120132
config, descriptor_pool, std::move(variable_types), extensions,
121-
std::move(container)));
133+
std::move(container), std::move(functions), std::move(function_impls)));
122134
ABSL_CHECK(PyGILState_Check());
123135
}
124136

cel_expr_python/py_cel_env.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "cel_expr_python/py_cel_env_config.h"
2929
#include "cel_expr_python/py_cel_expression.h"
3030
#include "cel_expr_python/py_cel_function.h"
31+
#include "cel_expr_python/py_cel_function_decl.h"
3132
#include "cel_expr_python/py_cel_type.h"
3233
#include <pybind11/pybind11.h>
3334

@@ -67,8 +68,11 @@ class PyCelEnv {
6768
private:
6869
// Private constructor. Use `py_cel.NewEnv()` in python to obtain an instance.
6970
PyCelEnv(const PyCelEnvConfig& config, PyObject* descriptor_pool,
70-
std::unordered_map<std::string, PyCelType> variable_types,
71-
const std::vector<PyObject*>& extensions, std::string container);
71+
const std::unordered_map<std::string, PyCelType>& variable_types,
72+
const std::vector<PyObject*>& extensions,
73+
const std::string& container,
74+
const std::vector<std::shared_ptr<PyCelFunctionDecl>>& functions,
75+
const std::unordered_map<std::string, py::object>& function_impls);
7276

7377
std::shared_ptr<PyCelEnvInternal> env_;
7478
};

0 commit comments

Comments
 (0)