diff options
author | David Lord <davidism@gmail.com> | 2021-04-04 10:20:49 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-04 10:20:49 -0700 |
commit | af5d80e999944b6204e99a07940ae13382909efd (patch) | |
tree | 32bc5e70d82b669a3459c0ef9f40dac2a78005ea | |
parent | 1c863d0447efb7e68e2593e7de3fec87670521cb (diff) | |
parent | ebf0e2dfdad237a938436cd18fc49405c5703dc6 (diff) | |
download | jinja2-af5d80e999944b6204e99a07940ae13382909efd.tar.gz |
Merge pull request #1248 from MLH-Fellowship/test-for-filter
allow optional use of a filter based on its existence
-rw-r--r-- | CHANGES.rst | 2 | ||||
-rw-r--r-- | src/jinja2/compiler.py | 28 | ||||
-rw-r--r-- | src/jinja2/runtime.py | 1 | ||||
-rw-r--r-- | tests/test_filters.py | 48 |
4 files changed, 77 insertions, 2 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 26c928b..4516d42 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -39,6 +39,8 @@ Unreleased - Update the template globals when calling ``Environment.get_template(globals=...)`` even if the template was already loaded. :issue:`295` +- Do not raise an error for undefined filters in unexecuted + if-statements and conditional expressions. :issue:`842` Version 2.11.3 diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index 50f98f1..e3bde16 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -136,6 +136,11 @@ class Frame: self.loop_frame = False self.block_frame = False + # track whether the frame is being used in an if-statement or conditional + # expression as it determines which errors should be raised during runtime + # or compile time. + self.soft_frame = False + def copy(self): """Create a copy of the current one.""" rv = object.__new__(self.__class__) @@ -154,10 +159,12 @@ class Frame: standalone thing as it shares the resources with the frame it was created of, but it's not a rootlevel frame any longer. - This is only used to implement if-statements. + This is only used to implement if-statements and conditional + expressions. """ rv = self.copy() rv.rootlevel = False + rv.soft_frame = True return rv __copy__ = copy @@ -442,7 +449,22 @@ class CodeGenerator(NodeVisitor): for name in getattr(visitor, dependency): if name not in mapping: mapping[name] = self.temporary_identifier() + # add check during runtime that dependencies used inside of executed + # blocks are defined, as this step may be skipped during compile time + self.writeline("try:") + self.indent() self.writeline(f"{mapping[name]} = environment.{dependency}[{name!r}]") + self.outdent() + self.writeline("except KeyError:") + self.indent() + self.writeline("@internalcode") + self.writeline(f"def {mapping[name]}(*unused):") + self.indent() + self.writeline( + f'raise TemplateRuntimeError("no filter named {name!r} found")' + ) + self.outdent() + self.outdent() def enter_frame(self, frame): undefs = [] @@ -1641,7 +1663,7 @@ class CodeGenerator(NodeVisitor): self.write("await auto_await(") self.write(self.filters[node.name] + "(") func = self.environment.filters.get(node.name) - if func is None: + if func is None and not frame.soft_frame: self.fail(f"no filter named {node.name!r}", node.lineno) if getattr(func, "contextfilter", False) is True: self.write("context, ") @@ -1679,6 +1701,8 @@ class CodeGenerator(NodeVisitor): @optimizeconst def visit_CondExpr(self, node, frame): + frame = frame.soft() + def write_expr2(): if node.expr2 is not None: return self.visit(node.expr2, frame) diff --git a/src/jinja2/runtime.py b/src/jinja2/runtime.py index 4a3c36e..9468a37 100644 --- a/src/jinja2/runtime.py +++ b/src/jinja2/runtime.py @@ -36,6 +36,7 @@ exported = [ "TemplateNotFound", "Namespace", "Undefined", + "internalcode", ] diff --git a/tests/test_filters.py b/tests/test_filters.py index efc82bc..44be6ad 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -6,7 +6,9 @@ import pytest from jinja2 import Environment from jinja2 import Markup from jinja2 import StrictUndefined +from jinja2 import TemplateRuntimeError from jinja2 import UndefinedError +from jinja2.exceptions import TemplateAssertionError class Magic: @@ -774,3 +776,49 @@ class TestFilter: t = env.from_string("{{ s|wordwrap(20) }}") result = t.render(s="Hello!\nThis is Jinja saying something.") assert result == "Hello!\nThis is Jinja saying\nsomething." + + def test_filter_undefined(self, env): + with pytest.raises(TemplateAssertionError, match="no filter named 'f'"): + env.from_string("{{ var|f }}") + + def test_filter_undefined_in_if(self, env): + t = env.from_string("{%- if x is defined -%}{{ x|f }}{%- else -%}x{% endif %}") + assert t.render() == "x" + with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"): + t.render(x=42) + + def test_filter_undefined_in_elif(self, env): + t = env.from_string( + "{%- if x is defined -%}{{ x }}{%- elif y is defined -%}" + "{{ y|f }}{%- else -%}foo{%- endif -%}" + ) + assert t.render() == "foo" + with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"): + t.render(y=42) + + def test_filter_undefined_in_else(self, env): + t = env.from_string( + "{%- if x is not defined -%}foo{%- else -%}{{ x|f }}{%- endif -%}" + ) + assert t.render() == "foo" + with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"): + t.render(x=42) + + def test_filter_undefined_in_nested_if(self, env): + t = env.from_string( + "{%- if x is not defined -%}foo{%- else -%}{%- if y " + "is defined -%}{{ y|f }}{%- endif -%}{{ x }}{%- endif -%}" + ) + assert t.render() == "foo" + assert t.render(x=42) == "42" + with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"): + t.render(x=24, y=42) + + def test_filter_undefined_in_condexpr(self, env): + t1 = env.from_string("{{ x|f if x is defined else 'foo' }}") + t2 = env.from_string("{{ 'foo' if x is not defined else x|f }}") + assert t1.render() == t2.render() == "foo" + + with pytest.raises(TemplateRuntimeError, match="no filter named 'f'"): + t1.render(x=42) + t2.render(x=42) |