diff --git a/CHANGES.rst b/CHANGES.rst index fede06ed3..7eca79c7d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,10 @@ .. currentmodule:: jinja2 +Unreleased + +- Breaking change. The evaluation order of an exponent expression changed. Now it is more consistent with how it's going in pure python. :issue:`1720` :issue:`1722` + + Version 3.1.2 ------------- diff --git a/docs/templates.rst b/docs/templates.rst index 2471cea39..d7f1fcf62 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -1369,7 +1369,10 @@ but exists for completeness' sake. The following operators are supported: ``**`` Raise the left operand to the power of the right operand. - ``{{ 2**3 }}`` would return ``8``. + ``{{ 2**3 }}`` would return ``8`` + + .. versionchanged:: 3.2 + From 3.2, this operator has the same evaluation order as in Python. ``{{ 2 ** 3 ** 2 }}`` will be evaluated as ``2 ** (3 ** 2)``. Unlike Python, chained pow is evaluated left to right. ``{{ 3**3**3 }}`` is evaluated as ``(3**3)**3`` in Jinja, but would diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index 3458095f5..5a8b8baf3 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -68,8 +68,9 @@ def visitor(self: "CodeGenerator", node: nodes.BinExpr, frame: Frame) -> None: self.write(", ") self.visit(node.right, frame) else: - self.write("(") + self.write("((") self.visit(node.left, frame) + self.write(")") self.write(f" {op} ") self.visit(node.right, frame) diff --git a/src/jinja2/parser.py b/src/jinja2/parser.py index cefce2dfa..dbec1be2e 100644 --- a/src/jinja2/parser.py +++ b/src/jinja2/parser.py @@ -604,7 +604,7 @@ def parse_concat(self) -> nodes.Expr: def parse_math2(self) -> nodes.Expr: lineno = self.stream.current.lineno - left = self.parse_pow() + left = self.parse_unary() while self.stream.current.type in ("mul", "div", "floordiv", "mod"): cls = _math_nodes[self.stream.current.type] next(self.stream) @@ -615,7 +615,7 @@ def parse_math2(self) -> nodes.Expr: def parse_pow(self) -> nodes.Expr: lineno = self.stream.current.lineno - left = self.parse_unary() + left = self.parse_primary() while self.stream.current.type == "pow": next(self.stream) right = self.parse_unary() @@ -635,7 +635,7 @@ def parse_unary(self, with_filter: bool = True) -> nodes.Expr: next(self.stream) node = nodes.Pos(self.parse_unary(False), lineno=lineno) else: - node = self.parse_primary() + node = self.parse_pow() node = self.parse_postfix(node) if with_filter: node = self.parse_filter_expr(node) diff --git a/tests/test_lexnparse.py b/tests/test_lexnparse.py index c02adad5a..bdee2e864 100644 --- a/tests/test_lexnparse.py +++ b/tests/test_lexnparse.py @@ -575,6 +575,26 @@ def test_parse_unary(self, env): tmpl = env.from_string('{{ -foo["bar"]|abs }}') assert tmpl.render(foo={"bar": 42}) == "42" + def test_negative_pow(self, env): + tmpl = env.from_string("{{ - 1 ** 2 }}") + assert tmpl.render() == "-1" + + def test_pow_associative_from_left_to_right(self, env): + tmpl = env.from_string("{{ 2 ** 3 ** 2 }}") + assert tmpl.render() == "512" + + def test_pow_negative_base(self, env): + tmpl = env.from_string("{{ (- 1) ** 2 }}") + assert tmpl.render() == "1" + + def test_pow_negative_base_variable_exponent(self, env): + tmpl = env.from_string("{{ (- 1) ** x }}", {"x": 2}) + assert tmpl.render() == "1" + + def test_negative_pow_variable_exponent(self, env): + tmpl = env.from_string("{{ - 1 ** x }}", {"x": 2}) + assert tmpl.render() == "-1" + class TestLstripBlocks: def test_lstrip(self, env):