- additional macro arguments now end up in `varargs`.
-- implemented `{% call %}` - unsure if this makes it into the final release.
+- implemented the `{% call %}` block. `call` and `endcall` can still be used
+ as identifiers until Jinja 1.3
- it's not possible to stream templates.
test:
@(cd tests; py.test $(TESTS))
+test-coverage:
+ @(cd tests; py.test -C $(TESTS))
+
documentation:
@(cd docs; ./generate.py)
def handle_file(filename, fp, dst, preproc):
now = datetime.now()
title = os.path.basename(filename)[:-4]
- content = fp.read()
+ content = fp.read().decode('utf-8')
suffix = not preproc and '.html' or ''
parts = generate_documentation(content, (lambda x: './%s%s' % (x, suffix)))
result = file(os.path.join(dst, title + '.html'), 'w')
<ul>
<% for item in seq %>
<li><%= item %></li>
- <% endfor %>
+ <% endfor %>
</ul>
Block / Variable Tag Unification
---------------------------------
+================================
If variable end and start tags are `None` or look the same as block tags and
you're running Jinja 1.1 or later the parser will switch into the
{else}
Something is {something}.
{endif}
+
+This feature however can cause strange looking templates because there is no
+visible difference between blocks and variables.
`and`, `block`, `cycle`, `elif`, `else`, `endblock`, `endfilter`,
`endfor`, `endif`, `endmacro`, `endraw`, `endtrans`, `extends`, `filter`,
`for`, `if`, `in`, `include`, `is`, `macro`, `not`, `or`, `pluralize`,
- `print`, `raw`, `recursive`, `set`, `trans`
+ `print`, `raw`, `recursive`, `set`, `trans`, `call`*, `endcall`*,
+
+keywords marked with `*` can be used until Jinja 1.3 as identifiers because
+they were introduced after the Jinja 1.0 release and can cause backwards
+compatiblity problems otherwise. You should not use them any more.
If you want to use such a name you have to prefix or suffix it or use
alternative names:
{{ macro_('foo') }}
{% endfor %}
-If future Jinja releases add new keywords those will be "light" keywords which
-means that they won't raise an error for several releases but yield warnings
-on the application side. But it's very unlikely that new keywords will be
-added.
-
Bleeding Edge
=============
class AutoEnvironment(Environment):
- def autorender(self):
- return self.render(sys._getframe(1).f_locals)
+ def autorender(self, template):
+ tmpl = self.get_template(template)
+ return tmpl.render(sys._getframe(1).f_locals)
You can use it now like this:
.. sourcecode:: python
- foo_tmpl = env.get_template('foo.html')
-
def foo():
seq = range(10)
foo = "blub"
- return foo_tmpl.autorender()
+ return env.autorender('foo.html')
+
+In the template you can now access the local variables `seq` and `foo`.
template. Of course you can modify the context too. For more informations about
the context object have a look at the `context object`_ documentation.
+The new ``{% call %}`` tag that exists with Jinja 1.1 onwards can not only
+be used with Jinja macros but also with Python functions. If a template
+designers uses the ``{% call %}`` tag to call a function provided by the
+application Jinja will call this function with a keyword argument called
+`caller` which points to a `function`. If you call this function (optionally
+with keyword arguments that appear in the context) you get a string back
+that was the content of that block. This should explain this:
+
+.. sourcecode:: python
+
+ def make_dialog(title, caller=None):
+ body = ''
+ if caller:
+ body = caller(title=title)
+ return '<div class="dialog"><h2>%s</h2>%s</div>' % (title, body)
+
+This can be used like this in the template now:
+
+.. sourcecode:: html+jinja
+
+ {% call make_dialog('Dialog Title') %}
+ This is the body of the dialog entitled "{{ title }}".
+ {% endcall %}
+
Deferred Values
===============
:license: BSD, see LICENSE for more details.
"""
from jinja.datastructure import Deferred, Undefined
+try:
+ from collections import deque
+except ImportError:
+ class deque(list):
+ """
+ Minimal subclass of list that provides the deque
+ interface used by the native `BaseContext`.
+ """
+
+ def appendleft(self, item):
+ list.insert(self, 0, item)
+
+ def popleft(self):
+ return list.pop(self, 0)
class BaseContext(object):
def __init__(self, undefined_singleton, globals, initial):
self._undefined_singleton = undefined_singleton
self.current = current = {}
- self.stack = [globals, initial, current]
- self._push = self.stack.append
- self._pop = self.stack.pop
+ self._stack = deque([current, initial, globals])
self.globals = globals
self.initial = initial
+ self._push = self._stack.appendleft
+ self._pop = self._stack.popleft
+
+ def stack(self):
+ return list(self._stack)[::-1]
+ stack = property(stack)
+
def pop(self):
"""
Pop the last layer from the stack and return it.
"""
rv = self._pop()
- self.current = self.stack[-1]
+ self.current = self._stack[0]
return rv
def push(self, data=None):
"""
- Push one layer to the stack. Layer must be a dict or omitted.
+ Push one layer to the stack and return it. Layer must be
+ a dict or omitted.
"""
data = data or {}
self._push(data)
- self.current = self.stack[-1]
+ self.current = self._stack[0]
return data
def __getitem__(self, name):
such as ``'::cycle1'``. Resolve deferreds.
"""
if not name.startswith('::'):
- # because the stack is usually quite small we better
- # use [::-1] which is faster than reversed() in such
- # a situation.
- for d in self.stack[::-1]:
+ for d in self._stack:
if name in d:
rv = d[name]
if rv.__class__ is Deferred:
"""
Check if the context contains a given variable.
"""
- for layer in self.stack:
+ for layer in self._stack:
if name in layer:
return True
return False
def __init__(self, gen):
self._gen = gen
- self._next = gen._next
+ self._next = gen.next
self.buffered = False
def disable_buffering(self):
'endfilter', 'endfor', 'endif', 'endmacro', 'endraw',
'endtrans', 'extends', 'filter', 'for', 'if', 'in',
'include', 'is', 'macro', 'not', 'or', 'pluralize', 'raw',
- 'recursive', 'set', 'trans', 'print', 'call', 'endcall'])
+ 'recursive', 'set', 'trans', 'print'])
class Failure(object):
else:
self.available = True
+ def __getattr__(self, name):
+ """
+ Not found attributes are redirected to the loader
+ """
+ return getattr(self.loader, name)
+
def get_source(self, name, parent=None):
"""Retrieve the sourcecode of a template."""
# just ascii chars are allowed as template names
to ``package_name + '/' + package_path``.
*New in Jinja 1.1*
=================== =================================================
+
+ Important note: If you're using an application that is inside of an
+ egg never set `auto_reload` to `True`. The egg resource manager will
+ automatically export files to the file system and touch them so that
+ you not only end up with additional temporary files but also an automatic
+ reload each time you load a template.
"""
def __init__(self, package_name, package_path, use_memcache=False,
self.package_path = package_path
if cache_salt is None:
cache_salt = package_name + '/' + package_path
- # if we have an loader we probably retrieved it from an egg
- # file. In that case don't use the auto_reload!
- if auto_reload and getattr(__import__(package_name, '', '', ['']),
- '__loader__', None) is not None:
- auto_reload = False
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
cache_folder, auto_reload, cache_salt)
end_of_if = StateTest.expect_name('endif')
end_of_filter = StateTest.expect_name('endfilter')
end_of_macro = StateTest.expect_name('endmacro')
-end_of_call = StateTest.expect_name('endcall')
+end_of_call = StateTest.expect_name('endcall_')
end_of_block_tag = StateTest.expect_name('endblock')
end_of_trans = StateTest.expect_name('endtrans')
"""
def __init__(self, environment, source, filename=None):
+ #XXX: with Jinja 1.3 call becomes a keyword. Add it also
+ # to the lexer.py file.
self.environment = environment
if isinstance(source, str):
source = source.decode(environment.template_charset, 'ignore')
'filter': self.handle_filter_directive,
'print': self.handle_print_directive,
'macro': self.handle_macro_directive,
- 'call': self.handle_call_directive,
+ 'call_': self.handle_call_directive,
'block': self.handle_block_directive,
'extends': self.handle_extends_directive,
'include': self.handle_include_directive,
# handle requirements code
if requirements:
- requirement_lines = [
- 'def bootstrap(context):',
- ' ctx_push = context.push',
- ' ctx_pop = context.pop'
- ]
+ requirement_lines = ['def bootstrap(context):']
has_requirements = False
for n in requirements:
requirement_lines.append(self.handle_node(n))
if data:
block_lines.extend([
'def %s(context):' % func_name,
- ' ctx_push = context.push',
- ' ctx_pop = context.pop',
self.indent(self.nodeinfo(item, True)),
data,
' if 0: yield None\n'
'# Name for disabled debugging\n'
'__name__ = %r\n\n'
'def generate(context):\n'
- ' assert environment is context.environment\n'
- ' ctx_push = context.push\n'
- ' ctx_pop = context.pop' % (
+ ' assert environment is context.environment' % (
'\n'.join([
'%s = environment.%s' % (item, item) for item in
['get_attribute', 'perform_test', 'apply_filters',
"""
Handle data around nodes.
"""
- # if we have a ascii only string we go with the
- # bytestring. otherwise we go with the unicode object
- try:
- data = str(node.text)
- except UnicodeError:
- data = node.text
return self.indent(self.nodeinfo(node)) + '\n' +\
- self.indent('yield %r' % data)
+ self.indent('yield %r' % node.text)
def handle_dynamic_text(self, node):
"""
buf = []
write = lambda x: buf.append(self.indent(x))
write(self.nodeinfo(node))
- write('ctx_push()')
+ write('context.push()')
# recursive loops
if node.recursive:
write('yield item')
self.indention -= 1
- write('ctx_pop()')
+ write('context.pop()')
return '\n'.join(buf)
def handle_if_condition(self, node):
if varargs_init:
arg_items.append(varargs_init)
- write('ctx_push({%s})' % ',\n '.join([
+ write('context.push({%s})' % ',\n '.join([
idx and self.indent(item) or item for idx, item
in enumerate(arg_items)
]))
data = self.handle_node(node.body)
if data:
buf.append(data)
- write('ctx_pop()')
+ write('context.pop()')
write('if 0: yield None')
self.indention -= 1
buf.append(self.indent('context[%r] = buffereater(macro)' %
write('def call(**kwargs):')
self.indention += 1
- write('ctx_push(kwargs)')
+ write('context.push(kwargs)')
data = self.handle_node(node.body)
if data:
buf.append(data)
- write('ctx_pop()')
+ write('context.pop()')
write('if 0: yield None')
self.indention -= 1
write('yield ' + self.handle_call_func(node.expr,
write = lambda x: buf.append(self.indent(x))
write('def filtered():')
self.indention += 1
- write('ctx_push()')
+ write('context.push()')
write(self.nodeinfo(node.body))
data = self.handle_node(node.body)
if data:
buf.append(data)
- write('ctx_pop()')
+ write('context.pop()')
write('if 0: yield None')
self.indention -= 1
write('yield %s' % self.filter('buffereater(filtered)()',
write = lambda x: buf.append(self.indent(x))
write(self.nodeinfo(node))
- write('ctx_push({\'super\': SuperBlock(%r, blocks, %r, context)})' % (
+ write('context.push({\'super\': SuperBlock(%r, blocks, %r, context)})' % (
str(node.name),
level
))
write(self.nodeinfo(node.body))
buf.append(rv)
- write('ctx_pop()')
+ write('context.pop()')
return '\n'.join(buf)
def handle_include(self, node):
from jinja import Environment
from jinja.parser import Parser
+try:
+ # This code adds support for coverage.py (see
+ # http://nedbatchelder.com/code/modules/coverage.html).
+ # It prints a coverage report for the modules specified in all
+ # module globals (of the test modules) named "coverage_modules".
+
+ import coverage, atexit
+
+ IGNORED_MODULES = ['jinja._speedups', 'jinja.defaults',
+ 'jinja.translators']
+
+ def report_coverage():
+ coverage.stop()
+ module_list = [
+ mod for name, mod in sys.modules.copy().iteritems() if
+ getattr(mod, '__file__', None) and
+ name.startswith('jinja.') and
+ name not in IGNORED_MODULES
+ ]
+ module_list.sort()
+ coverage.report(module_list)
+
+ def callback(option, opt_str, value, parser):
+ atexit.register(report_coverage)
+ coverage.erase()
+ coverage.start()
+
+ py.test.config.addoptions('Test options', py.test.config.Option('-C',
+ '--coverage', action='callback', callback=callback,
+ help='Output information about code coverage (slow!)'))
+
+except ImportError:
+ coverage = None
+
class GlobalLoader(object):
self.env = simple_env
super(Module, self).__init__(*args, **kwargs)
- def join(self, name):
- obj = getattr(self.obj, name)
- if hasattr(obj, 'func_code'):
- return JinjaTestFunction(name, parent=self)
+ def makeitem(self, name, obj, usefilters=True):
+ if name.startswith('test_'):
+ if hasattr(obj, 'func_code'):
+ return JinjaTestFunction(name, parent=self)
+ elif isinstance(obj, basestring):
+ return JinjaDocTest(name, parent=self)
class JinjaTestFunction(py.test.collect.Function):
target(self.parent.env, *args)
else:
target(*args)
+
+
+class JinjaDocTest(py.test.collect.Item):
+
+ def run(self):
+ mod = py.std.types.ModuleType(self.name)
+ mod.__doc__ = self.obj
+ self.execute(mod)
+
+ def execute(self, mod):
+ mod.env = self.parent.env
+ mod.MODULE = self.parent.obj
+ failed, tot = py.compat.doctest.testmod(mod, verbose=True)
+ if failed:
+ py.test.fail('doctest %s: %s failed out of %s' % (
+ self.fspath, failed, tot))
import jdebug
from StringIO import StringIO
-from genshi.builder import tag
-from genshi.template import MarkupTemplate
+try:
+ from genshi.builder import tag
+ from genshi.template import MarkupTemplate
+ have_genshi = True
+except ImportError:
+ have_genshi = False
from jinja import Environment
except ImportError:
have_kid = False
-from Cheetah.Template import Template as CheetahTemplate
+try:
+ from Cheetah.Template import Template as CheetahTemplate
+ have_cheetah = True
+except ImportError:
+ have_cheetah = False
try:
from mako.template import Template as MakoTemplate
table = [dict(zip('abcdefghij', map(unicode,range(1, 11))))
for x in range(1000)]
-genshi_tmpl = MarkupTemplate("""
+if have_genshi:
+ genshi_tmpl = MarkupTemplate("""
<table xmlns:py="http://genshi.edgewall.org/">
<tr py:for="row in table">
<td py:for="c in row.values()" py:content="c"/>
</table>
''')
-cheetah_tmpl = CheetahTemplate('''
+if have_cheetah:
+ cheetah_tmpl = CheetahTemplate('''
<table>
#for $row in $table
<tr>
def test_genshi():
"""Genshi Templates"""
+ if not have_genshi:
+ return
stream = genshi_tmpl.generate(table=table)
stream.render('html', strip_whitespace=False)
def test_cheetah():
"""Cheetah Templates"""
+ if not have_cheetah:
+ return
cheetah_tmpl.respond()
def test_mako():
:license: BSD, see LICENSE for more details.
"""
+import time
+import tempfile
from jinja import Environment, loaders
from jinja.exceptions import TemplateNotFound
choice_loader = loaders.ChoiceLoader([dict_loader, package_loader])
+class FakeLoader(loaders.BaseLoader):
+ local_attr = 42
+
+
def test_dict_loader():
env = Environment(loader=dict_loader)
tmpl = env.get_template('justdict.html')
pass
else:
raise AssertionError('expected template exception')
+
+
+def test_loader_redirect():
+ env = Environment(loader=FakeLoader())
+ assert env.loader.local_attr == 42
+ assert env.loader.get_source
+ assert env.loader.load
+
+
+class MemcacheTestingLoader(loaders.CachedLoaderMixin, loaders.BaseLoader):
+
+ def __init__(self, enable):
+ loaders.CachedLoaderMixin.__init__(self, enable, 40, None, True, 'foo')
+ self.times = {}
+ self.idx = 0
+
+ def touch(self, name):
+ self.times[name] = time.time()
+
+ def get_source(self, environment, name, parent):
+ self.touch(name)
+ self.idx += 1
+ return 'Template %s (%d)' % (name, self.idx)
+
+ def check_source_changed(self, environment, name):
+ if name in self.times:
+ return self.times[name]
+ return -1
+
+
+memcache_env = Environment(loader=MemcacheTestingLoader(True))
+no_memcache_env = Environment(loader=MemcacheTestingLoader(False))
+
+
+test_memcaching = r'''
+>>> not_caching = MODULE.no_memcache_env.loader
+>>> caching = MODULE.memcache_env.loader
+>>> touch = caching.touch
+
+>>> tmpl1 = not_caching.load('test.html')
+>>> tmpl2 = not_caching.load('test.html')
+>>> tmpl1 == tmpl2
+False
+
+>>> tmpl1 = caching.load('test.html')
+>>> tmpl2 = caching.load('test.html')
+>>> tmpl1 == tmpl2
+True
+
+>>> touch('test.html')
+>>> tmpl2 = caching.load('test.html')
+>>> tmpl1 == tmpl2
+False
+'''
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+ unit test for the parser
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+
+ :copyright: 2007 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from jinja import Environment
+
+NO_VARIABLE_BLOCK = '''\
+{# i'm a freaking comment #}\
+{% if foo %}{% foo %}{% endif %}
+{% for item in seq %}{% item %}{% endfor %}
+{% trans foo %}foo is {% foo %}{% endtrans %}
+{% trans foo %}one foo{% pluralize %}{% foo %} foos{% endtrans %}'''
+
+PHP_SYNTAX = '''\
+<!-- I'm a comment, I'm not interesting -->\
+<? for item in seq -?>
+ <?= item ?>
+<?- endfor ?>'''
+
+ERB_SYNTAX = '''\
+<%# I'm a comment, I'm not interesting %>\
+<% for item in seq -%>
+ <%= item %>
+<%- endfor %>'''
+
+COMMENT_SYNTAX = '''\
+<!--# I'm a comment, I'm not interesting -->\
+<!-- for item in seq --->
+ ${item}
+<!--- endfor -->'''
+
+SMARTY_SYNTAX = '''\
+{* I'm a comment, I'm not interesting *}\
+{for item in seq-}
+ {item}
+{-endfor}'''
+
+
+def test_no_variable_block():
+ env = Environment('{%', '%}', None, None)
+ tmpl = env.from_string(NO_VARIABLE_BLOCK)
+ assert tmpl.render(foo=42, seq=range(2)).splitlines() == [
+ '42',
+ '01',
+ 'foo is 42',
+ '42 foos'
+ ]
+
+
+def test_php_syntax():
+ env = Environment('<?', '?>', '<?=', '?>', '<!--', '-->')
+ tmpl = env.from_string(PHP_SYNTAX)
+ assert tmpl.render(seq=range(5)) == '01234'
+
+
+def test_erb_syntax():
+ env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>')
+ tmpl = env.from_string(ERB_SYNTAX)
+ assert tmpl.render(seq=range(5)) == '01234'
+
+
+def test_comment_syntax():
+ env = Environment('<!--', '-->', '${', '}', '<!--#', '-->')
+ tmpl = env.from_string(COMMENT_SYNTAX)
+ assert tmpl.render(seq=range(5)) == '01234'
+
+
+def test_smarty_syntax():
+ env = Environment('{', '}', '{', '}', '{*', '*}')
+ tmpl = env.from_string(SMARTY_SYNTAX)
+ assert tmpl.render(seq=range(5)) == '01234'
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+ unit test for security features
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ :copyright: 2007 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+
+
+class PrivateStuff(object):
+ bar = lambda self: 23
+ foo = lambda self: 42
+ foo.jinja_unsafe_call = True
+
+
+class PublicStuff(object):
+ jinja_allowed_attributes = ['bar']
+ bar = lambda self: 23
+ foo = lambda self: 42
+
+
+test_unsafe = '''
+>>> env.from_string("{{ foo.foo() }}").render(foo=MODULE.PrivateStuff())
+u''
+>>> env.from_string("{{ foo.bar() }}").render(foo=MODULE.PrivateStuff())
+u'23'
+
+>>> env.from_string("{{ foo.foo() }}").render(foo=MODULE.PublicStuff())
+u''
+>>> env.from_string("{{ foo.bar() }}").render(foo=MODULE.PublicStuff())
+u'23'
+
+>>> env.from_string("{{ foo.__class__ }}").render(foo=42)
+u''
+
+>>> env.from_string("{{ foo.func_code }}").render(foo=lambda:None)
+u''
+'''
+
+
+test_restricted = '''
+>>> env.from_string("{% for item.attribute in seq %}...{% endfor %}")
+Traceback (most recent call last):
+ ...
+TemplateSyntaxError: can't assign to expression. (line 1)
+'''
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+ unit test for streaming interface
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ :copyright: 2007 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+
+
+test_basic_streaming = r"""
+>>> tmpl = env.from_string("<ul>{% for item in seq %}<li>{{ loop.index "
+... "}} - {{ item }}</li>{%- endfor %}</ul>")
+>>> stream = tmpl.stream(seq=range(4))
+>>> stream.next()
+u'<ul>'
+>>> stream.next()
+u'<li>1 - 0</li>'
+>>> stream.next()
+u'<li>2 - 1</li>'
+>>> stream.next()
+u'<li>3 - 2</li>'
+>>> stream.next()
+u'<li>4 - 3</li>'
+>>> stream.next()
+u'</ul>'
+"""
+
+test_buffered_streaming = r"""
+>>> tmpl = env.from_string("<ul>{% for item in seq %}<li>{{ loop.index "
+... "}} - {{ item }}</li>{%- endfor %}</ul>")
+>>> stream = tmpl.stream(seq=range(4))
+>>> stream.enable_buffering(size=3)
+>>> stream.next()
+u'<ul><li>1 - 0</li><li>2 - 1</li>'
+>>> stream.next()
+u'<li>3 - 2</li><li>4 - 3</li></ul>'
+"""
+
+test_streaming_behavior = r"""
+>>> tmpl = env.from_string("")
+>>> stream = tmpl.stream()
+>>> stream.buffered
+False
+>>> stream.enable_buffering(20)
+>>> stream.buffered
+True
+>>> stream.disable_buffering()
+>>> stream.buffered
+False
+"""