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