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
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 %}',
58 'novars.html': '{% trans %}%(hello)s{% endtrans %}',
59 'vars.html': '{% trans %}{{ foo }}%(foo)s{% endtrans %}',
60 'explicitvars.html': '{% trans foo="42" %}%(foo)s{% endtrans %}'
66 'missing': u
'fehlend',
67 'watch out': u
'pass auf',
68 'One user online': u
'Ein Benutzer online',
69 '%(user_count)s users online': u
'%(user_count)s Benutzer online',
70 'User: %(num)s': u
'Benutzer: %(num)s',
71 'User: %(count)s': u
'Benutzer: %(count)s',
72 '%(num)s apple': u
'%(num)s Apfel',
73 '%(num)s apples': u
'%(num)s Äpfel'
79 def gettext(context
, string
):
80 language
= context
.get('LANGUAGE', 'en')
81 return languages
.get(language
, {}).get(string
, string
)
85 def ngettext(context
, s
, p
, n
):
86 language
= context
.get('LANGUAGE', 'en')
88 return languages
.get(language
, {}).get(p
, p
)
89 return languages
.get(language
, {}).get(s
, s
)
92 i18n_env
= Environment(
93 loader
=DictLoader(i18n_templates
),
94 extensions
=['jinja2.ext.i18n']
96 i18n_env
.globals.update({
102 newstyle_i18n_env
= Environment(
103 loader
=DictLoader(newstyle_i18n_templates
),
104 extensions
=['jinja2.ext.i18n']
106 newstyle_i18n_env
.install_gettext_callables(gettext
, ngettext
, newstyle
=True)
108 class TestExtension(Extension
):
112 def parse(self
, parser
):
113 return nodes
.Output([self
.call_method('_dump', [
114 nodes
.EnvironmentAttribute('sandboxed'),
115 self
.attr('ext_attr'),
116 nodes
.ImportedName(__name__
+ '.importable_object'),
117 nodes
.ContextReference()
118 ])]).set_lineno(next(parser
.stream
).lineno
)
120 def _dump(self
, sandboxed
, ext_attr
, imported_object
, context
):
121 return '%s|%s|%s|%s' % (
129 class PreprocessorExtension(Extension
):
131 def preprocess(self
, source
, name
, filename
=None):
132 return source
.replace('[[TEST]]', '({{ foo }})')
135 class StreamFilterExtension(Extension
):
137 def filter_stream(self
, stream
):
139 if token
.type == 'data':
140 for t
in self
.interpolate(token
):
145 def interpolate(self
, token
):
147 end
= len(token
.value
)
148 lineno
= token
.lineno
150 match
= _gettext_re
.search(token
.value
, pos
)
153 value
= token
.value
[pos
:match
.start()]
155 yield Token(lineno
, 'data', value
)
156 lineno
+= count_newlines(token
.value
)
157 yield Token(lineno
, 'variable_begin', None)
158 yield Token(lineno
, 'name', 'gettext')
159 yield Token(lineno
, 'lparen', None)
160 yield Token(lineno
, 'string', match
.group(1))
161 yield Token(lineno
, 'rparen', None)
162 yield Token(lineno
, 'variable_end', None)
165 yield Token(lineno
, 'data', token
.value
[pos
:])
168 class ExtensionsTestCase(JinjaTestCase
):
170 def test_extend_late(self
):
172 env
.add_extension('jinja2.ext.autoescape')
173 t
= env
.from_string('{% autoescape true %}{{ "<test>" }}{% endautoescape %}')
174 assert t
.render() == '<test>'
176 def test_loop_controls(self
):
177 env
= Environment(extensions
=['jinja2.ext.loopcontrols'])
179 tmpl
= env
.from_string('''
180 {%- for item in [1, 2, 3, 4] %}
181 {%- if item % 2 == 0 %}{% continue %}{% endif -%}
184 assert tmpl
.render() == '13'
186 tmpl
= env
.from_string('''
187 {%- for item in [1, 2, 3, 4] %}
188 {%- if item > 2 %}{% break %}{% endif -%}
191 assert tmpl
.render() == '12'
194 env
= Environment(extensions
=['jinja2.ext.do'])
195 tmpl
= env
.from_string('''
196 {%- set items = [] %}
197 {%- for char in "foo" %}
198 {%- do items.append(loop.index0 ~ char) %}
199 {%- endfor %}{{ items|join(', ') }}''')
200 assert tmpl
.render() == '0f, 1o, 2o'
203 env
= Environment(extensions
=['jinja2.ext.with_'])
204 tmpl
= env
.from_string('''\
205 {% with a=42, b=23 -%}
210 assert [x
.strip() for x
in tmpl
.render(a
=1, b
=2).splitlines()] \
211 == ['42 = 23', '1 = 2']
213 def test_extension_nodes(self
):
214 env
= Environment(extensions
=[TestExtension
])
215 tmpl
= env
.from_string('{% test %}')
216 assert tmpl
.render() == 'False|42|23|{}'
218 def test_identifier(self
):
219 assert TestExtension
.identifier
== __name__
+ '.TestExtension'
221 def test_rebinding(self
):
222 original
= Environment(extensions
=[TestExtension
])
223 overlay
= original
.overlay()
224 for env
in original
, overlay
:
225 for ext
in env
.extensions
.itervalues():
226 assert ext
.environment
is env
228 def test_preprocessor_extension(self
):
229 env
= Environment(extensions
=[PreprocessorExtension
])
230 tmpl
= env
.from_string('{[[TEST]]}')
231 assert tmpl
.render(foo
=42) == '{(42)}'
233 def test_streamfilter_extension(self
):
234 env
= Environment(extensions
=[StreamFilterExtension
])
235 env
.globals['gettext'] = lambda x
: x
.upper()
236 tmpl
= env
.from_string('Foo _(bar) Baz')
238 assert out
== 'Foo BAR Baz'
240 def test_extension_ordering(self
):
245 env
= Environment(extensions
=[T1
, T2
])
246 ext
= list(env
.iter_extensions())
247 assert ext
[0].__class
__ is T1
248 assert ext
[1].__class
__ is T2
251 class InternationalizationTestCase(JinjaTestCase
):
253 def test_trans(self
):
254 tmpl
= i18n_env
.get_template('child.html')
255 assert tmpl
.render(LANGUAGE
='de') == '<title>fehlend</title>pass auf'
257 def test_trans_plural(self
):
258 tmpl
= i18n_env
.get_template('plural.html')
259 assert tmpl
.render(LANGUAGE
='de', user_count
=1) == 'Ein Benutzer online'
260 assert tmpl
.render(LANGUAGE
='de', user_count
=2) == '2 Benutzer online'
262 def test_complex_plural(self
):
263 tmpl
= i18n_env
.from_string('{% trans foo=42, count=2 %}{{ count }} item{% '
264 'pluralize count %}{{ count }} items{% endtrans %}')
265 assert tmpl
.render() == '2 items'
266 self
.assert_raises(TemplateAssertionError
, i18n_env
.from_string
,
267 '{% trans foo %}...{% pluralize bar %}...{% endtrans %}')
269 def test_trans_stringformatting(self
):
270 tmpl
= i18n_env
.get_template('stringformat.html')
271 assert tmpl
.render(LANGUAGE
='de', user_count
=5) == 'Benutzer: 5'
273 def test_extract(self
):
274 from jinja2
.ext
import babel_extract
276 {{ gettext('Hello World') }}
277 {% trans %}Hello World{% endtrans %}
278 {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
279 '''.encode('ascii')) # make python 3 happy
280 assert list(babel_extract(source
, ('gettext', 'ngettext', '_'), [], {})) == [
281 (2, 'gettext', u
'Hello World', []),
282 (3, 'gettext', u
'Hello World', []),
283 (4, 'ngettext', (u
'%(users)s user', u
'%(users)s users', None), [])
286 def test_comment_extract(self
):
287 from jinja2
.ext
import babel_extract
290 {{ gettext('Hello World') }}
291 {% trans %}Hello World{% endtrans %}{# trans second #}
293 {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %}
294 '''.encode('utf-8')) # make python 3 happy
295 assert list(babel_extract(source
, ('gettext', 'ngettext', '_'), ['trans', ':'], {})) == [
296 (3, 'gettext', u
'Hello World', ['first']),
297 (4, 'gettext', u
'Hello World', ['second']),
298 (6, 'ngettext', (u
'%(users)s user', u
'%(users)s users', None), ['third'])
302 class NewstyleInternationalizationTestCase(JinjaTestCase
):
304 def test_trans(self
):
305 tmpl
= newstyle_i18n_env
.get_template('child.html')
306 assert tmpl
.render(LANGUAGE
='de') == '<title>fehlend</title>pass auf'
308 def test_trans_plural(self
):
309 tmpl
= newstyle_i18n_env
.get_template('plural.html')
310 assert tmpl
.render(LANGUAGE
='de', user_count
=1) == 'Ein Benutzer online'
311 assert tmpl
.render(LANGUAGE
='de', user_count
=2) == '2 Benutzer online'
313 def test_complex_plural(self
):
314 tmpl
= newstyle_i18n_env
.from_string('{% trans foo=42, count=2 %}{{ count }} item{% '
315 'pluralize count %}{{ count }} items{% endtrans %}')
316 assert tmpl
.render() == '2 items'
317 self
.assert_raises(TemplateAssertionError
, i18n_env
.from_string
,
318 '{% trans foo %}...{% pluralize bar %}...{% endtrans %}')
320 def test_trans_stringformatting(self
):
321 tmpl
= newstyle_i18n_env
.get_template('stringformat.html')
322 assert tmpl
.render(LANGUAGE
='de', user_count
=5) == 'Benutzer: 5'
324 def test_newstyle_plural(self
):
325 tmpl
= newstyle_i18n_env
.get_template('ngettext.html')
326 assert tmpl
.render(LANGUAGE
='de', apples
=1) == '1 Apfel'
327 assert tmpl
.render(LANGUAGE
='de', apples
=5) == u
'5 Äpfel'
329 def test_autoescape_support(self
):
330 env
= Environment(extensions
=['jinja2.ext.autoescape',
332 env
.install_gettext_callables(lambda x
: u
'<strong>Wert: %(name)s</strong>',
333 lambda s
, p
, n
: s
, newstyle
=True)
334 t
= env
.from_string('{% autoescape ae %}{{ gettext("foo", name='
335 '"<test>") }}{% endautoescape %}')
336 assert t
.render(ae
=True) == '<strong>Wert: <test></strong>'
337 assert t
.render(ae
=False) == '<strong>Wert: <test></strong>'
339 def test_num_used_twice(self
):
340 tmpl
= newstyle_i18n_env
.get_template('ngettext_long.html')
341 assert tmpl
.render(apples
=5, LANGUAGE
='de') == u
'5 Äpfel'
343 def test_num_called_num(self
):
344 source
= newstyle_i18n_env
.compile('''
345 {% trans num=3 %}{{ num }} apple{% pluralize
346 %}{{ num }} apples{% endtrans %}
348 # quite hacky, but the only way to properly test that. The idea is
349 # that the generated code does not pass num twice (although that
350 # would work) for better performance. This only works on the
351 # newstyle gettext of course
352 assert re
.search(r
"l_ngettext, u?'\%\(num\)s apple', u?'\%\(num\)s "
353 r
"apples', 3", source
) is not None
355 def test_trans_vars(self
):
356 t1
= newstyle_i18n_env
.get_template('transvars1.html')
357 t2
= newstyle_i18n_env
.get_template('transvars2.html')
358 t3
= newstyle_i18n_env
.get_template('transvars3.html')
359 assert t1
.render(num
=1, LANGUAGE
='de') == 'Benutzer: 1'
360 assert t2
.render(count
=23, LANGUAGE
='de') == 'Benutzer: 23'
361 assert t3
.render(num
=42, LANGUAGE
='de') == 'Benutzer: 42'
363 def test_novars_vars_escaping(self
):
364 t
= newstyle_i18n_env
.get_template('novars.html')
365 assert t
.render() == '%(hello)s'
366 t
= newstyle_i18n_env
.get_template('vars.html')
367 assert t
.render(foo
='42') == '42%(foo)s'
368 t
= newstyle_i18n_env
.get_template('explicitvars.html')
369 assert t
.render() == '%(foo)s'
372 class AutoEscapeTestCase(JinjaTestCase
):
374 def test_scoped_setting(self
):
375 env
= Environment(extensions
=['jinja2.ext.autoescape'],
377 tmpl
= env
.from_string('''
379 {% autoescape false %}
384 assert tmpl
.render().split() == \
385 [u
'<HelloWorld>', u
'<HelloWorld>', u
'<HelloWorld>']
387 env
= Environment(extensions
=['jinja2.ext.autoescape'],
389 tmpl
= env
.from_string('''
391 {% autoescape true %}
396 assert tmpl
.render().split() == \
397 [u
'<HelloWorld>', u
'<HelloWorld>', u
'<HelloWorld>']
399 def test_nonvolatile(self
):
400 env
= Environment(extensions
=['jinja2.ext.autoescape'],
402 tmpl
= env
.from_string('{{ {"foo": "<test>"}|xmlattr|escape }}')
403 assert tmpl
.render() == ' foo="<test>"'
404 tmpl
= env
.from_string('{% autoescape false %}{{ {"foo": "<test>"}'
405 '|xmlattr|escape }}{% endautoescape %}')
406 assert tmpl
.render() == ' foo="&lt;test&gt;"'
408 def test_volatile(self
):
409 env
= Environment(extensions
=['jinja2.ext.autoescape'],
411 tmpl
= env
.from_string('{% autoescape foo %}{{ {"foo": "<test>"}'
412 '|xmlattr|escape }}{% endautoescape %}')
413 assert tmpl
.render(foo
=False) == ' foo="&lt;test&gt;"'
414 assert tmpl
.render(foo
=True) == ' foo="<test>"'
416 def test_scoping(self
):
417 env
= Environment(extensions
=['jinja2.ext.autoescape'])
418 tmpl
= env
.from_string('{% autoescape true %}{% set x = "<x>" %}{{ x }}'
419 '{% endautoescape %}{{ x }}{{ "<y>" }}')
420 assert tmpl
.render(x
=1) == '<x>1<y>'
422 def test_volatile_scoping(self
):
423 env
= Environment(extensions
=['jinja2.ext.autoescape'])
429 {{ foo().__class__.__name__ }}
433 tmpl
= env
.from_string(tmplsource
)
434 assert tmpl
.render(val
=True).split()[0] == 'Markup'
435 assert tmpl
.render(val
=False).split()[0] == unicode.__name
__
437 # looking at the source we should see <testing> there in raw
438 # (and then escaped as well)
439 env
= Environment(extensions
=['jinja2.ext.autoescape'])
440 pysource
= env
.compile(tmplsource
, raw
=True)
441 assert '<testing>\\n' in pysource
443 env
= Environment(extensions
=['jinja2.ext.autoescape'],
445 pysource
= env
.compile(tmplsource
, raw
=True)
446 assert '<testing>\\n' in pysource
450 suite
= unittest
.TestSuite()
451 suite
.addTest(unittest
.makeSuite(ExtensionsTestCase
))
452 suite
.addTest(unittest
.makeSuite(InternationalizationTestCase
))
453 suite
.addTest(unittest
.makeSuite(NewstyleInternationalizationTestCase
))
454 suite
.addTest(unittest
.makeSuite(AutoEscapeTestCase
))