Skip to content

Commit

Permalink
Implement mul behavior from Jinja2 (#519)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko authored Jun 8, 2024
1 parent bffacbf commit 799e81a
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 6 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to MiniJinja are documented here.

## 2.0.2

- Implemented sequence (+ some iterator) and string repeating with the `*`
operator to match Jinja2 behavior. #519

## 2.0.1

- Fixed an issue that caused custom delimiters to not work in the Python
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
DOC_FEATURES=loader,json,urlencode,custom_syntax,fuel
TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax
TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax,deserialization

.PHONY: all
all: test
Expand Down
88 changes: 86 additions & 2 deletions minijinja/src/value/ops.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::error::{Error, ErrorKind};
use crate::value::{ObjectRepr, Value, ValueKind, ValueRepr};
use crate::value::{DynObject, ObjectRepr, Value, ValueKind, ValueRepr};

const MIN_I128_AS_POS_U128: u128 = 170141183460469231731687303715884105728;

Expand Down Expand Up @@ -204,9 +204,93 @@ pub fn add(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
}

math_binop!(sub, checked_sub, -);
math_binop!(mul, checked_mul, *);
math_binop!(rem, checked_rem_euclid, %);

pub fn mul(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
if let Some((s, n)) = lhs
.as_str()
.map(|s| (s, rhs))
.or_else(|| rhs.as_str().map(|s| (s, lhs)))
{
return Ok(Value::from(s.repeat(ok!(n.as_usize().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
"strings can only be multiplied with integers",
)
})))));
} else if let Some((seq, n)) = lhs
.as_object()
.map(|s| (s, rhs))
.or_else(|| rhs.as_object().map(|s| (s, lhs)))
.filter(|x| matches!(x.0.repr(), ObjectRepr::Iterable | ObjectRepr::Seq))
{
return repeat_iterable(n, seq);
}

match coerce(lhs, rhs) {
Some(CoerceResult::I128(a, b)) => match a.checked_mul(b) {
Some(val) => Ok(int_as_value(val)),
None => Err(failed_op(stringify!(*), lhs, rhs)),
},
Some(CoerceResult::F64(a, b)) => Ok((a * b).into()),
_ => Err(impossible_op(stringify!(*), lhs, rhs)),
}
}

fn repeat_iterable(n: &Value, seq: &DynObject) -> Result<Value, Error> {
struct LenIterWrap<I: Send + Sync>(usize, I);

impl<I: Iterator<Item = Value> + Send + Sync> Iterator for LenIterWrap<I> {
type Item = Value;

#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
self.1.next()
}

#[inline(always)]
fn size_hint(&self) -> (usize, Option<usize>) {
(self.0, Some(self.0))
}
}

let n = ok!(n.as_usize().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
"sequences and iterables can only be multiplied with integers",
)
}));

let len = ok!(seq.enumerator_len().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
"cannot repeat unsized iterables",
)
}));

// This is not optimal. We only query the enumerator for the length once
// but we support repeated iteration. We could both lie about our length
// here and we could actually deal with an object that changes how much
// data it returns. This is not really permissible so we won't try to
// improve on this here.
Ok(Value::make_object_iterable(seq.clone(), move |seq| {
Box::new(LenIterWrap(
len * n,
(0..n).flat_map(move |_| {
seq.try_iter().unwrap_or_else(|| {
Box::new(
std::iter::repeat(Value::from(Error::new(
ErrorKind::InvalidOperation,
"iterable did not iterate against expectations",
)))
.take(len),
)
})
}),
))
}))
}

