When overriding a block, the `scoped` modifier does not have to be provided.
+Required Blocks
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Blocks can be marked as required. They must be overridden at some point,
+but not necessarily by the direct child template. Required blocks can
+only contain whitespace or comments, and they cannot be rendered directly.
+
+For example::
+
+ # parent.tmpl
+ body: {% block body required %}{% endblock %}
+
+ # child.tmpl
+ {% extends "parent.tmpl" %}
+
+ # grandchild.tmpl
+ {% extends "child.tmpl" %}
+ {% block body %}Hi from grandchild.{% endblock %}
+
+
+Rendering ``child.tmpl`` will give
+``TemplateRuntimeError``
+
+Rendering ``grandchild.tmpl`` will give
+``Hi from grandchild.``
+
+When combined with ``scoped``, the ``required`` modifier must be placed `after`
+the scoped modifier. Here are some valid examples::
+
+ {% block body scoped %}{% endblock %}
+ {% block body required %}{% endblock %}
+ {% block body scoped required %}{% endblock %}
+
+
Template Objects
~~~~~~~~~~~~~~~~
node = nodes.Block(lineno=next(self.stream).lineno)
node.name = self.stream.expect("name").value
node.scoped = self.stream.skip_if("name:scoped")
+ node.required = self.stream.skip_if("name:required")
# common problem people encounter when switching from django
# to jinja. we do not support hyphens in block names, so let's
)
node.body = self.parse_statements(("name:endblock",), drop_needle=True)
+
+ # enforce that required blocks only contain whitespace or comments
+ # by asserting that the body, if not empty, is just TemplateData nodes
+ # with whitespace data
+ if node.required and not all(
+ isinstance(child, nodes.TemplateData) and child.data.isspace()
+ for body in node.body
+ for child in body.nodes
+ ):
+ self.fail("Required blocks can only contain comments or whitespace")
+
self.stream.skip_if("name:" + node.name)
return node
finally:
if end_tokens is not None:
self._end_token_stack.pop()
-
return body
def parse(self):
from jinja2 import DictLoader
from jinja2 import Environment
from jinja2 import TemplateRuntimeError
+from jinja2 import TemplateSyntaxError
LAYOUTTEMPLATE = """\
|{% block block1 %}block 1 from layout{% endblock %}
rv = env.get_template("index.html").render(the_foo=42).split()
assert rv == ["43", "44", "45"]
+ def test_level1_required(self, env):
+ env = Environment(
+ loader=DictLoader(
+ {
+ "master": "{% block x required %}{# comment #}\n {% endblock %}",
+ "level1": "{% extends 'master' %}{% block x %}[1]{% endblock %}",
+ }
+ )
+ )
+ rv = env.get_template("level1").render()
+ assert rv == "[1]"
+
+ def test_level2_required(self, env):
+ env = Environment(
+ loader=DictLoader(
+ {
+ "master": "{% block x required %}{% endblock %}",
+ "level1": "{% extends 'master' %}{% block x %}[1]{% endblock %}",
+ "level2": "{% extends 'master' %}{% block x %}[2]{% endblock %}",
+ }
+ )
+ )
+ rv1 = env.get_template("level1").render()
+ rv2 = env.get_template("level2").render()
+
+ assert rv1 == "[1]"
+ assert rv2 == "[2]"
+
+ def test_level3_required(self, env):
+ env = Environment(
+ loader=DictLoader(
+ {
+ "master": "{% block x required %}{% endblock %}",
+ "level1": "{% extends 'master' %}",
+ "level2": "{% extends 'level1' %}{% block x %}[2]{% endblock %}",
+ "level3": "{% extends 'level2' %}",
+ }
+ )
+ )
+ t1 = env.get_template("level1")
+ t2 = env.get_template("level2")
+ t3 = env.get_template("level3")
+
+ with pytest.raises(TemplateRuntimeError, match="Required block 'x' not found"):
+ assert t1.render()
+
+ assert t2.render() == "[2]"
+ assert t3.render() == "[2]"
+
+ def test_invalid_required(self, env):
+ env = Environment(
+ loader=DictLoader(
+ {
+ "master": "{% block x required %}data {# #}{% endblock %}",
+ "master1": "{% block x required %}{% block y %}"
+ "{% endblock %} {% endblock %}",
+ "master2": "{% block x required %}{% if true %}"
+ "{% endif %} {% endblock %}",
+ "level1": "{% if master %}{% extends master %}"
+ "{% else %}{% extends 'master' %}{% endif %}"
+ "{%- block x %}CHILD{% endblock %}",
+ }
+ )
+ )
+ t = env.get_template("level1")
+
+ with pytest.raises(
+ TemplateSyntaxError,
+ match="Required blocks can only contain comments or whitespace",
+ ):
+ assert t.render(master="master")
+ assert t.render(master="master2")
+ assert t.render(master="master3")
+
+ def test_required_with_scope(self, env):
+ env = Environment(
+ loader=DictLoader(
+ {
+ "master1": "{% for item in seq %}[{% block item scoped required %}"
+ "{% endblock %}]{% endfor %}",
+ "child1": "{% extends 'master1' %}{% block item %}"
+ "{{ item }}{% endblock %}",
+ "master2": "{% for item in seq %}[{% block item required scoped %}"
+ "{% endblock %}]{% endfor %}",
+ "child2": "{% extends 'master2' %}{% block item %}"
+ "{{ item }}{% endblock %}",
+ }
+ )
+ )
+ t1 = env.get_template("child1")
+ t2 = env.get_template("child2")
+
+ assert t1.render(seq=list(range(3))) == "[0][1][2]"
+
+ # scoped must come before required
+ with pytest.raises(TemplateSyntaxError):
+ t2.render(seq=list(range(3)))
+
+ def test_duplicate_required_or_scoped(self, env):
+ env = Environment(
+ loader=DictLoader(
+ {
+ "master1": "{% for item in seq %}[{% block item "
+ "scoped scoped %}}{{% endblock %}}]{{% endfor %}}",
+ "master2": "{% for item in seq %}[{% block item "
+ "required required %}}{{% endblock %}}]{{% endfor %}}",
+ "child": "{% if master %}{% extends master %}{% else %}"
+ "{% extends 'master1' %}{% endif %}{%- block x %}"
+ "CHILD{% endblock %}",
+ }
+ )
+ )
+ tmpl = env.get_template("child")
+ with pytest.raises(TemplateSyntaxError):
+ tmpl.render(master="master1", seq=list(range(3)))
+ tmpl.render(master="master2", seq=list(range(3)))
+
class TestBugFix:
def test_fixed_macro_scoping_bug(self, env):