Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ variables are not known at compile time.

### Evaluation

To evaluate a compiled expression, you need to create an activation, which
To evaluate a compiled expression, you need to provide bindings for
variables and then call `eval()`.

you need to create an activation, which
provides bindings for variables, and then call `eval()`.

```python
# Provide variable values in a dictionary.
activation = cel_env.Activation({"x": 7, "y": 4})

# Evaluate the expression.
result = expr.eval(activation)
# Provide variable values in a dictionary and evaluate the expression.
result = expr.eval(data={"x": 7, "y": 4})

# The result is a `CelValue` object, which contains the result's CEL type and
# value.
Expand All @@ -82,9 +82,32 @@ Result type: BOOL
Result value: True
```

### Using an Activation

The `eval()` function can also be invoked with an `Activation` object that
holds variable bindings and a pointer to an `Arena` (see below).
This is particularly useful when multiple expressions need to be evaluated
with the same set of variable values, such as multiple policies
on the same server request.

```python
expr1 = cel_env.compile("user.role in ['admin', 'owner']")
expr2 = cel_env.compile("user.organization == 'myorg'")

# Provide variable values as an Activation.
activation = cel_env.Activation({"user": user})

# Evaluate the expression.
result1 = expr1.eval(activation)

# Evaluate another expression using the same variable bindings
result2 = expr2.eval(activation)
```

### Using an Arena

An `Activation` can also take an `Arena` for memory management during
The `eval()` function as well as an `Activation` can also take an `Arena`
for memory management during
evaluation. This is a memory optimization technique that allows temporary
C++ objects created during the evaluation to be released as a group. The same
`Arena` can be shared across multiple activations; just keep in mind that none
Expand Down
38 changes: 36 additions & 2 deletions py_cel_expression.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>

#include "cel/expr/checked.pb.h"
#include "cel/expr/syntax.pb.h"
Expand All @@ -43,14 +46,15 @@
#include "py_cel_activation.h"
#include "py_cel_arena.h"
#include "py_cel_env_internal.h"
#include "py_cel_function.h"
#include "py_cel_type.h"
#include "py_cel_value.h"
#include "py_error_status.h"
#include "status_macros.h"
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "pybind11_abseil/status_casters.h"


namespace cel_python {

using ::cel::expr::CheckedExpr;
Expand All @@ -67,7 +71,37 @@ void PyCelExpression::DefinePythonBindings(py::module& m) {
// decoder from std::string to Python `str`.
return py::bytes(self.PyCelExpression::Serialize());
})
.def("eval", &PyCelExpression::Eval, py::arg("activation"));
.def(
"eval",
[](PyCelExpression& self,
std::optional<std::shared_ptr<PyCelActivation>> activation,
const std::optional<std::unordered_map<std::string, py::object>>&
data,
const std::optional<std::vector<std::shared_ptr<PyCelFunction>>>&
functions,
const std::shared_ptr<PyCelArena>& arena =
nullptr) -> absl::StatusOr<PyCelValue> {
if (activation) {
if (data || functions || arena) {
return absl::InvalidArgumentError(
"Cannot provide both activation and any other arguments.");
}
return self.Eval(**activation);
}
std::unordered_map<std::string, PyObject*> data_ptrs;
if (data) {
for (auto const& [key, val] : *data) {
data_ptrs[key] = val.ptr();
}
}
return self.Eval(PyCelActivation(
self.env_, data_ptrs,
functions.value_or(
std::vector<std::shared_ptr<PyCelFunction>>{}),
arena ? arena : NewArena()));
},
py::arg("activation") = py::none(), py::arg("data") = py::none(),
py::arg("functions") = py::none(), py::arg("arena") = nullptr);
}

absl::StatusOr<PyCelExpression> PyCelExpression::Compile(
Expand Down
15 changes: 15 additions & 0 deletions py_cel_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,21 @@ def testActivationWithArena(self):
gc.collect()
self.assertEqual(cel._InternalArena._get_instance_count(), 0)

def testImplicitActivation(self):
expr = self.env.compile("'Hello, ' + var_str")
res = expr.eval(data={"var_str": "World!"})
self.assertEqual(res.value(), "Hello, World!")

def testActivationAndOtherArgs(self):
expr = self.env.compile("'Hello, ' + var_str")
with self.assertRaises(Exception) as e:
expr.eval(
self.env.Activation(data={"var_str": "World!"}),
data={"var_str": "World!"},
)
self.assertIn("Cannot provide both activation and any other arguments",
str(e.exception))

def testCompilationErrorHandling(self):
# Check parser error.
with self.assertRaises(Exception) as e:
Expand Down