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 &lt;table&gt;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 &lt;table&gt;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 &lt;table&gt;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 &lt;table&gt;s")
362
363        self.assertEqual(loader.load("include.html").generate(name=name),
364                         b"escaped: Bobby &lt;table&gt;s\n"
365                         b"unescaped: Bobby <table>s\n"
366                         b"default: Bobby &lt;table&gt;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: &lt;script&gt;")
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: &lt;script&gt;")
385
386        self.assertEqual(render("unescaped_extends_escaped.html"),
387                         b"base: &lt;script&gt;")
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: &lt;&gt;&amp;&quot;\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