Skip to content

Commit 9d1c3f8

Browse files
authored
fix(native): Jinja - pass kwargs correctly into Python (#9276)
In Python, *args and **kwargs allow functions to accept an arbitrary number of arguments. Kwarg should be passed separately when you are calling a function. It's a separate structure. Python example: @template.function def arg_named_arguments(arg1, arg2): return "arg1: " + arg1 + ", arg2: " + arg2 Jinja example: arg_named_arguments1: {{ arg_named_arguments(arg2="2 arg", arg1="1 arg") }} arg_named_arguments2: {{ arg_named_arguments(arg1="1 arg", arg2="2 arg") }} Expected output: arg_named_arguments1: "arg1: 1 arg, arg2: 2 arg" arg_named_arguments2: "arg1: 1 arg, arg2: 2 arg"
1 parent 4a4e82b commit 9d1c3f8

File tree

15 files changed

+110
-39
lines changed

15 files changed

+110
-39
lines changed

packages/cubejs-backend-native/src/cross/clrepr.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,24 @@ use std::collections::hash_map::{IntoIter, Iter, Keys};
1515
use std::collections::HashMap;
1616
use std::sync::Arc;
1717

18+
#[derive(Debug, Clone)]
19+
pub enum CLReprObjectKind {
20+
Object,
21+
KWargs,
22+
}
23+
1824
#[derive(Clone)]
19-
pub struct CLReprObject(pub(crate) HashMap<String, CLRepr>);
25+
pub struct CLReprObject(pub(crate) HashMap<String, CLRepr>, CLReprObjectKind);
2026

2127
impl Default for CLReprObject {
2228
fn default() -> Self {
23-
Self::new()
29+
Self::new(CLReprObjectKind::Object)
2430
}
2531
}
2632

2733
impl CLReprObject {
28-
pub fn new() -> Self {
29-
Self(HashMap::new())
34+
pub fn new(kind: CLReprObjectKind) -> Self {
35+
Self(HashMap::new(), kind)
3036
}
3137

3238
pub fn get(&self, key: &str) -> Option<&CLRepr> {
@@ -110,6 +116,15 @@ pub enum CLRepr {
110116
Null,
111117
}
112118

119+
impl CLRepr {
120+
pub fn is_kwarg(&self) -> bool {
121+
match self {
122+
CLRepr::Object(obj) => matches!(obj.1, CLReprObjectKind::KWargs),
123+
_ => false,
124+
}
125+
}
126+
}
127+
113128
impl std::fmt::Display for CLRepr {
114129
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115130
std::fmt::Display::fmt(&self, f)
@@ -179,7 +194,7 @@ impl CLRepr {
179194

180195
Ok(CLRepr::Array(r))
181196
} else if from.is_a::<JsObject, _>(cx) {
182-
let mut obj = CLReprObject::new();
197+
let mut obj = CLReprObject::new(CLReprObjectKind::Object);
183198

184199
let v = from.downcast_or_throw::<JsObject, _>(cx)?;
185200
let properties = v.get_own_property_names(cx)?;

packages/cubejs-backend-native/src/cross/clrepr_python.rs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::cross::clrepr::CLReprObject;
2-
use crate::cross::{CLRepr, StringType};
2+
use crate::cross::{CLRepr, CLReprObjectKind, StringType};
33
use pyo3::exceptions::{PyNotImplementedError, PyTypeError};
44
use pyo3::types::{
55
PyBool, PyComplex, PyDate, PyDict, PyFloat, PyFrame, PyFunction, PyInt, PyList, PySequence,
@@ -16,15 +16,9 @@ pub enum PythonRef {
1616
PyExternalFunction(Py<PyFunction>),
1717
}
1818

19-
pub trait CLReprPython: Sized {
20-
fn from_python_ref(v: &PyAny) -> Result<Self, PyErr>;
21-
fn into_py_impl(from: CLRepr, py: Python) -> Result<PyObject, PyErr>;
22-
fn into_py(self, py: Python) -> Result<PyObject, PyErr>;
23-
}
24-
25-
impl CLReprPython for CLRepr {
19+
impl CLRepr {
2620
/// Convert python value to CLRepr
27-
fn from_python_ref(v: &PyAny) -> Result<Self, PyErr> {
21+
pub fn from_python_ref(v: &PyAny) -> Result<Self, PyErr> {
2822
if v.is_none() {
2923
return Ok(Self::Null);
3024
}
@@ -47,7 +41,7 @@ impl CLReprPython for CLRepr {
4741
Self::Int(i)
4842
} else if v.get_type().is_subclass_of::<PyDict>()? {
4943
let d = v.downcast::<PyDict>()?;
50-
let mut obj = CLReprObject::new();
44+
let mut obj = CLReprObject::new(CLReprObjectKind::Object);
5145

5246
for (k, v) in d.iter() {
5347
if k.get_type().is_subclass_of::<PyString>()? {
@@ -126,6 +120,16 @@ impl CLReprPython for CLRepr {
126120
})
127121
}
128122

123+
fn into_py_dict_impl(obj: CLReprObject, py: Python) -> Result<&PyDict, PyErr> {
124+
let r = PyDict::new(py);
125+
126+
for (k, v) in obj.into_iter() {
127+
r.set_item(k, Self::into_py_impl(v, py)?)?;
128+
}
129+
130+
Ok(r)
131+
}
132+
129133
fn into_py_impl(from: CLRepr, py: Python) -> Result<PyObject, PyErr> {
130134
Ok(match from {
131135
CLRepr::String(v, _) => PyString::new(py, &v).to_object(py),
@@ -155,11 +159,7 @@ impl CLReprPython for CLRepr {
155159
PyTuple::new(py, elements).to_object(py)
156160
}
157161
CLRepr::Object(obj) => {
158-
let r = PyDict::new(py);
159-
160-
for (k, v) in obj.into_iter() {
161-
r.set_item(k, Self::into_py_impl(v, py)?)?;
162-
}
162+
let r = Self::into_py_dict_impl(obj, py)?;
163163

164164
r.to_object(py)
165165
}
@@ -189,7 +189,19 @@ impl CLReprPython for CLRepr {
189189
})
190190
}
191191

192-
fn into_py(self, py: Python) -> Result<PyObject, PyErr> {
192+
pub fn into_py_dict(self, py: Python) -> Result<&PyDict, PyErr> {
193+
Ok(match self {
194+
CLRepr::Object(obj) => Self::into_py_dict_impl(obj, py)?,
195+
other => {
196+
return Err(PyErr::new::<PyNotImplementedError, _>(format!(
197+
"Unable to convert {:?} into PyDict",
198+
other.kind()
199+
)))
200+
}
201+
})
202+
}
203+
204+
pub fn into_py(self, py: Python) -> Result<PyObject, PyErr> {
193205
Self::into_py_impl(self, py)
194206
}
195207
}

packages/cubejs-backend-native/src/cross/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod clrepr_python;
44
#[cfg(feature = "python")]
55
mod py_in_js;
66

7-
pub use clrepr::{CLRepr, CLReprKind, CLReprObject, StringType};
7+
pub use clrepr::{CLRepr, CLReprKind, CLReprObject, CLReprObjectKind, StringType};
88

99
#[cfg(feature = "python")]
10-
pub use clrepr_python::{CLReprPython, PythonRef};
10+
pub use clrepr_python::PythonRef;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use convert_case::{Case, Casing};
22
use neon::prelude::*;
33
use pyo3::{PyAny, PyResult};
44

5-
use crate::cross::{CLRepr, CLReprObject, CLReprPython};
5+
use crate::cross::{CLRepr, CLReprObject, CLReprObjectKind};
66

77
pub struct CubeConfigPy {
88
properties: CLReprObject,
@@ -11,7 +11,7 @@ pub struct CubeConfigPy {
1111
impl CubeConfigPy {
1212
pub fn new() -> Self {
1313
Self {
14-
properties: CLReprObject::new(),
14+
properties: CLReprObject::new(CLReprObjectKind::Object),
1515
}
1616
}
1717

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ fn python_load_model(mut cx: FunctionContext) -> JsResult<JsPromise> {
7777

7878
let model_module = PyModule::from_code(py, &model_content, &model_file_name, "")?;
7979

80-
let mut collected_functions = CLReprObject::new();
81-
let mut collected_variables = CLReprObject::new();
82-
let mut collected_filters = CLReprObject::new();
80+
let mut collected_functions = CLReprObject::new(CLReprObjectKind::Object);
81+
let mut collected_variables = CLReprObject::new(CLReprObjectKind::Object);
82+
let mut collected_filters = CLReprObject::new(CLReprObjectKind::Object);
8383

8484
if model_module.hasattr("template")? {
8585
let template = model_module.getattr("template")?;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use neon::prelude::*;
22

3-
use crate::cross::{CLRepr, CLReprObject};
3+
use crate::cross::{CLRepr, CLReprObject, CLReprObjectKind};
44

55
pub struct CubePythonModel {
66
functions: CLReprObject,
@@ -23,7 +23,7 @@ impl Finalize for CubePythonModel {}
2323
impl CubePythonModel {
2424
#[allow(clippy::wrong_self_convention)]
2525
pub fn to_object<'a, C: Context<'a>>(self, cx: &mut C) -> JsResult<'a, JsValue> {
26-
let mut obj = CLReprObject::new();
26+
let mut obj = CLReprObject::new(CLReprObjectKind::Object);
2727
obj.insert("functions".to_string(), CLRepr::Object(self.functions));
2828
obj.insert("variables".to_string(), CLRepr::Object(self.variables));
2929
obj.insert("filters".to_string(), CLRepr::Object(self.filters));

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::cross::{CLRepr, CLReprPython};
1+
use crate::cross::CLRepr;
22
use crate::python::neon_py::*;
33
use crate::tokio_runtime_node;
44
use cubesql::CubeError;
@@ -95,14 +95,19 @@ impl PyRuntime {
9595
let (fun, args, callback) = task.split();
9696

9797
let task_result = Python::with_gil(move |py| -> PyResult<PyScheduledFunResult> {
98-
let mut args_tuple = Vec::with_capacity(args.len());
98+
let mut prep_tuple = Vec::with_capacity(args.len());
99+
let mut py_kwargs = None;
99100

100101
for arg in args {
101-
args_tuple.push(arg.into_py(py)?);
102+
if arg.is_kwarg() {
103+
py_kwargs = Some(arg.into_py_dict(py)?);
104+
} else {
105+
prep_tuple.push(arg.into_py(py)?);
106+
}
102107
}
103108

104-
let args = PyTuple::new(py, args_tuple);
105-
let call_res = fun.call1(py, args)?;
109+
let py_args = PyTuple::new(py, prep_tuple);
110+
let call_res = fun.call(py, py_args, py_kwargs)?;
106111

107112
let is_coroutine = unsafe { pyo3::ffi::PyCoro_CheckExact(call_res.as_ptr()) == 1 };
108113
if is_coroutine {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ impl JinjaEngine {
110110
let template_compile_context = CLRepr::from_js_ref(cx.argument::<JsValue>(1)?, &mut cx)?;
111111
let template_python_context = CLRepr::from_js_ref(cx.argument::<JsValue>(2)?, &mut cx)?;
112112

113-
let mut to_jinja_ctx = CLReprObject::new();
113+
let mut to_jinja_ctx = CLReprObject::new(CLReprObjectKind::Object);
114114
to_jinja_ctx.insert("COMPILE_CONTEXT".to_string(), template_compile_context);
115115

116116
if !template_python_context.is_null() {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ pub fn from_minijinja_value(from: &mjv::Value) -> Result<CLRepr, mj::Error> {
6464
Ok(CLRepr::Array(arr))
6565
}
6666
mjv::ValueKind::Map => {
67-
let mut obj = CLReprObject::new();
67+
let mut obj = CLReprObject::new(if from.is_kwargs() {
68+
CLReprObjectKind::KWargs
69+
} else {
70+
CLReprObjectKind::Object
71+
});
6872

6973
for key in from.try_iter()? {
7074
let value = if let Ok(v) = from.get_item(&key) {

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ exports[`Jinja (new api) render arguments-test.yml.jinja: arguments-test.yml.jin
185185
arg_seq_1: [1,2,3,4,5]
186186
arg_seq_2: [5,4,3,2,1]
187187
arg_sum_tuple: 3
188-
arg_sum_map: 20"
188+
arg_sum_map: 20
189+
arg_kwargs1: \\"arg1: first value, arg2: second value, kwarg:(three=3 arg)\\"
190+
arg_kwargs2: \\"arg1: first value, arg2: second value, kwarg:(four=4 arg,three=3 arg)\\"
191+
arg_kwargs3: \\"arg1: first value, arg2: second value, kwarg:(five=5 arg,four=4 arg,three=3 arg)\\"
192+
arg_named_arguments1: \\"arg1: 1 arg, arg2: 2 arg\\"
193+
arg_named_arguments2: \\"arg1: 1 arg, arg2: 2 arg\\""
189194
`;
190195

191196
exports[`Jinja (new api) render data-model.yml.jinja: data-model.yml.jinja 1`] = `
@@ -249,7 +254,7 @@ dump:
249254
exports[`Jinja (new api) render template_error_python.jinja: template_error_python.jinja 1`] = `
250255
[Error: could not render block: Call error: Python error: Exception: Random Exception
251256
Traceback (most recent call last):
252-
File "jinja-instance.py", line 110, in throw_exception
257+
File "jinja-instance.py", line 120, in throw_exception
253258
254259
------------------------- template_error_python.jinja -------------------------
255260
3 | 3

0 commit comments

Comments
 (0)