Skip to content

Commit 1e92352

Browse files
authored
feat(native): Jinja - passing filters via TemplateContext from Python (#7284)
1 parent e9c2a53 commit 1e92352

File tree

12 files changed

+229
-89
lines changed

12 files changed

+229
-89
lines changed

packages/cubejs-backend-native/js/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,9 @@ export const pythonLoadConfig = async (content: string, options: { fileName: str
364364
export type PythonCtx = {
365365
__type: 'PythonCtx'
366366
} & {
367+
filters: Record<string, Function>
367368
functions: Record<string, Function>
368-
variables: Record<string, Function>
369+
variables: Record<string, any>
369370
};
370371

371372
export interface JinjaEngine {
@@ -386,7 +387,7 @@ export class NativeInstance {
386387
return this.native;
387388
}
388389

389-
public newJinjaEngine(options: { debugInfo?: boolean }): JinjaEngine {
390+
public newJinjaEngine(options: { debugInfo?: boolean, filters: Record<string, Function> }): JinjaEngine {
390391
return this.getNative().newJinjaEngine(options);
391392
}
392393

packages/cubejs-backend-native/python/cube/src/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,12 @@ class TemplateException(Exception):
165165
class TemplateContext:
166166
functions: dict[str, Callable]
167167
variables: dict[str, Any]
168+
filters: dict[str, Callable]
168169

169170
def __init__(self):
170171
self.functions = {}
171172
self.variables = {}
173+
self.filters = {}
172174

173175
def add_function(self, name, func):
174176
if not callable(func):
@@ -186,7 +188,7 @@ def add_filter(self, name, func):
186188
if not callable(func):
187189
raise TemplateException("function registration must be used with functions, actual: '%s'" % type(func).__name__)
188190

189-
raise TemplateException("filter registration is not supported")
191+
self.filters[name] = func
190192

191193
def function(self, func):
192194
if isinstance(func, str):

packages/cubejs-backend-native/src/python/entry.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,15 @@ fn python_load_model(mut cx: FunctionContext) -> JsResult<JsPromise> {
7070
PyModule::from_code(py, cube_code, "__init__.py", "cube")?;
7171

7272
let model_module = PyModule::from_code(py, &model_content, &model_file_name, "")?;
73+
7374
let mut collected_functions = CLReprObject::new();
7475
let mut collected_variables = CLReprObject::new();
76+
let mut collected_filters = CLReprObject::new();
7577

7678
if model_module.hasattr("template")? {
7779
let template = model_module.getattr("template")?;
7880

7981
let functions = template.getattr("functions")?.downcast::<PyDict>()?;
80-
8182
for (local_key, local_value) in functions.iter() {
8283
if local_value.is_instance_of::<PyFunction>() {
8384
let fun: Py<PyFunction> = local_value.downcast::<PyFunction>()?.into();
@@ -89,12 +90,20 @@ fn python_load_model(mut cx: FunctionContext) -> JsResult<JsPromise> {
8990
}
9091

9192
let variables = template.getattr("variables")?.downcast::<PyDict>()?;
92-
9393
for (local_key, local_value) in variables.iter() {
9494
collected_variables
9595
.insert(local_key.to_string(), CLRepr::from_python_ref(local_value)?);
9696
}
9797

98+
let filters = template.getattr("filters")?.downcast::<PyDict>()?;
99+
for (local_key, local_value) in filters.iter() {
100+
let fun: Py<PyFunction> = local_value.downcast::<PyFunction>()?.into();
101+
collected_filters.insert(
102+
local_key.to_string(),
103+
CLRepr::PythonRef(PythonRef::PyExternalFunction(fun)),
104+
);
105+
}
106+
98107
// TODO remove all other ways of defining functions
99108
} else if model_module.hasattr("__execution_context_locals")? {
100109
let execution_context_locals = model_module
@@ -139,6 +148,7 @@ fn python_load_model(mut cx: FunctionContext) -> JsResult<JsPromise> {
139148
Ok(CubePythonModel::new(
140149
collected_functions,
141150
collected_variables,
151+
collected_filters,
142152
))
143153
});
144154

packages/cubejs-backend-native/src/python/python_model.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ use crate::cross::{CLRepr, CLReprObject};
55
pub struct CubePythonModel {
66
functions: CLReprObject,
77
variables: CLReprObject,
8+
filters: CLReprObject,
89
}
910

1011
impl CubePythonModel {
11-
pub fn new(functions: CLReprObject, variables: CLReprObject) -> Self {
12+
pub fn new(functions: CLReprObject, variables: CLReprObject, filters: CLReprObject) -> Self {
1213
Self {
1314
functions,
1415
variables,
16+
filters,
1517
}
1618
}
1719
}
@@ -24,6 +26,7 @@ impl CubePythonModel {
2426
let mut obj = CLReprObject::new();
2527
obj.insert("functions".to_string(), CLRepr::Object(self.functions));
2628
obj.insert("variables".to_string(), CLRepr::Object(self.variables));
29+
obj.insert("filters".to_string(), CLRepr::Object(self.filters));
2730

2831
CLRepr::Object(obj).into_js(cx)
2932
}

packages/cubejs-backend-native/src/template/entry.rs

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::cross::{CLRepr, CLReprObject};
2-
use crate::template::mj_value::to_minijinja_value;
1+
use crate::cross::*;
2+
use crate::template::mj_value::*;
33
use crate::utils::bind_method;
44
use log::trace;
55
use minijinja as mj;
@@ -8,6 +8,9 @@ use neon::prelude::*;
88
use std::cell::RefCell;
99
use std::error::Error;
1010

11+
#[cfg(feature = "python")]
12+
use pyo3::{exceptions::PyNotImplementedError, prelude::*, types::PyTuple, AsPyPointer};
13+
1114
trait NeonMiniJinjaContext {
1215
fn throw_from_mj_error<T>(&mut self, err: mj::Error) -> NeonResult<T>;
1316
}
@@ -100,6 +103,80 @@ impl JinjaEngine {
100103
);
101104
engine.set_auto_escape_callback(|_name: &str| mj::AutoEscape::Json);
102105

106+
#[cfg(feature = "python")]
107+
{
108+
let filters = options
109+
.get_value(cx, "filters")?
110+
.downcast_or_throw::<JsObject, _>(cx)?;
111+
112+
let filter_names = filters.get_own_property_names(cx)?;
113+
for i in 0..filter_names.len(cx) {
114+
let filter_name: Handle<JsString> = filter_names.get(cx, i)?;
115+
let filter_fun = CLRepr::from_js_ref(filters.get_value(cx, filter_name)?, cx)?;
116+
117+
let py_fun = match filter_fun {
118+
CLRepr::PythonRef(py_ref) => match py_ref {
119+
PythonRef::PyFunction(py_fun_ref)
120+
| PythonRef::PyExternalFunction(py_fun_ref) => py_fun_ref,
121+
other => {
122+
return cx.throw_error(format!(
123+
"minijinja::filter must be a function, actual: CLRepr::PythonRef({:?})",
124+
other
125+
))
126+
}
127+
},
128+
other => {
129+
return cx.throw_error(format!(
130+
"minijinja::filter must be a function, actual: {:?}",
131+
other.kind()
132+
))
133+
}
134+
};
135+
136+
engine.add_filter(
137+
filter_name.value(cx),
138+
move |_state: &mj::State,
139+
args: &[mj::value::Value]|
140+
-> Result<mj::value::Value, mj::Error> {
141+
let mut arguments = Vec::with_capacity(args.len());
142+
143+
for arg in args {
144+
arguments.push(from_minijinja_value(arg)?);
145+
}
146+
147+
let python_call_res = Python::with_gil(|py| {
148+
let mut args_tuple = Vec::with_capacity(args.len());
149+
150+
for arg in arguments {
151+
args_tuple.push(arg.into_py(py)?);
152+
}
153+
154+
let tuple = PyTuple::new(py, args_tuple);
155+
156+
let call_res = py_fun.call1(py, tuple)?;
157+
158+
let is_coroutine =
159+
unsafe { pyo3::ffi::PyCoro_CheckExact(call_res.as_ptr()) == 1 };
160+
if is_coroutine {
161+
Err(PyErr::new::<PyNotImplementedError, _>(
162+
"Calling async is not supported",
163+
))
164+
} else {
165+
CLRepr::from_python_ref(call_res.as_ref(py))
166+
}
167+
});
168+
match python_call_res {
169+
Ok(r) => Ok(to_minijinja_value(r)),
170+
Err(err) => Err(mj::Error::new(
171+
minijinja::ErrorKind::InvalidOperation,
172+
format!("Error while calling filter: {}", err),
173+
)),
174+
}
175+
},
176+
)
177+
}
178+
}
179+
103180
Ok(Self { inner: engine })
104181
}
105182
}

packages/cubejs-backend-native/src/template/mj_value/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ pub fn to_minijinja_value(from: CLRepr) -> mjv::Value {
3434
),
3535
}
3636
}
37+
38+
#[cfg(feature = "python")]
39+
pub use python::from_minijinja_value;

packages/cubejs-backend-native/test/__snapshots__/jinja.test.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,12 @@ print:
230230
unknown_fallback: \\"value\\""
231231
`;
232232

233+
exports[`Jinja (new api) render filters.yml.jinja: filters.yml.jinja 1`] = `
234+
"variables:
235+
str_filter: \\"str from python\\"
236+
str_filter_test_arg: \\"my string\\""
237+
`;
238+
233239
exports[`Jinja (new api) render python.yml: python.yml 1`] = `
234240
"test:
235241
unsafe_string: \\"\\"\\\\\\"unsafe string\\\\\\" <>\\"\\"

0 commit comments

Comments
 (0)