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