pub fn div(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
fn do_it(lhs: &Value, rhs: &Value) -> Option<Value> {
let a = some!(as_f64(lhs));
Expand Down
4 changes: 4 additions & 0 deletions minijinja/tests/inputs/err_repeat_iterator_with_object.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
}
---
{{ [1, 2, 3] * {} }}
4 changes: 4 additions & 0 deletions minijinja/tests/inputs/err_repeat_iterator_with_string.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
}
---
{{ [1, 2, 3] * "3" }}
4 changes: 4 additions & 0 deletions minijinja/tests/inputs/err_repeat_true_iterator.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
}
---
{{ one_shot_iterator * 3 }}
9 changes: 9 additions & 0 deletions minijinja/tests/inputs/mul.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
}
---
{{ range(3) * 3 }}
{{ "foo" * 3 }}
{{ [1, 2] * 2 }}
{{ [1, 2, 3, 4][:2] * 2 }}
{{ 2 * [1, 2] }}
{{ 2 * "foo" }}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ State {
depth: 0,
},
"f": minijinja::functions::builtins::range,
"one_shot_iterator": <iterator>,
"upper": 1,
},
env: Environment {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: minijinja/tests/test_templates.rs
description: "{{ [1, 2, 3] * {} }}"
info: {}
input_file: minijinja/tests/inputs/err_repeat_iterator_with_object.txt
---
!!!ERROR!!!

Error {
kind: InvalidOperation,
detail: "sequences and iterables can only be multiplied with integers",
name: "err_repeat_iterator_with_object.txt",
line: 1,
}

invalid operation: sequences and iterables can only be multiplied with integers (in err_repeat_iterator_with_object.txt:1)
--------------------- err_repeat_iterator_with_object.txt ---------------------
1 > {{ [1, 2, 3] * {} }}
i ^^^^^^^^^^^^^^ invalid operation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
No referenced variables
-------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: minijinja/tests/test_templates.rs
description: "{{ [1, 2, 3] * \"3\" }}"
info: {}
input_file: minijinja/tests/inputs/err_repeat_iterator_with_string.txt
---
!!!ERROR!!!

Error {
kind: InvalidOperation,
detail: "strings can only be multiplied with integers",
name: "err_repeat_iterator_with_string.txt",
line: 1,
}

invalid operation: strings can only be multiplied with integers (in err_repeat_iterator_with_string.txt:1)
--------------------- err_repeat_iterator_with_string.txt ---------------------
1 > {{ [1, 2, 3] * "3" }}
i ^^^^^^^^^^^^^^^ invalid operation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
No referenced variables
-------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
source: minijinja/tests/test_templates.rs
description: "{{ one_shot_iterator * 3 }}"
info: {}
input_file: minijinja/tests/inputs/err_repeat_true_iterator.txt
---
!!!ERROR!!!

Error {
kind: InvalidOperation,
detail: "cannot repeat unsized iterables",
name: "err_repeat_true_iterator.txt",
line: 1,
}

invalid operation: cannot repeat unsized iterables (in err_repeat_true_iterator.txt:1)
------------------------ err_repeat_true_iterator.txt -------------------------
1 > {{ one_shot_iterator * 3 }}
i ^^^^^^^^^^^^^^^^^^^^^ invalid operation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Referenced variables: {
one_shot_iterator: <iterator>,
}
-------------------------------------------------------------------------------
12 changes: 12 additions & 0 deletions minijinja/tests/snapshots/test_templates__vm@mul.txt.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: minijinja/tests/test_templates.rs
description: "{{ range(3) * 3 }}\n{{ \"foo\" * 3 }}\n{{ [1, 2] * 2 }}\n{{ [1, 2, 3, 4][:2] * 2 }}\n{{ 2 * [1, 2] }}\n{{ 2 * \"foo\" }}"
info: {}
input_file: minijinja/tests/inputs/mul.txt
---
[0, 1, 2, 0, 1, 2, 0, 1, 2]
foofoofoo
[1, 2, 1, 2]
[1, 2, 1, 2]
[1, 2, 1, 2]
foofoo
11 changes: 8 additions & 3 deletions minijinja/tests/test_templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
feature = "multi_template",
feature = "macros",
feature = "builtins",
feature = "adjacent_loop_items"
feature = "adjacent_loop_items",
feature = "deserialization"
))]
use std::collections::BTreeMap;
use std::fmt::Write;
Expand Down Expand Up @@ -35,7 +36,7 @@ fn test_vm() {
let contents = std::fs::read_to_string(path).unwrap();
let mut iter = contents.splitn(2, "\n---\n");
let mut env = Environment::new();
let ctx: serde_json::Value = serde_json::from_str(iter.next().unwrap()).unwrap();
let ctx: Value = serde_json::from_str(iter.next().unwrap()).unwrap();

for (path, source) in &refs {
let ref_filename = path.file_name().unwrap().to_str().unwrap();
Expand All @@ -50,7 +51,11 @@ fn test_vm() {
} else {
let template = env.get_template(filename).unwrap();

match template.render(&ctx) {
let actual_context = context! {
one_shot_iterator => Value::make_one_shot_iterator(0..3),
..ctx.clone()
};
match template.render(&actual_context) {
Ok(mut rendered) => {
rendered.push('\n');
rendered
Expand Down

0 comments on commit 799e81a

Please sign in to comment.