Skip to content

Commit e3dec88

Browse files
authored
feat(native): Jinja - passing variables via TemplateContext from Python (#7280)
1 parent 1f6d49d commit e3dec88

File tree

9 files changed

+88
-18
lines changed

9 files changed

+88
-18
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,12 +364,13 @@ export const pythonLoadConfig = async (content: string, options: { fileName: str
364364
export type PythonCtx = {
365365
__type: 'PythonCtx'
366366
} & {
367-
[key: string]: Function
367+
functions: Record<string, Function>
368+
variables: Record<string, Function>
368369
};
369370

370371
export interface JinjaEngine {
371372
loadTemplate(templateName: string, templateContent: string): void;
372-
renderTemplate(templateName: string, context: unknown, pythonContext: PythonCtx | null): string;
373+
renderTemplate(templateName: string, context: unknown, pythonContext: Record<string, any> | null): string;
373374
}
374375

375376
export class NativeInstance {

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,24 @@ class TemplateException(Exception):
164164

165165
class TemplateContext:
166166
functions: dict[str, Callable]
167+
variables: dict[str, Any]
167168

168169
def __init__(self):
169170
self.functions = {}
171+
self.variables = {}
170172

171173
def add_function(self, name, func):
172174
if not callable(func):
173175
raise TemplateException("function registration must be used with functions, actual: '%s'" % type(func).__name__)
174176

175177
self.functions[name] = func
176178

179+
def add_variable(self, name, val):
180+
if name in self.functions:
181+
raise TemplateException("unable to register variable: name '%s' is already in use for function" % name)
182+
183+
self.variables[name] = val
184+
177185
def add_filter(self, name, func):
178186
if not callable(func):
179187
raise TemplateException("function registration must be used with functions, actual: '%s'" % type(func).__name__)
@@ -194,9 +202,6 @@ def filter(self, func):
194202
self.add_filter(func.__name__, func)
195203
return func
196204

197-
def variable(self, func):
198-
raise TemplateException("variable registration is not supported")
199-
200205
class TemplateFunctionRef:
201206
context: TemplateContext
202207
attribute: str

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::cross::{CLRepr, CLReprObject, PythonRef};
1+
use crate::cross::*;
22
use crate::python::cube_config::CubeConfigPy;
33
use crate::python::python_model::CubePythonModel;
44
use crate::python::runtime::py_runtime_init;
@@ -71,12 +71,12 @@ fn python_load_model(mut cx: FunctionContext) -> JsResult<JsPromise> {
7171

7272
let model_module = PyModule::from_code(py, &model_content, &model_file_name, "")?;
7373
let mut collected_functions = CLReprObject::new();
74+
let mut collected_variables = CLReprObject::new();
7475

7576
if model_module.hasattr("template")? {
76-
let functions = model_module
77-
.getattr("template")?
78-
.getattr("functions")?
79-
.downcast::<PyDict>()?;
77+
let template = model_module.getattr("template")?;
78+
79+
let functions = template.getattr("functions")?.downcast::<PyDict>()?;
8080

8181
for (local_key, local_value) in functions.iter() {
8282
if local_value.is_instance_of::<PyFunction>() {
@@ -87,6 +87,14 @@ fn python_load_model(mut cx: FunctionContext) -> JsResult<JsPromise> {
8787
);
8888
}
8989
}
90+
91+
let variables = template.getattr("variables")?.downcast::<PyDict>()?;
92+
93+
for (local_key, local_value) in variables.iter() {
94+
collected_variables
95+
.insert(local_key.to_string(), CLRepr::from_python_ref(local_value)?);
96+
}
97+
9098
// TODO remove all other ways of defining functions
9199
} else if model_module.hasattr("__execution_context_locals")? {
92100
let execution_context_locals = model_module
@@ -128,7 +136,10 @@ fn python_load_model(mut cx: FunctionContext) -> JsResult<JsPromise> {
128136
}
129137
};
130138

131-
Ok(CubePythonModel::new(collected_functions))
139+
Ok(CubePythonModel::new(
140+
collected_functions,
141+
collected_variables,
142+
))
132143
});
133144

134145
deferred.settle_with(&channel, move |mut cx| match conf_res {

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ use crate::cross::{CLRepr, CLReprObject};
44

55
pub struct CubePythonModel {
66
functions: CLReprObject,
7+
variables: CLReprObject,
78
}
89

910
impl CubePythonModel {
10-
pub fn new(functions: CLReprObject) -> Self {
11-
Self { functions }
11+
pub fn new(functions: CLReprObject, variables: CLReprObject) -> Self {
12+
Self {
13+
functions,
14+
variables,
15+
}
1216
}
1317
}
1418

@@ -17,6 +21,10 @@ impl Finalize for CubePythonModel {}
1721
impl CubePythonModel {
1822
#[allow(clippy::wrong_self_convention)]
1923
pub fn to_object<'a, C: Context<'a>>(self, cx: &mut C) -> JsResult<'a, JsValue> {
20-
CLRepr::Object(self.functions).into_js(cx)
24+
let mut obj = CLReprObject::new();
25+
obj.insert("functions".to_string(), CLRepr::Object(self.functions));
26+
obj.insert("variables".to_string(), CLRepr::Object(self.variables));
27+
28+
CLRepr::Object(obj).into_js(cx)
2129
}
2230
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,14 @@ exports[`Jinja (new api) render template_error.jinja: template_error.jinja 1`] =
250250
251251
-------------------------------------------------------------------------------]
252252
`;
253+
254+
exports[`Jinja (new api) render variables.yml.jinja: variables.yml.jinja 1`] = `
255+
"variables:
256+
var1: \\"test string\\"
257+
var2: true
258+
var3: false
259+
var4: null
260+
var5: {\\"obj_key\\":\\"val\\"}
261+
var6: [1,2,3,4,5,6]
262+
var7: [6,5,4,3,2,1]"
263+
`;

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ function testTemplateBySnapshot(engine: JinjaEngine, templateName: string, ctx:
3838

3939
function testTemplateWithPythonCtxBySnapshot(engine: JinjaEngine, templateName: string, ctx: unknown, utilsFile: string) {
4040
test(`render ${templateName}`, async () => {
41-
const actual = engine.renderTemplate(templateName, ctx, await loadPythonCtxFromUtils(utilsFile));
41+
const pyCtx = await loadPythonCtxFromUtils(utilsFile);
42+
const actual = engine.renderTemplate(templateName, ctx, {
43+
...pyCtx.variables,
44+
...pyCtx.functions,
45+
});
4246

4347
expect(actual).toMatchSnapshot(templateName);
4448
});
@@ -60,7 +64,7 @@ suite('Python model', () => {
6064
it('load jinja-instance.py', async () => {
6165
const pythonModule = await loadPythonCtxFromUtils('jinja-instance.py');
6266

63-
expect(pythonModule).toEqual({
67+
expect(pythonModule.functions).toEqual({
6468
load_data: expect.any(Object),
6569
load_data_sync: expect.any(Object),
6670
arg_bool: expect.any(Object),
@@ -75,14 +79,24 @@ suite('Python model', () => {
7579
new_safe_string: expect.any(Object),
7680
load_class_model: expect.any(Object),
7781
});
82+
83+
expect(pythonModule.variables).toEqual({
84+
var1: 'test string',
85+
var2: true,
86+
var3: false,
87+
var4: undefined,
88+
var5: { obj_key: 'val' },
89+
var6: [1, 2, 3, 4, 5, 6],
90+
var7: [6, 5, 4, 3, 2, 1],
91+
});
7892
});
7993
});
8094

8195
darwinSuite('Scope Python model', () => {
8296
it('load scoped-utils.py', async () => {
8397
const pythonModule = await loadPythonCtxFromUtils('scoped-utils.py');
8498

85-
expect(pythonModule).toEqual({
99+
expect(pythonModule.functions).toEqual({
86100
load_data: expect.any(Object),
87101
load_data_sync: expect.any(Object),
88102
arg_bool: expect.any(Object),
@@ -113,6 +127,7 @@ function createTestSuite(utilsFile: string) {
113127
loadTemplateFile(jinjaEngine, 'data-model.yml.jinja');
114128
loadTemplateFile(jinjaEngine, 'arguments-test.yml.jinja');
115129
loadTemplateFile(jinjaEngine, 'python.yml');
130+
loadTemplateFile(jinjaEngine, 'variables.yml.jinja');
116131

117132
for (let i = 1; i < 9; i++) {
118133
loadTemplateFile(jinjaEngine, `0${i}.yml.jinja`);
@@ -139,6 +154,7 @@ function createTestSuite(utilsFile: string) {
139154
testTemplateWithPythonCtxBySnapshot(jinjaEngine, 'data-model.yml.jinja', {}, utilsFile);
140155
testTemplateWithPythonCtxBySnapshot(jinjaEngine, 'arguments-test.yml.jinja', {}, utilsFile);
141156
testTemplateWithPythonCtxBySnapshot(jinjaEngine, 'python.yml', {}, utilsFile);
157+
testTemplateWithPythonCtxBySnapshot(jinjaEngine, 'variables.yml.jinja', {}, utilsFile);
142158

143159
testLoadBrokenTemplateBySnapshot(jinjaEngine, 'template_error.jinja');
144160

packages/cubejs-backend-native/test/templates/jinja-instance.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
from cube import (TemplateContext, SafeString)
22

33
template = TemplateContext()
4+
template.add_variable('var1', "test string")
5+
template.add_variable('var2', True)
6+
template.add_variable('var3', False)
7+
template.add_variable('var4', None)
8+
template.add_variable('var5', {'obj_key': 'val'})
9+
template.add_variable('var6', [1,2,3,4,5,6])
10+
template.add_variable('var7', [6,5,4,3,2,1])
411

512
@template.function
613
def arg_sum_integers(a, b):
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
variables:
2+
var1: {{ var1 }}
3+
var2: {{ var2 }}
4+
var3: {{ var3 }}
5+
var4: {{ var4 }}
6+
var5: {{ var5 }}
7+
var6: {{ var6 }}
8+
var7: {{ var7 }}

packages/cubejs-schema-compiler/src/compiler/DataSchemaCompiler.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ export class DataSchemaCompiler {
6868

6969
return {
7070
fileName: file.fileName,
71-
exports
71+
exports: {
72+
...exports.variables,
73+
...exports.functions,
74+
}
7275
};
7376
}));
7477

0 commit comments

Comments
 (0)