1 # -*- coding: utf-8 -*-
6 Tests for the extensions.
8 :copyright: (c) 2010 by the Jinja Team.
9 :license: BSD, see LICENSE for more details.
14 from jinja2.testsuite import JinjaTestCase, filesystem_loader
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
24 from io import BytesIO
26 from StringIO import StringIO as BytesIO
29 importable_object = 23
31 _gettext_re = re.compile(r'_\((.*?)\)(?s)')
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) }}'
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 %}'
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'
76 def gettext(context, string):
77 language = context.get('LANGUAGE', 'en')
78 return languages.get(language, {}).get(string, string)
82 def ngettext(context, s, p, n):
83 language = context.get('LANGUAGE', 'en')
85 return languages.get(language, {}).get(p, p)
86 return languages.get(language, {}).get(s, s)
89 i18n_env = Environment(
90 loader=DictLoader(i18n_templates),
91 extensions=['jinja2.ext.i18n']
93 i18n_env.globals.update({
99 newstyle_i18n_env = Environment(
100 loader=DictLoader(newstyle_i18n_templates),
101 extensions=['jinja2.ext.i18n']
103 newstyle_i18n_env.install_gettext_callables(gettext, ngettext, newstyle=True)
105 class TestExtension(Extension):
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)
117 def _dump(self, sandboxed, ext_attr, imported_object, context):
118 return '%s|%s|%s|%s' % (
126 class PreprocessorExtension(Extension):
128 def preprocess(self, source, name, filename=None):
129 return source.replace('[[TEST]]', '({{ foo }})')
132 class StreamFilterExtension(Extension):
134 def filter_stream(self, stream):
136 if token.type == 'data':
137 for t in self.interpolate(token):
142 def interpolate(self, token):
144 end = len(token.value)
145 lineno = token.lineno
147 match = _gettext_re.search(token.value, pos)
150 value = token.value[pos:match.start()]
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)
162 yield Token(lineno, 'data', token.value[pos:])
165 class ExtensionsTestCase(JinjaTestCase):
167 def test_extend_late(self):
169 env.add_extension('jinja2.ext.autoescape')
170 t = env.from_string('{% autoescape true %}{{ "<test>" }}{% endautoescape %}')
171 assert t.render() == '<test>'
173 def test_loop_controls(self):
174 env = Environment(extensions=['jinja2.ext.loopcontrols'])
176 tmpl = env.from_string('''
177 {%- for item in [1, 2, 3, 4] %}
178 {%- if item % 2 == 0 %}{% continue %}{% endif -%}
181 assert tmpl.render() == '13'
183 tmpl = env.from_string('''
184 {%- for item in [1, 2, 3, 4] %}
185 {%- if item > 2 %}{% break %}{% endif -%}
188 assert tmpl.render() == '12'
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'
200 env = Environment(extensions=['jinja2.ext.with_'])
201 tmpl = env.from_string('''\
202 {% with a=42, b=23 -%}
207 assert [x.strip() for x in tmpl.render(a=1, b=2).splitlines()] \
208 == ['42 = 23', '1 = 2']
210 def test_extension_nodes(self):
211 env = Environment(extensions=[TestExtension])
212 tmpl = env.from_string('{% test %}')
213 assert tmpl.render() == 'False|42|23|{}'
215 def test_identifier(self):
216 assert TestExtension.identifier == __name__ + '.TestExtension'
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
225 def test_preprocessor_extension(self):
226 env = Environment(extensions=[PreprocessorExtension])
227 tmpl = env.from_string('{[[TEST]]}')
228 assert tmpl.render(foo=42) == '{(42)}'
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')
235 assert out == 'Foo BAR Baz'
237 def test_extension_ordering(self):
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
248 class InternationalizationTestCase(JinjaTestCase):
250 def test_trans(self):
251 tmpl = i18n_env.get_template('child.html')
252 assert tmpl.render(LANGUAGE='de') == '<title>fehlend</title>pass auf'
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'
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 %}')
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'
270 def test_extract(self):
271 from jinja2.ext import babel_extract
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), [])
283 def test_comment_extract(self):
284 from jinja2.ext import babel_extract
287 {{ gettext('Hello World') }}
288 {% trans %}Hello World{% endtrans %}{# trans second #}
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'])
299 class NewstyleInternationalizationTestCase(JinjaTestCase):
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'
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'
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 %}')
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'
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'
326 def test_autoescape_support(self):
327 env = Environment(extensions=['jinja2.ext.autoescape',
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: <test></strong>'
334 assert t.render(ae=False) == '<strong>Wert: <test></strong>'
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'
340 def test_num_called_num(self):
341 source = newstyle_i18n_env.compile('''
342 {% trans num=3 %}{{ num }} apple{% pluralize
343 %}{{ num }} apples{% endtrans %}
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
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'
361 class AutoEscapeTestCase(JinjaTestCase):
363 def test_scoped_setting(self):
364 env = Environment(extensions=['jinja2.ext.autoescape'],
366 tmpl = env.from_string('''
368 {% autoescape false %}
373 assert tmpl.render().split() == \
374 [u'<HelloWorld>', u'<HelloWorld>', u'<HelloWorld>']
376 env = Environment(extensions=['jinja2.ext.autoescape'],
378 tmpl = env.from_string('''
380 {% autoescape true %}
385 assert tmpl.render().split() == \
386 [u'<HelloWorld>', u'<HelloWorld>', u'<HelloWorld>']
388 def test_nonvolatile(self):
389 env = Environment(extensions=['jinja2.ext.autoescape'],
391 tmpl = env.from_string('{{ {"foo": "<test>"}|xmlattr|escape }}')
392 assert tmpl.render() == ' foo="<test>"'
393 tmpl = env.from_string('{% autoescape false %}{{ {"foo": "<test>"}'
394 '|xmlattr|escape }}{% endautoescape %}')
395 assert tmpl.render() == ' foo="&lt;test&gt;"'
397 def test_volatile(self):
398 env = Environment(extensions=['jinja2.ext.autoescape'],
400 tmpl = env.from_string('{% autoescape foo %}{{ {"foo": "<test>"}'
401 '|xmlattr|escape }}{% endautoescape %}')
402 assert tmpl.render(foo=False) == ' foo="&lt;test&gt;"'
403 assert tmpl.render(foo=True) == ' foo="<test>"'
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) == '<x>1<y>'
411 def test_volatile_scoping(self):
412 env = Environment(extensions=['jinja2.ext.autoescape'])
418 {{ foo().__class__.__name__ }}
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__
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
432 env = Environment(extensions=['jinja2.ext.autoescape'],
434 pysource = env.compile(tmplsource, raw=True)
435 assert '<testing>\\n' in pysource
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))