Skip to content

Commit 0b29682

Browse files
committed
Support lazy Python mappings via dynamic MapValue
1 parent efa8faf commit 0b29682

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+19726
-30
lines changed

Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ cel = { version = "0.11.4", features = ["chrono", "json", "regex"] }
1414
log = "0.4.27"
1515
pyo3-log = "0.12.4"
1616
chrono = { version = "0.4.41", features = ["serde"] }
17+
18+
[patch.crates-io]
19+
cel = { path = "vendor/cel" }

src/lib.rs

Lines changed: 160 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
mod context;
22

33
use ::cel::objects::{Key, TryIntoValue};
4+
use ::cel::types::map::MapValue;
45
use ::cel::{Context as CelContext, ExecutionError, Program, Value};
56
use log::debug;
67
use pyo3::exceptions::{PyRuntimeError, PyTypeError, PyValueError};
78
use pyo3::prelude::*;
89
use pyo3::BoundObject;
10+
use pyo3::IntoPyObject;
911
use std::panic::{self, AssertUnwindSafe};
1012

1113
use chrono::{DateTime, Duration as ChronoDuration, Offset, TimeZone};
12-
use pyo3::types::{PyBool, PyBytes, PyDict, PyList, PyTuple};
14+
use pyo3::types::{PyBool, PyBytes, PyDict, PyList, PyMapping, PyTuple};
1315

1416
use std::collections::HashMap;
1517
use std::error::Error;
@@ -66,6 +68,17 @@ impl<'py> IntoPyObject<'py> for RustyCelType {
6668

6769
python_dict.into_any()
6870
}
71+
RustyCelType(Value::DynamicMap(map)) => {
72+
let python_dict = PyDict::new(py);
73+
74+
for (key, value) in map.iter() {
75+
let key_obj = PyMappingValue::key_to_python(py, &key);
76+
let value_obj = RustyCelType(value).into_pyobject(py)?;
77+
python_dict.set_item(key_obj.bind(py), &value_obj)?;
78+
}
79+
80+
python_dict.into_any()
81+
}
6982

