Run `./2to3.py -w jinja2`
[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
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 io 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':                      'fehlend',
67         'watch out':                    'pass auf',
68         'One user online':              'Ein Benutzer online',
69         '%(user_count)s users online':  '%(user_count)s Benutzer online',
70         'User: %(num)s':                'Benutzer: %(num)s',
71         'User: %(count)s':              'Benutzer: %(count)s',
72         '%(num)s apple':                '%(num)s Apfel',
73         '%(num)s apples':               '%(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.values():
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', 'Hello World', []),
282             (3, 'gettext', 'Hello World', []),
283             (4, 'ngettext', ('%(users)s user', '%(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', 'Hello World', ['first']),
297             (4, 'gettext', 'Hello World', ['second']),
298             (6, 'ngettext', ('%(users)s user', '%(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) == '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: '<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') == '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             ['&lt;HelloWorld&gt;', '<HelloWorld>', '&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             ['<HelloWorld>', '&lt;HelloWorld&gt;', '<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] == str.__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