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 %}'
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'
72 def gettext(context, string):
73 language = context.get('LANGUAGE', 'en')
74 return languages.get(language, {}).get(string, string)
78 def ngettext(context, s, p, n):
79 language = context.get('LANGUAGE', 'en')
81 return languages.get(language, {}).get(p, p)
82 return languages.get(language, {}).get(s, s)
85 i18n_env = Environment(
86 loader=DictLoader(i18n_templates),
87 extensions=['jinja2.ext.i18n']
89 i18n_env.globals.update({
95 newstyle_i18n_env = Environment(
96 loader=DictLoader(newstyle_i18n_templates),
97 extensions=['jinja2.ext.i18n']
99 newstyle_i18n_env.install_gettext_callables(gettext, ngettext, newstyle=True)
101 class TestExtension(Extension):
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)
113 def _dump(self, sandboxed, ext_attr, imported_object, context):
114 return '%s|%s|%s|%s' % (
122 class PreprocessorExtension(Extension):
124 def preprocess(self, source, name, filename=None):
125 return source.replace('[[TEST]]', '({{ foo }})')
128 class StreamFilterExtension(Extension):
130 def filter_stream(self, stream):
132 if token.type == 'data':
133 for t in self.interpolate(token):
138 def interpolate(self, token):
140 end = len(token.value)
141 lineno = token.lineno
143 match = _gettext_re.search(token.value, pos)
146 value = token.value[pos:match.start()]
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)
158 yield Token(lineno, 'data', token.value[pos:])
161 class ExtensionsTestCase(JinjaTestCase):
163 def test_extend_late(self):
165 env.add_extension('jinja2.ext.autoescape')
166 t = env.from_string('{% autoescape true %}{{ "<test>" }}{% endautoescape %}')
167 assert t.render() == '<test>'
169 def test_loop_controls(self):
170 env = Environment(extensions=['jinja2.ext.loopcontrols'])
172 tmpl = env.from_string('''
173 {%- for item in [1, 2, 3, 4] %}
174 {%- if item % 2 == 0 %}{% continue %}{% endif -%}
177 assert tmpl.render() == '13'
179 tmpl = env.from_string('''
180 {%- for item in [1, 2, 3, 4] %}
181 {%- if item > 2 %}{% break %}{% endif -%}
184 assert tmpl.render() == '12'
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'
196 env = Environment(extensions=['jinja2.ext.with_'])
197 tmpl = env.from_string('''\
198 {% with a=42, b=23 -%}
203 assert [x.strip() for x in tmpl.render(a=1, b=2).splitlines()] \
204 == ['42 = 23', '1 = 2']
206 def test_extension_nodes(self):
207 env = Environment(extensions=[TestExtension])
208 tmpl = env.from_string('{% test %}')
209 assert tmpl.render() == 'False|42|23|{}'
211 def test_identifier(self):
212 assert TestExtension.identifier == __name__ + '.TestExtension'
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
221 def test_preprocessor_extension(self):
222 env = Environment(extensions=[PreprocessorExtension])
223 tmpl = env.from_string('{[[TEST]]}')
224 assert tmpl.render(foo=42) == '{(42)}'
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')
231 assert out == 'Foo BAR Baz'
233 def test_extension_ordering(self):
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
244 class InternationalizationTestCase(JinjaTestCase):
246 def test_trans(self):
247 tmpl = i18n_env.get_template('child.html')
248 assert tmpl.render(LANGUAGE='de') == '<title>fehlend</title>pass auf'
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'
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 %}')
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'
266 def test_extract(self):
267 from jinja2.ext import babel_extract
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), [])
279 def test_comment_extract(self):
280 from jinja2.ext import babel_extract
283 {{ gettext('Hello World') }}
284 {% trans %}Hello World{% endtrans %}{# trans second #}
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'])
295 class NewstyleInternationalizationTestCase(JinjaTestCase):
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'
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'
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 %}')
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'
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'
322 def test_autoescape_support(self):
323 env = Environment(extensions=['jinja2.ext.autoescape',
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: <test></strong>'
330 assert t.render(ae=False) == '<strong>Wert: <test></strong>'
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'
336 def test_num_called_num(self):
337 source = newstyle_i18n_env.compile('''
338 {% trans num=3 %}{{ num }} apple{% pluralize
339 %}{{ num }} apples{% endtrans %}
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
349 class AutoEscapeTestCase(JinjaTestCase):
351 def test_scoped_setting(self):
352 env = Environment(extensions=['jinja2.ext.autoescape'],
354 tmpl = env.from_string('''
356 {% autoescape false %}
361 assert tmpl.render().split() == \
362 [u'<HelloWorld>', u'<HelloWorld>', u'<HelloWorld>']
364 env = Environment(extensions=['jinja2.ext.autoescape'],
366 tmpl = env.from_string('''
368 {% autoescape true %}
373 assert tmpl.render().split() == \
374 [u'<HelloWorld>', u'<HelloWorld>', u'<HelloWorld>']
376 def test_nonvolatile(self):
377 env = Environment(extensions=['jinja2.ext.autoescape'],
379 tmpl = env.from_string('{{ {"foo": "<test>"}|xmlattr|escape }}')
380 assert tmpl.render() == ' foo="<test>"'
381 tmpl = env.from_string('{% autoescape false %}{{ {"foo": "<test>"}'
382 '|xmlattr|escape }}{% endautoescape %}')
383 assert tmpl.render() == ' foo="&lt;test&gt;"'
385 def test_volatile(self):
386 env = Environment(extensions=['jinja2.ext.autoescape'],
388 tmpl = env.from_string('{% autoescape foo %}{{ {"foo": "<test>"}'
389 '|xmlattr|escape }}{% endautoescape %}')
390 assert tmpl.render(foo=False) == ' foo="&lt;test&gt;"'
391 assert tmpl.render(foo=True) == ' foo="<test>"'
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) == '<x>1<y>'
399 def test_volatile_scoping(self):
400 env = Environment(extensions=['jinja2.ext.autoescape'])
406 {{ foo().__class__.__name__ }}
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__
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
420 env = Environment(extensions=['jinja2.ext.autoescape'],
422 pysource = env.compile(tmplsource, raw=True)
423 assert '<testing>\\n' in pysource
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))