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