template_test.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import os
  2. import traceback
  3. import unittest
  4. from tornado.escape import utf8, native_str, to_unicode
  5. from tornado.template import Template, DictLoader, ParseError, Loader
  6. from tornado.util import ObjectDict
  7. import typing # noqa: F401
  8. class TemplateTest(unittest.TestCase):
  9. def test_simple(self):
  10. template = Template("Hello {{ name }}!")
  11. self.assertEqual(template.generate(name="Ben"), b"Hello Ben!")
  12. def test_bytes(self):
  13. template = Template("Hello {{ name }}!")
  14. self.assertEqual(template.generate(name=utf8("Ben")), b"Hello Ben!")
  15. def test_expressions(self):
  16. template = Template("2 + 2 = {{ 2 + 2 }}")
  17. self.assertEqual(template.generate(), b"2 + 2 = 4")
  18. def test_comment(self):
  19. template = Template("Hello{# TODO i18n #} {{ name }}!")
  20. self.assertEqual(template.generate(name=utf8("Ben")), b"Hello Ben!")
  21. def test_include(self):
  22. loader = DictLoader(
  23. {
  24. "index.html": '{% include "header.html" %}\nbody text',
  25. "header.html": "header text",
  26. }
  27. )
  28. self.assertEqual(
  29. loader.load("index.html").generate(), b"header text\nbody text"
  30. )
  31. def test_extends(self):
  32. loader = DictLoader(
  33. {
  34. "base.html": """\
  35. <title>{% block title %}default title{% end %}</title>
  36. <body>{% block body %}default body{% end %}</body>
  37. """,
  38. "page.html": """\
  39. {% extends "base.html" %}
  40. {% block title %}page title{% end %}
  41. {% block body %}page body{% end %}
  42. """,
  43. }
  44. )
  45. self.assertEqual(
  46. loader.load("page.html").generate(),
  47. b"<title>page title</title>\n<body>page body</body>\n",
  48. )
  49. def test_relative_load(self):
  50. loader = DictLoader(
  51. {
  52. "a/1.html": "{% include '2.html' %}",
  53. "a/2.html": "{% include '../b/3.html' %}",
  54. "b/3.html": "ok",
  55. }
  56. )
  57. self.assertEqual(loader.load("a/1.html").generate(), b"ok")
  58. def test_escaping(self):
  59. self.assertRaises(ParseError, lambda: Template("{{"))
  60. self.assertRaises(ParseError, lambda: Template("{%"))
  61. self.assertEqual(Template("{{!").generate(), b"{{")
  62. self.assertEqual(Template("{%!").generate(), b"{%")
  63. self.assertEqual(Template("{#!").generate(), b"{#")
  64. self.assertEqual(
  65. Template("{{ 'expr' }} {{!jquery expr}}").generate(),
  66. b"expr {{jquery expr}}",
  67. )
  68. def test_unicode_template(self):
  69. template = Template(utf8("\u00e9"))
  70. self.assertEqual(template.generate(), utf8("\u00e9"))
  71. def test_unicode_literal_expression(self):
  72. # Unicode literals should be usable in templates. Note that this
  73. # test simulates unicode characters appearing directly in the
  74. # template file (with utf8 encoding), i.e. \u escapes would not
  75. # be used in the template file itself.
  76. template = Template(utf8('{{ "\u00e9" }}'))
  77. self.assertEqual(template.generate(), utf8("\u00e9"))
  78. def test_custom_namespace(self):
  79. loader = DictLoader(
  80. {"test.html": "{{ inc(5) }}"}, namespace={"inc": lambda x: x + 1}
  81. )
  82. self.assertEqual(loader.load("test.html").generate(), b"6")
  83. def test_apply(self):
  84. def upper(s):
  85. return s.upper()
  86. template = Template(utf8("{% apply upper %}foo{% end %}"))
  87. self.assertEqual(template.generate(upper=upper), b"FOO")
  88. def test_unicode_apply(self):
  89. def upper(s):
  90. return to_unicode(s).upper()
  91. template = Template(utf8("{% apply upper %}foo \u00e9{% end %}"))
  92. self.assertEqual(template.generate(upper=upper), utf8("FOO \u00c9"))
  93. def test_bytes_apply(self):
  94. def upper(s):
  95. return utf8(to_unicode(s).upper())
  96. template = Template(utf8("{% apply upper %}foo \u00e9{% end %}"))
  97. self.assertEqual(template.generate(upper=upper), utf8("FOO \u00c9"))
  98. def test_if(self):
  99. template = Template(utf8("{% if x > 4 %}yes{% else %}no{% end %}"))
  100. self.assertEqual(template.generate(x=5), b"yes")
  101. self.assertEqual(template.generate(x=3), b"no")
  102. def test_if_empty_body(self):
  103. template = Template(utf8("{% if True %}{% else %}{% end %}"))
  104. self.assertEqual(template.generate(), b"")
  105. def test_try(self):
  106. template = Template(
  107. utf8(
  108. """{% try %}
  109. try{% set y = 1/x %}
  110. {% except %}-except
  111. {% else %}-else
  112. {% finally %}-finally
  113. {% end %}"""
  114. )
  115. )
  116. self.assertEqual(template.generate(x=1), b"\ntry\n-else\n-finally\n")
  117. self.assertEqual(template.generate(x=0), b"\ntry-except\n-finally\n")
  118. def test_comment_directive(self):
  119. template = Template(utf8("{% comment blah blah %}foo"))
  120. self.assertEqual(template.generate(), b"foo")
  121. def test_break_continue(self):
  122. template = Template(
  123. utf8(
  124. """\
  125. {% for i in range(10) %}
  126. {% if i == 2 %}
  127. {% continue %}
  128. {% end %}
  129. {{ i }}
  130. {% if i == 6 %}
  131. {% break %}
  132. {% end %}
  133. {% end %}"""
  134. )
  135. )
  136. result = template.generate()
  137. # remove extraneous whitespace
  138. result = b"".join(result.split())
  139. self.assertEqual(result, b"013456")
  140. def test_break_outside_loop(self):
  141. with self.assertRaises(ParseError, msg="Did not get expected exception"):
  142. Template(utf8("{% break %}"))
  143. def test_break_in_apply(self):
  144. # This test verifies current behavior, although of course it would
  145. # be nice if apply didn't cause seemingly unrelated breakage
  146. with self.assertRaises(ParseError, msg="Did not get expected exception"):
  147. Template(
  148. utf8("{% for i in [] %}{% apply foo %}{% break %}{% end %}{% end %}")
  149. )
  150. @unittest.skip("no testable future imports")
  151. def test_no_inherit_future(self):
  152. # TODO(bdarnell): make a test like this for one of the future
  153. # imports available in python 3. Unfortunately they're harder
  154. # to use in a template than division was.
  155. # This file has from __future__ import division...
  156. self.assertEqual(1 / 2, 0.5)
  157. # ...but the template doesn't
  158. template = Template("{{ 1 / 2 }}")
  159. self.assertEqual(template.generate(), "0")
  160. def test_non_ascii_name(self):
  161. loader = DictLoader({"t\u00e9st.html": "hello"})
  162. self.assertEqual(loader.load("t\u00e9st.html").generate(), b"hello")
  163. class StackTraceTest(unittest.TestCase):
  164. def test_error_line_number_expression(self):
  165. loader = DictLoader(
  166. {
  167. "test.html": """one
  168. two{{1/0}}
  169. three
  170. """
  171. }
  172. )
  173. try:
  174. loader.load("test.html").generate()
  175. self.fail("did not get expected exception")
  176. except ZeroDivisionError:
  177. self.assertTrue("# test.html:2" in traceback.format_exc())
  178. def test_error_line_number_directive(self):
  179. loader = DictLoader(
  180. {
  181. "test.html": """one
  182. two{%if 1/0%}
  183. three{%end%}
  184. """
  185. }
  186. )
  187. try:
  188. loader.load("test.html").generate()
  189. self.fail("did not get expected exception")
  190. except ZeroDivisionError:
  191. self.assertTrue("# test.html:2" in traceback.format_exc())
  192. def test_error_line_number_module(self):
  193. loader = None # type: typing.Optional[DictLoader]
  194. def load_generate(path, **kwargs):
  195. assert loader is not None
  196. return loader.load(path).generate(**kwargs)
  197. loader = DictLoader(
  198. {"base.html": "{% module Template('sub.html') %}", "sub.html": "{{1/0}}"},
  199. namespace={"_tt_modules": ObjectDict(Template=load_generate)},
  200. )
  201. try:
  202. loader.load("base.html").generate()
  203. self.fail("did not get expected exception")
  204. except ZeroDivisionError:
  205. exc_stack = traceback.format_exc()
  206. self.assertTrue("# base.html:1" in exc_stack)
  207. self.assertTrue("# sub.html:1" in exc_stack)
  208. def test_error_line_number_include(self):
  209. loader = DictLoader(
  210. {"base.html": "{% include 'sub.html' %}", "sub.html": "{{1/0}}"}
  211. )
  212. try:
  213. loader.load("base.html").generate()
  214. self.fail("did not get expected exception")
  215. except ZeroDivisionError:
  216. self.assertTrue("# sub.html:1 (via base.html:1)" in traceback.format_exc())
  217. def test_error_line_number_extends_base_error(self):
  218. loader = DictLoader(
  219. {"base.html": "{{1/0}}", "sub.html": "{% extends 'base.html' %}"}
  220. )
  221. try:
  222. loader.load("sub.html").generate()
  223. self.fail("did not get expected exception")
  224. except ZeroDivisionError:
  225. exc_stack = traceback.format_exc()
  226. self.assertIn("# base.html:1", exc_stack)
  227. def test_error_line_number_extends_sub_error(self):
  228. loader = DictLoader(
  229. {
  230. "base.html": "{% block 'block' %}{% end %}",
  231. "sub.html": """
  232. {% extends 'base.html' %}
  233. {% block 'block' %}
  234. {{1/0}}
  235. {% end %}
  236. """,
  237. }
  238. )
  239. try:
  240. loader.load("sub.html").generate()
  241. self.fail("did not get expected exception")
  242. except ZeroDivisionError:
  243. self.assertIn("# sub.html:4 (via base.html:1)", traceback.format_exc())
  244. def test_multi_includes(self):
  245. loader = DictLoader(
  246. {
  247. "a.html": "{% include 'b.html' %}",
  248. "b.html": "{% include 'c.html' %}",
  249. "c.html": "{{1/0}}",
  250. }
  251. )
  252. try:
  253. loader.load("a.html").generate()
  254. self.fail("did not get expected exception")
  255. except ZeroDivisionError:
  256. self.assertIn("# c.html:1 (via b.html:1, a.html:1)", traceback.format_exc())
  257. class ParseErrorDetailTest(unittest.TestCase):
  258. def test_details(self):
  259. loader = DictLoader({"foo.html": "\n\n{{"})
  260. with self.assertRaises(ParseError) as cm:
  261. loader.load("foo.html")
  262. self.assertEqual("Missing end expression }} at foo.html:3", str(cm.exception))
  263. self.assertEqual("foo.html", cm.exception.filename)
  264. self.assertEqual(3, cm.exception.lineno)
  265. def test_custom_parse_error(self):
  266. # Make sure that ParseErrors remain compatible with their
  267. # pre-4.3 signature.
  268. self.assertEqual("asdf at None:0", str(ParseError("asdf")))
  269. class AutoEscapeTest(unittest.TestCase):
  270. def setUp(self):
  271. self.templates = {
  272. "escaped.html": "{% autoescape xhtml_escape %}{{ name }}",
  273. "unescaped.html": "{% autoescape None %}{{ name }}",
  274. "default.html": "{{ name }}",
  275. "include.html": """\
  276. escaped: {% include 'escaped.html' %}
  277. unescaped: {% include 'unescaped.html' %}
  278. default: {% include 'default.html' %}
  279. """,
  280. "escaped_block.html": """\
  281. {% autoescape xhtml_escape %}\
  282. {% block name %}base: {{ name }}{% end %}""",
  283. "unescaped_block.html": """\
  284. {% autoescape None %}\
  285. {% block name %}base: {{ name }}{% end %}""",
  286. # Extend a base template with different autoescape policy,
  287. # with and without overriding the base's blocks
  288. "escaped_extends_unescaped.html": """\
  289. {% autoescape xhtml_escape %}\
  290. {% extends "unescaped_block.html" %}""",
  291. "escaped_overrides_unescaped.html": """\
  292. {% autoescape xhtml_escape %}\
  293. {% extends "unescaped_block.html" %}\
  294. {% block name %}extended: {{ name }}{% end %}""",
  295. "unescaped_extends_escaped.html": """\
  296. {% autoescape None %}\
  297. {% extends "escaped_block.html" %}""",
  298. "unescaped_overrides_escaped.html": """\
  299. {% autoescape None %}\
  300. {% extends "escaped_block.html" %}\
  301. {% block name %}extended: {{ name }}{% end %}""",
  302. "raw_expression.html": """\
  303. {% autoescape xhtml_escape %}\
  304. expr: {{ name }}
  305. raw: {% raw name %}""",
  306. }
  307. def test_default_off(self):
  308. loader = DictLoader(self.templates, autoescape=None)
  309. name = "Bobby <table>s"
  310. self.assertEqual(
  311. loader.load("escaped.html").generate(name=name), b"Bobby &lt;table&gt;s"
  312. )
  313. self.assertEqual(
  314. loader.load("unescaped.html").generate(name=name), b"Bobby <table>s"
  315. )
  316. self.assertEqual(
  317. loader.load("default.html").generate(name=name), b"Bobby <table>s"
  318. )
  319. self.assertEqual(
  320. loader.load("include.html").generate(name=name),
  321. b"escaped: Bobby &lt;table&gt;s\n"
  322. b"unescaped: Bobby <table>s\n"
  323. b"default: Bobby <table>s\n",
  324. )
  325. def test_default_on(self):
  326. loader = DictLoader(self.templates, autoescape="xhtml_escape")
  327. name = "Bobby <table>s"
  328. self.assertEqual(
  329. loader.load("escaped.html").generate(name=name), b"Bobby &lt;table&gt;s"
  330. )
  331. self.assertEqual(
  332. loader.load("unescaped.html").generate(name=name), b"Bobby <table>s"
  333. )
  334. self.assertEqual(
  335. loader.load("default.html").generate(name=name), b"Bobby &lt;table&gt;s"
  336. )
  337. self.assertEqual(
  338. loader.load("include.html").generate(name=name),
  339. b"escaped: Bobby &lt;table&gt;s\n"
  340. b"unescaped: Bobby <table>s\n"
  341. b"default: Bobby &lt;table&gt;s\n",
  342. )
  343. def test_unextended_block(self):
  344. loader = DictLoader(self.templates)
  345. name = "<script>"
  346. self.assertEqual(
  347. loader.load("escaped_block.html").generate(name=name),
  348. b"base: &lt;script&gt;",
  349. )
  350. self.assertEqual(
  351. loader.load("unescaped_block.html").generate(name=name), b"base: <script>"
  352. )
  353. def test_extended_block(self):
  354. loader = DictLoader(self.templates)
  355. def render(name):
  356. return loader.load(name).generate(name="<script>")
  357. self.assertEqual(render("escaped_extends_unescaped.html"), b"base: <script>")
  358. self.assertEqual(
  359. render("escaped_overrides_unescaped.html"), b"extended: &lt;script&gt;"
  360. )
  361. self.assertEqual(
  362. render("unescaped_extends_escaped.html"), b"base: &lt;script&gt;"
  363. )
  364. self.assertEqual(
  365. render("unescaped_overrides_escaped.html"), b"extended: <script>"
  366. )
  367. def test_raw_expression(self):
  368. loader = DictLoader(self.templates)
  369. def render(name):
  370. return loader.load(name).generate(name='<>&"')
  371. self.assertEqual(
  372. render("raw_expression.html"), b"expr: &lt;&gt;&amp;&quot;\n" b'raw: <>&"'
  373. )
  374. def test_custom_escape(self):
  375. loader = DictLoader({"foo.py": "{% autoescape py_escape %}s = {{ name }}\n"})
  376. def py_escape(s):
  377. self.assertEqual(type(s), bytes)
  378. return repr(native_str(s))
  379. def render(template, name):
  380. return loader.load(template).generate(py_escape=py_escape, name=name)
  381. self.assertEqual(render("foo.py", "<html>"), b"s = '<html>'\n")
  382. self.assertEqual(render("foo.py", "';sys.exit()"), b"""s = "';sys.exit()"\n""")
  383. self.assertEqual(
  384. render("foo.py", ["not a string"]), b"""s = "['not a string']"\n"""
  385. )
  386. def test_manual_minimize_whitespace(self):
  387. # Whitespace including newlines is allowed within template tags
  388. # and directives, and this is one way to avoid long lines while
  389. # keeping extra whitespace out of the rendered output.
  390. loader = DictLoader(
  391. {
  392. "foo.txt": """\
  393. {% for i in items
  394. %}{% if i > 0 %}, {% end %}{#
  395. #}{{i
  396. }}{% end
  397. %}"""
  398. }
  399. )
  400. self.assertEqual(
  401. loader.load("foo.txt").generate(items=range(5)), b"0, 1, 2, 3, 4"
  402. )
  403. def test_whitespace_by_filename(self):
  404. # Default whitespace handling depends on the template filename.
  405. loader = DictLoader(
  406. {
  407. "foo.html": " \n\t\n asdf\t ",
  408. "bar.js": " \n\n\n\t qwer ",
  409. "baz.txt": "\t zxcv\n\n",
  410. "include.html": " {% include baz.txt %} \n ",
  411. "include.txt": "\t\t{% include foo.html %} ",
  412. }
  413. )
  414. # HTML and JS files have whitespace compressed by default.
  415. self.assertEqual(loader.load("foo.html").generate(), b"\nasdf ")
  416. self.assertEqual(loader.load("bar.js").generate(), b"\nqwer ")
  417. # TXT files do not.
  418. self.assertEqual(loader.load("baz.txt").generate(), b"\t zxcv\n\n")
  419. # Each file maintains its own status even when included in
  420. # a file of the other type.
  421. self.assertEqual(loader.load("include.html").generate(), b" \t zxcv\n\n\n")
  422. self.assertEqual(loader.load("include.txt").generate(), b"\t\t\nasdf ")
  423. def test_whitespace_by_loader(self):
  424. templates = {"foo.html": "\t\tfoo\n\n", "bar.txt": "\t\tbar\n\n"}
  425. loader = DictLoader(templates, whitespace="all")
  426. self.assertEqual(loader.load("foo.html").generate(), b"\t\tfoo\n\n")
  427. self.assertEqual(loader.load("bar.txt").generate(), b"\t\tbar\n\n")
  428. loader = DictLoader(templates, whitespace="single")
  429. self.assertEqual(loader.load("foo.html").generate(), b" foo\n")
  430. self.assertEqual(loader.load("bar.txt").generate(), b" bar\n")
  431. loader = DictLoader(templates, whitespace="oneline")
  432. self.assertEqual(loader.load("foo.html").generate(), b" foo ")
  433. self.assertEqual(loader.load("bar.txt").generate(), b" bar ")
  434. def test_whitespace_directive(self):
  435. loader = DictLoader(
  436. {
  437. "foo.html": """\
  438. {% whitespace oneline %}
  439. {% for i in range(3) %}
  440. {{ i }}
  441. {% end %}
  442. {% whitespace all %}
  443. pre\tformatted
  444. """
  445. }
  446. )
  447. self.assertEqual(
  448. loader.load("foo.html").generate(), b" 0 1 2 \n pre\tformatted\n"
  449. )
  450. class TemplateLoaderTest(unittest.TestCase):
  451. def setUp(self):
  452. self.loader = Loader(os.path.join(os.path.dirname(__file__), "templates"))
  453. def test_utf8_in_file(self):
  454. tmpl = self.loader.load("utf8.html")
  455. result = tmpl.generate()
  456. self.assertEqual(to_unicode(result).strip(), "H\u00e9llo")