summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Lord <davidism@gmail.com>2021-04-04 10:20:49 -0700
committerGitHub <noreply@github.com>2021-04-04 10:20:49 -0700
commitaf5d80e999944b6204e99a07940ae13382909efd (patch)
tree32bc5e70d82b669a3459c0ef9f40dac2a78005ea
parent1c863d0447efb7e68e2593e7de3fec87670521cb (diff)
parentebf0e2dfdad237a938436cd18fc49405c5703dc6 (diff)
downloadjinja2-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.rst2
-rw-r--r--src/jinja2/compiler.py28
-rw-r--r--src/jinja2/runtime.py1
-rw-r--r--tests/test_filters.py48
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)