template_test.py 18 KB

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