Added anothe test
[jinja2.git] / jinja2 / testsuite / ext.py
1 # -*- coding: utf-8 -*-
2 """
3     jinja2.testsuite.ext
4     ~~~~~~~~~~~~~~~~~~~~
5
6     Tests for the extensions.
7
8     :copyright: (c) 2010 by the Jinja Team.
9     :license: BSD, see LICENSE for more details.
10 """
11 import re
12 import unittest
13
14 from jinja2.testsuite import JinjaTestCase, filesystem_loader
15
16 from jinja2 import Environment, DictLoader, contextfunction, nodes
17 from jinja2.exceptions import TemplateAssertionError
18 from jinja2.ext import Extension
19 from jinja2.lexer import Token, count_newlines
20 from jinja2.utils import next
21
22 # 2.x / 3.x
23 try:
24     from io import BytesIO
25 except ImportError:
26     from StringIO import StringIO as BytesIO
27
28
29 importable_object = 23
30
31 _gettext_re = re.compile(r'_\((.*?)\)(?s)')
32
33
34 i18n_templates = {
35     'master.html': '<title>{{ page_title|default(_("missing")) }}</title>'
36                    '{% block body %}{% endblock %}',
37     'child.html': '{% extends "master.html" %}{% block body %}'
38                   '{% trans %}watch out{% endtrans %}{% endblock %}',
39     'plural.html': '{% trans user_count %}One user online{% pluralize %}'
40                    '{{ user_count }} users online{% endtrans %}',
41     'stringformat.html': '{{ _("User: %(num)s")|format(num=user_count) }}'
42 }
43
44 newstyle_i18n_templates = {
45     'master.html': '<title>{{ page_title|default(_("missing")) }}</title>'
46                    '{% block body %}{% endblock %}',
47     'child.html': '{% extends "master.html" %}{% block body %}'
48                   '{% trans %}watch out{% endtrans %}{% endblock %}',
49     'plural.html': '{% trans user_count %}One user online{% pluralize %}'
50                    '{{ user_count }} users online{% endtrans %}',
51     'stringformat.html': '{{ _("User: %(num)s", num=user_count) }}',
52     'ngettext.html': '{{ ngettext("%(num)s apple", "%(num)s apples", apples) }}',
53     'ngettext_long.html': '{% trans num=apples %}{{ num }} apple{% pluralize %}'
54                           '{{ num }} apples{% endtrans %}',
55     'transvars1.html': '{% trans %}User: {{ num }}{% endtrans %}',
56     'transvars2.html': '{% trans num=count %}User: {{ num }}{% endtrans %}',
57     'transvars3.html': '{% trans count=num %}User: {{ count }}{% endtrans %}',
58     'novars.html': '{% trans %}%(hello)s{% endtrans %}',
59     'vars.html': '{% trans %}{{ foo }}%(foo)s{% endtrans %}',
60     'explicitvars.html': '{% trans foo="42" %}%(foo)s{% endtrans %}'
61 }
62
63
64 languages = {
65     'de': {
66         'missing':                      u'fehlend',
67         'watch out':                    u'pass auf',
68         'One user online':              u'Ein Benutzer online',
69         '%(user_count)s users online':  u'%(user_count)s Benutzer online',
70         'User: %(num)s':                u'Benutzer: %(num)s',
71         'User: %(count)s':              u'Benutzer: %(count)s',
72         '%(num)s apple':                u'%(num)s Apfel',
73         '%(num)s apples':               u'%(num)s Äpfel'
74     }
75 }
76
77
78 @contextfunction
79 def gettext(context, string):
80     language = context.get('LANGUAGE', 'en')
81     return languages.get(language, {}).get(string, string)
82
83
84 @contextfunction
85 def ngettext(context, s, p, n):
86     language = context.get('LANGUAGE', 'en')
87     if n != 1:
88         return languages.get(language, {}).get(p, p)
89     return languages.get(language, {}).get(s, s)
90
91
92 i18n_env = Environment(
93     loader=DictLoader(i18n_templates),
94     extensions=['jinja2.ext.i18n']
95 )
96 i18n_env.globals.update({
97     '_':            gettext,
98     'gettext':      gettext,
99     'ngettext':     ngettext
100 })
101
102 newstyle_i18n_env = Environment(
103     loader=DictLoader(newstyle_i18n_templates),
104     extensions=['jinja2.ext.i18n']
105 )
106 newstyle_i18n_env.install_gettext_callables(gettext, ngettext, newstyle=True)
107
108 class TestExtension(Extension):
109     tags = set(['test'])
110     ext_attr = 42
111
112     def parse(self, parser):
113         return nodes.Output([self.call_method('_dump', [
114             nodes.EnvironmentAttribute('sandboxed'),
115             self.attr('ext_attr'),
116             nodes.ImportedName(__name__ + '.importable_object'),
117             nodes.ContextReference()
118         ])]).set_lineno(next(parser.stream).lineno)
119
120     def _dump(self, sandboxed, ext_attr, imported_object, context):
121         return '%s|%s|%s|%s' % (
122             sandboxed,
123             ext_attr,
124             imported_object,
125             context.blocks
126         )
127
128
129 class PreprocessorExtension(Extension):
130
131     def preprocess(self, source, name, filename=None):
132         return source.replace('[[TEST]]', '({{ foo }})')
133
134
135 class StreamFilterExtension(Extension):
136
137     def filter_stream(self, stream):
138         for token in stream:
139             if token.type == 'data':
140                 for t in self.interpolate(token):
141                     yield t
142             else:
143                 yield token
144
145     def interpolate(self, token):
146         pos = 0
147         end = len(token.value)
148         lineno = token.lineno
149         while 1:
150             match = _gettext_re.search(token.value, pos)
151             if match is None:
152                 break
153             value = token.value[pos:match.start()]
154             if value:
155                 yield Token(lineno, 'data', value)
156             lineno += count_newlines(token.value)
157             yield Token(lineno, 'variable_begin', None)
158             yield Token(lineno, 'name', 'gettext')
159             yield Token(lineno, 'lparen', None)
160             yield Token(lineno, 'string', match.group(1))
161             yield Token(lineno, 'rparen', None)
162             yield Token(lineno, 'variable_end', None)
163             pos = match.end()
164         if pos < end:
165             yield Token(lineno, 'data', token.value[pos:])
166
167
168 class ExtensionsTestCase(JinjaTestCase):
169
170     def test_extend_late(self):
171         env = Environment()
172         env.add_extension('jinja2.ext.autoescape')
173         t = env.from_string('{% autoescape true %}{{ "<test>" }}{% endautoescape %}')
174         assert t.render() == '&lt;test&gt;'
175
176     def test_loop_controls(self):
177         env = Environment(extensions=['jinja2.ext.loopcontrols'])
178
179         tmpl = env.from_string('''
180             {%- for item in [1, 2, 3, 4] %}
181                 {%- if item % 2 == 0 %}{% continue %}{% endif -%}
182                 {{ item }}
183             {%- endfor %}''')
184         assert tmpl.render() == '13'
185
186         tmpl = env.from_string('''
187             {%- for item in [1, 2, 3, 4] %}
188                 {%- if item > 2 %}{% break %}{% endif -%}
189                 {{ item }}
190             {%- endfor %}''')
191         assert tmpl.render() == '12'
192
193     def test_do(self):
194         env = Environment(extensions=['jinja2.ext.do'])
195         tmpl = env.from_string('''
196             {%- set items = [] %}
197             {%- for char in "foo" %}
198                 {%- do items.append(loop.index0 ~ char) %}
199             {%- endfor %}{{ items|join(', ') }}''')
200         assert tmpl.render() == '0f, 1o, 2o'
201
202     def test_with(self):
203         env = Environment(extensions=['jinja2.ext.with_'])
204         tmpl = env.from_string('''\
205         {% with a=42, b=23 -%}
206             {{ a }} = {{ b }}
207         {% endwith -%}
208             {{ a }} = {{ b }}\
209         ''')
210         assert [x.strip() for x in tmpl.render(a=1, b=2).splitlines()] \
211             == ['42 = 23', '1 = 2']
212
213     def test_extension_nodes(self):
214         env = Environment(extensions=[TestExtension])
215         tmpl = env.from_string('{% test %}')
216         assert tmpl.render() == 'False|42|23|{}'
217
218     def test_identifier(self):
219         assert TestExtension.identifier == __name__ + '.TestExtension'
220
221     def test_rebinding(self):
222         original = Environment(extensions=[TestExtension])
223         overlay = original.overlay()
224         for env in original, overlay:
225             for ext in env.extensions.itervalues():
226                 assert ext.environment is env
227
228     def test_preprocessor_extension(self):
229         env = Environment(extensions=[PreprocessorExtension])
230         tmpl = env.from_string('{[[TEST]]}')
231         assert tmpl.render(foo=42) == '{(42)}'
232
233     def test_streamfilter_extension(self):
234         env = Environment(extensions=[StreamFilterExtension])
235         env.globals['gettext'] = lambda x: x.upper()
236         tmpl = env.from_string('Foo _(bar) Baz')
237         out = tmpl.render()
238         assert out == 'Foo BAR Baz'
239
240     def test_extension_ordering(self):
241         class T1(Extension):
242             priority = 1
243         class T2(Extension):
244             priority = 2
245         env = Environment(extensions=[T1, T2])
246         ext = list(env.iter_extensions())
247         assert ext[0].__class__ is T1
248         assert ext[1].__class__ is T2
249
250
251 class InternationalizationTestCase(JinjaTestCase):
252
253     def test_trans(self):
254         tmpl = i18n_env.get_template('child.html')
255         assert tmpl.render(LANGUAGE='de') == '<title>fehlend</title>pass auf'
256
257     def test_trans_plural(self):
258         tmpl = i18n_env.get_template('plural.html')
259         assert tmpl.render(LANGUAGE='de', user_count=1) == 'Ein Benutzer online'
260         assert tmpl.render(LANGUAGE='de', user_count=2) == '2 Benutzer online'
261
262     def test_complex_plural(self):
263         tmpl = i18n_env.from_string('{% trans foo=42, count=2 %}{{ count }} item{% '
264                                     'pluralize count %}{{ count }} items{% endtrans %}')
265         assert tmpl.render() == '2 items'
266         self.assert_raises(TemplateAssertionError, i18n_env.from_string,
267                            '{% trans foo %}...{% pluralize bar %}...{% endtrans %}')
268
269     def test_trans_stringformatting(self):
270         tmpl = i18n_env.get_template('stringformat.html')
271         assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5'
272
273     def test_extract(self):
274         from jinja2.ext import babel_extract
275         source = BytesIO('''
276         {{ gettext('Hello World') }}
277         {% trans %}Hello World{% endtrans %}
278         {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
279         '''.encode('ascii')) # make python 3 happy
280         assert list(babel_extract(source, ('gettext', 'ngettext', '_'), [], {})) == [
281             (2, 'gettext', u'Hello World', []),
282             (3, 'gettext', u'Hello World', []),
283             (4, 'ngettext', (u'%(users)s user', u'%(users)s users', None), [])
284         ]
285
286     def test_comment_extract(self):
287         from jinja2.ext import babel_extract
288         source = BytesIO('''
289         {# trans first #}
290         {{ gettext('Hello World') }}
291         {% trans %}Hello World{% endtrans %}{# trans second #}
292         {#: third #}
293         {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
294         '''.encode('utf-8')) # make python 3 happy
295         assert list(babel_extract(source, ('gettext', 'ngettext', '_'), ['trans', ':'], {})) == [
296             (3, 'gettext', u'Hello World', ['first']),
297             (4, 'gettext', u'Hello World', ['second']),
298             (6, 'ngettext', (u'%(users)s user', u'%(users)s users', None), ['third'])
299         ]
300
301
302 class NewstyleInternationalizationTestCase(JinjaTestCase):
303
304     def test_trans(self):
305         tmpl = newstyle_i18n_env.get_template('child.html')
306         assert tmpl.render(LANGUAGE='de') == '<title>fehlend</title>pass auf'
307
308     def test_trans_plural(self):
309         tmpl = newstyle_i18n_env.get_template('plural.html')
310         assert tmpl.render(LANGUAGE='de', user_count=1) == 'Ein Benutzer online'
311         assert tmpl.render(LANGUAGE='de', user_count=2) == '2 Benutzer online'
312
313     def test_complex_plural(self):
314         tmpl = newstyle_i18n_env.from_string('{% trans foo=42, count=2 %}{{ count }} item{% '
315                                     'pluralize count %}{{ count }} items{% endtrans %}')
316         assert tmpl.render() == '2 items'
317         self.assert_raises(TemplateAssertionError, i18n_env.from_string,
318                            '{% trans foo %}...{% pluralize bar %}...{% endtrans %}')
319
320     def test_trans_stringformatting(self):
321         tmpl = newstyle_i18n_env.get_template('stringformat.html')
322         assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5'
323
324     def test_newstyle_plural(self):
325         tmpl = newstyle_i18n_env.get_template('ngettext.html')
326         assert tmpl.render(LANGUAGE='de', apples=1) == '1 Apfel'
327         assert tmpl.render(LANGUAGE='de', apples=5) == u'5 Äpfel'
328
329     def test_autoescape_support(self):
330         env = Environment(extensions=['jinja2.ext.autoescape',
331                                       'jinja2.ext.i18n'])
332         env.install_gettext_callables(lambda x: u'<strong>Wert: %(name)s</strong>',
333                                       lambda s, p, n: s, newstyle=True)
334         t = env.from_string('{% autoescape ae %}{{ gettext("foo", name='
335                             '"<test>") }}{% endautoescape %}')
336         assert t.render(ae=True) == '<strong>Wert: &lt;test&gt;</strong>'
337         assert t.render(ae=False) == '<strong>Wert: <test></strong>'
338
339     def test_num_used_twice(self):
340         tmpl = newstyle_i18n_env.get_template('ngettext_long.html')
341         assert tmpl.render(apples=5, LANGUAGE='de') == u'5 Äpfel'
342
343     def test_num_called_num(self):
344         source = newstyle_i18n_env.compile('''
345             {% trans num=3 %}{{ num }} apple{% pluralize
346             %}{{ num }} apples{% endtrans %}
347         ''', raw=True)
348         # quite hacky, but the only way to properly test that.  The idea is
349         # that the generated code does not pass num twice (although that
350         # would work) for better performance.  This only works on the
351         # newstyle gettext of course
352         assert re.search(r"l_ngettext, u?'\%\(num\)s apple', u?'\%\(num\)s "
353                          r"apples', 3", source) is not None
354
355     def test_trans_vars(self):
356         t1 = newstyle_i18n_env.get_template('transvars1.html')
357         t2 = newstyle_i18n_env.get_template('transvars2.html')
358         t3 = newstyle_i18n_env.get_template('transvars3.html')
359         assert t1.render(num=1, LANGUAGE='de') == 'Benutzer: 1'
360         assert t2.render(count=23, LANGUAGE='de') == 'Benutzer: 23'
361         assert t3.render(num=42, LANGUAGE='de') == 'Benutzer: 42'
362
363     def test_novars_vars_escaping(self):
364         t = newstyle_i18n_env.get_template('novars.html')
365         assert t.render() == '%(hello)s'
366         t = newstyle_i18n_env.get_template('vars.html')
367         assert t.render(foo='42') == '42%(foo)s'
368         t = newstyle_i18n_env.get_template('explicitvars.html')
369         assert t.render() == '%(foo)s'
370
371
372 class AutoEscapeTestCase(JinjaTestCase):
373
374     def test_scoped_setting(self):
375         env = Environment(extensions=['jinja2.ext.autoescape'],
376                           autoescape=True)
377         tmpl = env.from_string('''
378             {{ "<HelloWorld>" }}
379             {% autoescape false %}
380                 {{ "<HelloWorld>" }}
381             {% endautoescape %}
382             {{ "<HelloWorld>" }}
383         ''')
384         assert tmpl.render().split() == \
385             [u'&lt;HelloWorld&gt;', u'<HelloWorld>', u'&lt;HelloWorld&gt;']
386
387         env = Environment(extensions=['jinja2.ext.autoescape'],
388                           autoescape=False)
389         tmpl = env.from_string('''
390             {{ "<HelloWorld>" }}
391             {% autoescape true %}
392                 {{ "<HelloWorld>" }}
393             {% endautoescape %}
394             {{ "<HelloWorld>" }}
395         ''')
396         assert tmpl.render().split() == \
397             [u'<HelloWorld>', u'&lt;HelloWorld&gt;', u'<HelloWorld>']
398
399     def test_nonvolatile(self):
400         env = Environment(extensions=['jinja2.ext.autoescape'],
401                           autoescape=True)
402         tmpl = env.from_string('{{ {"foo": "<test>"}|xmlattr|escape }}')
403         assert tmpl.render() == ' foo="&lt;test&gt;"'
404         tmpl = env.from_string('{% autoescape false %}{{ {"foo": "<test>"}'
405                                '|xmlattr|escape }}{% endautoescape %}')
406         assert tmpl.render() == ' foo=&#34;&amp;lt;test&amp;gt;&#34;'
407
408     def test_volatile(self):
409         env = Environment(extensions=['jinja2.ext.autoescape'],
410                           autoescape=True)
411         tmpl = env.from_string('{% autoescape foo %}{{ {"foo": "<test>"}'
412                                '|xmlattr|escape }}{% endautoescape %}')
413         assert tmpl.render(foo=False) == ' foo=&#34;&amp;lt;test&amp;gt;&#34;'
414         assert tmpl.render(foo=True) == ' foo="&lt;test&gt;"'
415
416     def test_scoping(self):
417         env = Environment(extensions=['jinja2.ext.autoescape'])
418         tmpl = env.from_string('{% autoescape true %}{% set x = "<x>" %}{{ x }}'
419                                '{% endautoescape %}{{ x }}{{ "<y>" }}')
420         assert tmpl.render(x=1) == '&lt;x&gt;1<y>'
421
422     def test_volatile_scoping(self):
423         env = Environment(extensions=['jinja2.ext.autoescape'])
424         tmplsource = '''
425         {% autoescape val %}
426             {% macro foo(x) %}
427                 [{{ x }}]
428             {% endmacro %}
429             {{ foo().__class__.__name__ }}
430         {% endautoescape %}
431         {{ '<testing>' }}
432         '''
433         tmpl = env.from_string(tmplsource)
434         assert tmpl.render(val=True).split()[0] == 'Markup'
435         assert tmpl.render(val=False).split()[0] == unicode.__name__
436
437         # looking at the source we should see <testing> there in raw
438         # (and then escaped as well)
439         env = Environment(extensions=['jinja2.ext.autoescape'])
440         pysource = env.compile(tmplsource, raw=True)
441         assert '<testing>\\n' in pysource
442
443         env = Environment(extensions=['jinja2.ext.autoescape'],
444                           autoescape=True)
445         pysource = env.compile(tmplsource, raw=True)
446         assert '&lt;testing&gt;\\n' in pysource
447
448
449 def suite():
450     suite = unittest.TestSuite()
451     suite.addTest(unittest.makeSuite(ExtensionsTestCase))
452     suite.addTest(unittest.makeSuite(InternationalizationTestCase))
453     suite.addTest(unittest.makeSuite(NewstyleInternationalizationTestCase))
454     suite.addTest(unittest.makeSuite(AutoEscapeTestCase))
455     return suite