mirror of
https://gitee.com/openharmony/third_party_jinja2
synced 2024-11-23 15:19:46 +00:00
add required attribute to blocks
required blocks must be overridden at some point, although not necessarily by the direct child template
This commit is contained in:
parent
bc22a8b0d3
commit
8da77f9753
@ -19,6 +19,8 @@ Unreleased
|
||||
- Fix UndefinedError incorrectly being thrown on an undefined variable
|
||||
instead of ``Undefined`` being returned on
|
||||
``NativeEnvironment`` on Python 3.10. :issue:`1335`
|
||||
- Add ``required`` attribute to blocks that must be overridden at some
|
||||
point, but not necessarily by the direct child :issue:`1147`
|
||||
|
||||
|
||||
Version 2.11.2
|
||||
|
@ -540,6 +540,40 @@ modifier to a block declaration::
|
||||
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
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -797,6 +797,15 @@ class CodeGenerator(NodeVisitor):
|
||||
else:
|
||||
context = self.get_context_ref()
|
||||
|
||||
if node.required:
|
||||
self.writeline(f"if len(context.blocks[{node.name!r}]) <= 1:", node)
|
||||
self.indent()
|
||||
self.writeline(
|
||||
f'raise TemplateRuntimeError("Required block {node.name!r} not found")',
|
||||
node,
|
||||
)
|
||||
self.outdent()
|
||||
|
||||
if not self.environment.is_async and frame.buffer is None:
|
||||
self.writeline(
|
||||
f"yield from context.blocks[{node.name!r}][0]({context})", node
|
||||
|
@ -340,9 +340,13 @@ class With(Stmt):
|
||||
|
||||
|
||||
class Block(Stmt):
|
||||
"""A node that represents a block."""
|
||||
"""A node that represents a block.
|
||||
|
||||
fields = ("name", "body", "scoped")
|
||||
.. versionchanged:: 3.0.0
|
||||
the `required` field was added.
|
||||
"""
|
||||
|
||||
fields = ("name", "body", "scoped", "required")
|
||||
|
||||
|
||||
class Include(Stmt):
|
||||
|
@ -255,6 +255,7 @@ class Parser:
|
||||
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
|
||||
@ -266,6 +267,17 @@ class Parser:
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@ -924,7 +936,6 @@ class Parser:
|
||||
finally:
|
||||
if end_tokens is not None:
|
||||
self._end_token_stack.pop()
|
||||
|
||||
return body
|
||||
|
||||
def parse(self):
|
||||
|
@ -38,7 +38,7 @@ def test_basics():
|
||||
|
||||
def test_complex():
|
||||
title_block = nodes.Block(
|
||||
"title", [nodes.Output([nodes.TemplateData("Page Title")])], False
|
||||
"title", [nodes.Output([nodes.TemplateData("Page Title")])], False, False
|
||||
)
|
||||
|
||||
render_title_macro = nodes.Macro(
|
||||
@ -137,6 +137,7 @@ def test_complex():
|
||||
nodes.Output([nodes.TemplateData("\n </ul>\n")]),
|
||||
],
|
||||
False,
|
||||
False,
|
||||
)
|
||||
|
||||
tmpl = nodes.Template(
|
||||
|
@ -3,6 +3,7 @@ import pytest
|
||||
from jinja2 import DictLoader
|
||||
from jinja2 import Environment
|
||||
from jinja2 import TemplateRuntimeError
|
||||
from jinja2 import TemplateSyntaxError
|
||||
|
||||
LAYOUTTEMPLATE = """\
|
||||
|{% block block1 %}block 1 from layout{% endblock %}
|
||||
@ -230,6 +231,123 @@ class TestInheritance:
|
||||
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):
|
||||
|
Loading…
Reference in New Issue
Block a user