test_inheritance.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import pytest
  2. from jinja2 import DictLoader
  3. from jinja2 import Environment
  4. from jinja2 import TemplateRuntimeError
  5. from jinja2 import TemplateSyntaxError
  6. LAYOUTTEMPLATE = """\
  7. |{% block block1 %}block 1 from layout{% endblock %}
  8. |{% block block2 %}block 2 from layout{% endblock %}
  9. |{% block block3 %}
  10. {% block block4 %}nested block 4 from layout{% endblock %}
  11. {% endblock %}|"""
  12. LEVEL1TEMPLATE = """\
  13. {% extends "layout" %}
  14. {% block block1 %}block 1 from level1{% endblock %}"""
  15. LEVEL2TEMPLATE = """\
  16. {% extends "level1" %}
  17. {% block block2 %}{% block block5 %}nested block 5 from level2{%
  18. endblock %}{% endblock %}"""
  19. LEVEL3TEMPLATE = """\
  20. {% extends "level2" %}
  21. {% block block5 %}block 5 from level3{% endblock %}
  22. {% block block4 %}block 4 from level3{% endblock %}
  23. """
  24. LEVEL4TEMPLATE = """\
  25. {% extends "level3" %}
  26. {% block block3 %}block 3 from level4{% endblock %}
  27. """
  28. WORKINGTEMPLATE = """\
  29. {% extends "layout" %}
  30. {% block block1 %}
  31. {% if false %}
  32. {% block block2 %}
  33. this should work
  34. {% endblock %}
  35. {% endif %}
  36. {% endblock %}
  37. """
  38. DOUBLEEXTENDS = """\
  39. {% extends "layout" %}
  40. {% extends "layout" %}
  41. {% block block1 %}
  42. {% if false %}
  43. {% block block2 %}
  44. this should work
  45. {% endblock %}
  46. {% endif %}
  47. {% endblock %}
  48. """
  49. @pytest.fixture
  50. def env():
  51. return Environment(
  52. loader=DictLoader(
  53. {
  54. "layout": LAYOUTTEMPLATE,
  55. "level1": LEVEL1TEMPLATE,
  56. "level2": LEVEL2TEMPLATE,
  57. "level3": LEVEL3TEMPLATE,
  58. "level4": LEVEL4TEMPLATE,
  59. "working": WORKINGTEMPLATE,
  60. "doublee": DOUBLEEXTENDS,
  61. }
  62. ),
  63. trim_blocks=True,
  64. )
  65. class TestInheritance:
  66. def test_layout(self, env):
  67. tmpl = env.get_template("layout")
  68. assert tmpl.render() == (
  69. "|block 1 from layout|block 2 from layout|nested block 4 from layout|"
  70. )
  71. def test_level1(self, env):
  72. tmpl = env.get_template("level1")
  73. assert tmpl.render() == (
  74. "|block 1 from level1|block 2 from layout|nested block 4 from layout|"
  75. )
  76. def test_level2(self, env):
  77. tmpl = env.get_template("level2")
  78. assert tmpl.render() == (
  79. "|block 1 from level1|nested block 5 from "
  80. "level2|nested block 4 from layout|"
  81. )
  82. def test_level3(self, env):
  83. tmpl = env.get_template("level3")
  84. assert tmpl.render() == (
  85. "|block 1 from level1|block 5 from level3|block 4 from level3|"
  86. )
  87. def test_level4(self, env):
  88. tmpl = env.get_template("level4")
  89. assert tmpl.render() == (
  90. "|block 1 from level1|block 5 from level3|block 3 from level4|"
  91. )
  92. def test_super(self, env):
  93. env = Environment(
  94. loader=DictLoader(
  95. {
  96. "a": "{% block intro %}INTRO{% endblock %}|"
  97. "BEFORE|{% block data %}INNER{% endblock %}|AFTER",
  98. "b": '{% extends "a" %}{% block data %}({{ '
  99. "super() }}){% endblock %}",
  100. "c": '{% extends "b" %}{% block intro %}--{{ '
  101. "super() }}--{% endblock %}\n{% block data "
  102. "%}[{{ super() }}]{% endblock %}",
  103. }
  104. )
  105. )
  106. tmpl = env.get_template("c")
  107. assert tmpl.render() == "--INTRO--|BEFORE|[(INNER)]|AFTER"
  108. def test_working(self, env):
  109. env.get_template("working")
  110. def test_reuse_blocks(self, env):
  111. tmpl = env.from_string(
  112. "{{ self.foo() }}|{% block foo %}42{% endblock %}|{{ self.foo() }}"
  113. )
  114. assert tmpl.render() == "42|42|42"
  115. def test_preserve_blocks(self, env):
  116. env = Environment(
  117. loader=DictLoader(
  118. {
  119. "a": "{% if false %}{% block x %}A{% endblock %}"
  120. "{% endif %}{{ self.x() }}",
  121. "b": '{% extends "a" %}{% block x %}B{{ super() }}{% endblock %}',
  122. }
  123. )
  124. )
  125. tmpl = env.get_template("b")
  126. assert tmpl.render() == "BA"
  127. def test_dynamic_inheritance(self, env):
  128. env = Environment(
  129. loader=DictLoader(
  130. {
  131. "default1": "DEFAULT1{% block x %}{% endblock %}",
  132. "default2": "DEFAULT2{% block x %}{% endblock %}",
  133. "child": "{% extends default %}{% block x %}CHILD{% endblock %}",
  134. }
  135. )
  136. )
  137. tmpl = env.get_template("child")
  138. for m in range(1, 3):
  139. assert tmpl.render(default=f"default{m}") == f"DEFAULT{m}CHILD"
  140. def test_multi_inheritance(self, env):
  141. env = Environment(
  142. loader=DictLoader(
  143. {
  144. "default1": "DEFAULT1{% block x %}{% endblock %}",
  145. "default2": "DEFAULT2{% block x %}{% endblock %}",
  146. "child": (
  147. "{% if default %}{% extends default %}{% else %}"
  148. "{% extends 'default1' %}{% endif %}"
  149. "{% block x %}CHILD{% endblock %}"
  150. ),
  151. }
  152. )
  153. )
  154. tmpl = env.get_template("child")
  155. assert tmpl.render(default="default2") == "DEFAULT2CHILD"
  156. assert tmpl.render(default="default1") == "DEFAULT1CHILD"
  157. assert tmpl.render() == "DEFAULT1CHILD"
  158. def test_scoped_block(self, env):
  159. env = Environment(
  160. loader=DictLoader(
  161. {
  162. "default.html": "{% for item in seq %}[{% block item scoped %}"
  163. "{% endblock %}]{% endfor %}"
  164. }
  165. )
  166. )
  167. t = env.from_string(
  168. "{% extends 'default.html' %}{% block item %}{{ item }}{% endblock %}"
  169. )
  170. assert t.render(seq=list(range(5))) == "[0][1][2][3][4]"
  171. def test_super_in_scoped_block(self, env):
  172. env = Environment(
  173. loader=DictLoader(
  174. {
  175. "default.html": "{% for item in seq %}[{% block item scoped %}"
  176. "{{ item }}{% endblock %}]{% endfor %}"
  177. }
  178. )
  179. )
  180. t = env.from_string(
  181. '{% extends "default.html" %}{% block item %}'
  182. "{{ super() }}|{{ item * 2 }}{% endblock %}"
  183. )
  184. assert t.render(seq=list(range(5))) == "[0|0][1|2][2|4][3|6][4|8]"
  185. def test_scoped_block_after_inheritance(self, env):
  186. env = Environment(
  187. loader=DictLoader(
  188. {
  189. "layout.html": """
  190. {% block useless %}{% endblock %}
  191. """,
  192. "index.html": """
  193. {%- extends 'layout.html' %}
  194. {% from 'helpers.html' import foo with context %}
  195. {% block useless %}
  196. {% for x in [1, 2, 3] %}
  197. {% block testing scoped %}
  198. {{ foo(x) }}
  199. {% endblock %}
  200. {% endfor %}
  201. {% endblock %}
  202. """,
  203. "helpers.html": """
  204. {% macro foo(x) %}{{ the_foo + x }}{% endmacro %}
  205. """,
  206. }
  207. )
  208. )
  209. rv = env.get_template("index.html").render(the_foo=42).split()
  210. assert rv == ["43", "44", "45"]
  211. def test_level1_required(self, env):
  212. env = Environment(
  213. loader=DictLoader(
  214. {
  215. "default": "{% block x required %}{# comment #}\n {% endblock %}",
  216. "level1": "{% extends 'default' %}{% block x %}[1]{% endblock %}",
  217. }
  218. )
  219. )
  220. rv = env.get_template("level1").render()
  221. assert rv == "[1]"
  222. def test_level2_required(self, env):
  223. env = Environment(
  224. loader=DictLoader(
  225. {
  226. "default": "{% block x required %}{% endblock %}",
  227. "level1": "{% extends 'default' %}{% block x %}[1]{% endblock %}",
  228. "level2": "{% extends 'default' %}{% block x %}[2]{% endblock %}",
  229. }
  230. )
  231. )
  232. rv1 = env.get_template("level1").render()
  233. rv2 = env.get_template("level2").render()
  234. assert rv1 == "[1]"
  235. assert rv2 == "[2]"
  236. def test_level3_required(self, env):
  237. env = Environment(
  238. loader=DictLoader(
  239. {
  240. "default": "{% block x required %}{% endblock %}",
  241. "level1": "{% extends 'default' %}",
  242. "level2": "{% extends 'level1' %}{% block x %}[2]{% endblock %}",
  243. "level3": "{% extends 'level2' %}",
  244. }
  245. )
  246. )
  247. t1 = env.get_template("level1")
  248. t2 = env.get_template("level2")
  249. t3 = env.get_template("level3")
  250. with pytest.raises(TemplateRuntimeError, match="Required block 'x' not found"):
  251. assert t1.render()
  252. assert t2.render() == "[2]"
  253. assert t3.render() == "[2]"
  254. def test_invalid_required(self, env):
  255. env = Environment(
  256. loader=DictLoader(
  257. {
  258. "empty": "{% block x required %}{% endblock %}",
  259. "blank": "{% block x required %} {# c #}{% endblock %}",
  260. "text": "{% block x required %}data {# c #}{% endblock %}",
  261. "block": "{% block x required %}{% block y %}"
  262. "{% endblock %}{% endblock %}",
  263. "if": "{% block x required %}{% if true %}"
  264. "{% endif %}{% endblock %}",
  265. "top": "{% extends t %}{% block x %}CHILD{% endblock %}",
  266. }
  267. )
  268. )
  269. t = env.get_template("top")
  270. assert t.render(t="empty") == "CHILD"
  271. assert t.render(t="blank") == "CHILD"
  272. required_block_check = pytest.raises(
  273. TemplateSyntaxError,
  274. match="Required blocks can only contain comments or whitespace",
  275. )
  276. with required_block_check:
  277. t.render(t="text")
  278. with required_block_check:
  279. t.render(t="block")
  280. with required_block_check:
  281. t.render(t="if")
  282. def test_required_with_scope(self, env):
  283. env = Environment(
  284. loader=DictLoader(
  285. {
  286. "default1": "{% for item in seq %}[{% block item scoped required %}"
  287. "{% endblock %}]{% endfor %}",
  288. "child1": "{% extends 'default1' %}{% block item %}"
  289. "{{ item }}{% endblock %}",
  290. "default2": "{% for item in seq %}[{% block item required scoped %}"
  291. "{% endblock %}]{% endfor %}",
  292. "child2": "{% extends 'default2' %}{% block item %}"
  293. "{{ item }}{% endblock %}",
  294. }
  295. )
  296. )
  297. t1 = env.get_template("child1")
  298. t2 = env.get_template("child2")
  299. assert t1.render(seq=list(range(3))) == "[0][1][2]"
  300. # scoped must come before required
  301. with pytest.raises(TemplateSyntaxError):
  302. t2.render(seq=list(range(3)))
  303. def test_duplicate_required_or_scoped(self, env):
  304. env = Environment(
  305. loader=DictLoader(
  306. {
  307. "default1": "{% for item in seq %}[{% block item "
  308. "scoped scoped %}}{{% endblock %}}]{{% endfor %}}",
  309. "default2": "{% for item in seq %}[{% block item "
  310. "required required %}}{{% endblock %}}]{{% endfor %}}",
  311. "child": "{% if default %}{% extends default %}{% else %}"
  312. "{% extends 'default1' %}{% endif %}{%- block x %}"
  313. "CHILD{% endblock %}",
  314. }
  315. )
  316. )
  317. tmpl = env.get_template("child")
  318. with pytest.raises(TemplateSyntaxError):
  319. tmpl.render(default="default1", seq=list(range(3)))
  320. with pytest.raises(TemplateSyntaxError):
  321. tmpl.render(default="default2", seq=list(range(3)))
  322. class TestBugFix:
  323. def test_fixed_macro_scoping_bug(self, env):
  324. assert Environment(
  325. loader=DictLoader(
  326. {
  327. "test.html": """\
  328. {% extends 'details.html' %}
  329. {% macro my_macro() %}
  330. my_macro
  331. {% endmacro %}
  332. {% block inner_box %}
  333. {{ my_macro() }}
  334. {% endblock %}
  335. """,
  336. "details.html": """\
  337. {% extends 'standard.html' %}
  338. {% macro my_macro() %}
  339. my_macro
  340. {% endmacro %}
  341. {% block content %}
  342. {% block outer_box %}
  343. outer_box
  344. {% block inner_box %}
  345. inner_box
  346. {% endblock %}
  347. {% endblock %}
  348. {% endblock %}
  349. """,
  350. "standard.html": """
  351. {% block content %} {% endblock %}
  352. """,
  353. }
  354. )
  355. ).get_template("test.html").render().split() == ["outer_box", "my_macro"]
  356. def test_double_extends(self, env):
  357. """Ensures that a template with more than 1 {% extends ... %} usage
  358. raises a ``TemplateError``.
  359. """
  360. with pytest.raises(TemplateRuntimeError, match="extended multiple times"):
  361. env.get_template("doublee").render()