7083
// Turn everything else into a String:
7184
nonprimitive => format!("{nonprimitive:?}").into_pyobject(py)?.into_any(),
@@ -77,6 +90,115 @@ impl<'py> IntoPyObject<'py> for RustyCelType {
7790
#[derive(Debug)]
7891
struct RustyPyType<'a>(&'a Bound<'a, PyAny>);
7992

93+
#[derive(Clone)]
94+
struct PyMappingValue {
95+
mapping: Py<PyAny>,
96+
}
97+
98+
impl PyMappingValue {
99+
fn new(mapping: Py<PyAny>) -> Self {
100+
Self { mapping }
101+
}
102+
103+
fn key_to_python(py: Python<'_>, key: &Key) -> Py<PyAny> {
104+
match key {
105+
Key::Int(value) => value.into_pyobject(py).unwrap().unbind().into(),
106+
Key::Uint(value) => value.into_pyobject(py).unwrap().unbind().into(),
107+
Key::Bool(value) => value.into_pyobject(py).unwrap().unbind().into(),
108+
Key::String(value) => value.as_str().into_pyobject(py).unwrap().unbind().into(),
109+
}
110+
}
111+
112+
fn py_to_key(obj: &Bound<'_, PyAny>) -> Option<Key> {
113+
if obj.is_none() {
114+
return None;
115+
}
116+
117+
if let Ok(value) = obj.extract::<i64>() {
118+
Some(Key::Int(value))
119+
} else if let Ok(value) = obj.extract::<u64>() {
120+
Some(Key::Uint(value))
121+
} else if let Ok(value) = obj.extract::<bool>() {
122+
Some(Key::Bool(value))
123+
} else if let Ok(value) = obj.extract::<String>() {
124+
Some(Key::String(value.into()))
125+
} else {
126+
None
127+
}
128+
}
129+
}
130+
131+
impl fmt::Debug for PyMappingValue {
132+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133+
f.debug_struct("PyMappingValue").finish()
134+
}
135+
}
136+
137+
impl MapValue for PyMappingValue {
138+
fn get(&self, key: &Key) -> Option<Value> {
139+
Python::with_gil(|py| {
140+
let bound = self.mapping.bind(py);
141+
let mapping = bound.downcast::<PyMapping>().ok()?;
142+
let py_key = Self::key_to_python(py, key);
143+
let value = mapping.get_item(py_key).ok()?;
144+
RustyPyType(&value).try_into_value().ok()
145+
})
146+
}
147+
148+
fn contains_key(&self, key: &Key) -> bool {
149+
Python::with_gil(|py| {
150+
let bound = self.mapping.bind(py);
151+
let mapping = match bound.downcast::<PyMapping>() {
152+
Ok(mapping) => mapping,
153+
Err(_) => return false,
154+
};
155+
let py_key = Self::key_to_python(py, key);
156+
mapping.contains(py_key).unwrap_or(false)
157+
})
158+
}
159+
160+
fn len(&self) -> usize {
161+
Python::with_gil(|py| {
162+
let bound = self.mapping.bind(py);
163+
bound
164+
.downcast::<PyMapping>()
165+
.ok()
166+
.and_then(|mapping| mapping.len().ok())
167+
.unwrap_or(0)
168+
})
169+
}
170+
171+
fn iter(&self) -> Box<dyn Iterator<Item = (Key, Value)> + '_> {
172+
let items = Python::with_gil(|py| {
173+
let bound = self.mapping.bind(py);
174+
let mapping = match bound.downcast::<PyMapping>() {
175+
Ok(mapping) => mapping,
176+
Err(_) => return Vec::new(),
177+
};
178+
let list = match mapping.items() {
179+
Ok(items) => items,
180+
Err(_) => return Vec::new(),
181+
};
182+
183+
list.iter()
184+
.filter_map(|item| {
185+
let tuple = item.downcast::<PyTuple>().ok()?;
186+
if tuple.len() != 2 {
187+
return None;
188+
}
189+
let key_obj = tuple.get_item(0).ok()?;
190+
let value_obj = tuple.get_item(1).ok()?;
191+
let key = Self::py_to_key(&key_obj)?;
192+
let value = RustyPyType(&value_obj).try_into_value().ok()?;
193+
Some((key, value))
194+
})
195+
.collect::<Vec<_>>()
196+
});
197+
198+
Box::new(items.into_iter())
199+
}
200+
}
201+
80202
#[derive(Debug, PartialEq, Clone)]
81203
pub enum CelError {
82204
ConversionError(String),
@@ -212,35 +334,45 @@ impl TryIntoValue for RustyPyType<'_> {
212334
.map(|item| RustyPyType(&item).try_into_value())
213335
.collect::<Result<Vec<Value>, Self::Error>>();
214336
list.map(|v| Value::List(Arc::new(v)))
215-
} else if let Ok(value) = pyobject.downcast::<PyDict>() {
216-
let mut map: HashMap<Key, Value> = HashMap::new();
217-
for (key, value) in value.into_iter() {
218-
let key = if key.is_none() {
219-
return Err(CelError::ConversionError(
220-
"None cannot be used as a key in dictionaries".to_string(),
221-
));
222-
} else if let Ok(k) = key.extract::<i64>() {
223-
Key::Int(k)
224-
} else if let Ok(k) = key.extract::<u64>() {
225-
Key::Uint(k)
226-
} else if let Ok(k) = key.extract::<bool>() {
227-
Key::Bool(k)
228-
} else if let Ok(k) = key.extract::<String>() {
229-
Key::String(k.into())
230-
} else {
231-
return Err(CelError::ConversionError(
232-
"Failed to convert PyDict key to Key".to_string(),
233-
));
234-
};
235-
if let Ok(dict_value) = RustyPyType(&value).try_into_value() {
236-
map.insert(key, dict_value);
237-
} else {
238-
return Err(CelError::ConversionError(
239-
"Failed to convert PyDict value to Value".to_string(),
240-
));
337+
} else if let Ok(dict) = pyobject.downcast::<PyDict>() {
338+
if pyobject.is_exact_instance_of::<PyDict>() {
339+
let mut map: HashMap<Key, Value> = HashMap::new();
340+
for (key, value) in dict.iter() {
341+
let key = if key.is_none() {
342+
return Err(CelError::ConversionError(
343+
"None cannot be used as a key in dictionaries".to_string(),
344+
));
345+
} else if let Ok(k) = key.extract::<i64>() {
346+
Key::Int(k)
347+
} else if let Ok(k) = key.extract::<u64>() {
348+
Key::Uint(k)
349+
} else if let Ok(k) = key.extract::<bool>() {
350+
Key::Bool(k)
351+
} else if let Ok(k) = key.extract::<String>() {
352+
Key::String(k.into())
353+
} else {
354+
return Err(CelError::ConversionError(
355+
"Failed to convert PyDict key to Key".to_string(),
356+
));
357+
};
358+
if let Ok(dict_value) = RustyPyType(&value).try_into_value() {
359+
map.insert(key, dict_value);
360+
} else {
361+
return Err(CelError::ConversionError(
362+
"Failed to convert PyDict value to Value".to_string(),
363+
));
364+
}
241365
}
366+
Ok(Value::Map(map.into()))
367+
} else {
368+
Ok(Value::DynamicMap(Arc::new(PyMappingValue::new(
369+
dict.clone().into_any().unbind(),
370+
))))
242371
}
243-
Ok(Value::Map(map.into()))
372+
} else if let Ok(mapping) = pyobject.downcast::<PyMapping>() {
373+
Ok(Value::DynamicMap(Arc::new(PyMappingValue::new(
374+
mapping.clone().into_any().unbind(),
375+
))))
244376
} else if let Ok(value) = pyobject.extract::<Vec<u8>>() {
245377
Ok(Value::Bytes(value.into()))
246378
} else {

tests/test_context.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,20 @@ def test_nested_context_none():
107107
assert cel.evaluate("spec.host", cel_context) == "github.com"
108108
assert cel.evaluate("data['response-code']", cel_context) == "NOERROR"
109109
assert cel.evaluate("size(data.A)", cel_context) == 1
110+
111+
112+
def test_lazy_mapping_lookup():
113+
class LazyDict(dict):
114+
def __init__(self):
115+
super().__init__()
116+
self._storage = {}
117+
118+
def __getitem__(self, key):
119+
if key not in self._storage:
120+
self._storage[key] = f"computed-{key}"
121+
return self._storage[key]
122+
123+
data = LazyDict()
124+
context = cel.Context({"data": data})
125+
126+
assert cel.evaluate("data.key", context) == "computed-key"

vendor/cel/.cargo-ok

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"v":1}

vendor/cel/.cargo_vcs_info.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"git": {
3+
"sha1": "4e4d40d33529add8a4e3dd70ddacc7c7b5a91663"
4+
},
5+
"path_in_vcs": "cel"
6+
}

0 commit comments

Comments
 (0)