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