- inclusions and imports "with context" forward all variables now, not only
the initial context.
+- added a cycle helper called `cycle`.
+
Version 2.0
-----------
(codename jinjavitus, released on July 17th 2008)
<li class="{{ loop.cycle('odd', 'even') }}">{{ row }}</li>
{% endfor %}
+With Jinja 2.1 an extra `cycle` helper exists that allows loop-unbound
+cycling. For more information have a look at the :ref:`builtin-globals`.
+
.. _loop-filtering:
Unlike in Python it's not possible to `break` or `continue` in a loop. You
.. jinjatests::
+.. _builtin-globals:
List of Global Functions
------------------------
A convenient alternative to dict literals. ``{'foo': 'bar'}`` is the same
as ``dict(foo='bar')``.
+.. class:: cycler(\*items)
+
+ The cycler allows you to cycle among values similar to how `loop.cycle`
+ works. Unlike `loop.cycle` however you can use this cycler outside of
+ loops or over multiple loops.
+
+ This is for example very useful if you want to show a list of folders and
+ files, with the folders on top, but both in the same list with alteranting
+ row colors.
+
+ The following example shows how `cycler` can be used::
+
+ {% set row_class = cycler('odd', 'even') %}
+ <ul class="browser">
+ {% for folder in folders %}
+ <li class="folder {{ row_class.next() }}">{{ folder|e }}</li>
+ {% endfor %}
+ {% for filename in files %}
+ <li class="file {{ row_class.next() }}">{{ filename|e }}</li>
+ {% endfor %}
+ </ul>
+
+ A cycler has the following attributes and methods:
+
+ .. method:: reset()
+
+ Resets the cycle to the first item.
+
+ .. method:: next()
+
+ Goes one item a head and returns the then current item.
+
+ .. attribute:: current
+
+ Returns the current item.
+
+ **new in Jinja 2.1**
+
Extensions
----------
__all__ = [
'Environment', 'Template', 'BaseLoader', 'FileSystemLoader',
'PackageLoader', 'DictLoader', 'FunctionLoader', 'PrefixLoader',
- 'ChoiceLoader', 'Undefined', 'DebugUndefined', 'StrictUndefined',
- 'TemplateError', 'UndefinedError', 'TemplateNotFound',
+ 'ChoiceLoader', 'BytecodeCache', 'FileSystemBytecodeCache',
+ 'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined',
+ 'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound',
'TemplateSyntaxError', 'TemplateAssertionError', 'environmentfilter',
'contextfilter', 'Markup', 'escape', 'environmentfunction',
'contextfunction', 'clear_caches', 'is_undefined'
from hashlib import sha1
except ImportError:
from sha import new as sha1
+from jinja2.utils import open_if_exists
bc_version = 1
return path.join(self.directory, self.pattern % bucket.key)
def load_bytecode(self, bucket):
- filename = self._get_cache_filename(bucket)
- if path.exists(filename):
- f = file(filename, 'rb')
+ f = open_if_exists(self._get_cache_filename(bucket), 'rb')
+ if f is not None:
try:
bucket.load_bytecode(f)
finally:
f.close()
def dump_bytecode(self, bucket):
- filename = self._get_cache_filename(bucket)
- f = file(filename, 'wb')
+ f = file(self._get_cache_filename(bucket), 'wb')
try:
bucket.write_bytecode(f)
finally:
return exc_info[:2] + (result_tb or initial_tb,)
-def translate_syntax_error(error):
- """When passed a syntax error it will generate a new traceback with
- more debugging information.
- """
- filename = error.filename
- if filename is None:
- filename = '<template>'
- elif isinstance(filename, unicode):
- filename = filename.encode('utf-8')
- code = compile('\n' * (error.lineno - 1) + 'raise __jinja_exception__',
- filename, 'exec')
- try:
- exec code in {'__jinja_exception__': error}
- except:
- exc_info = sys.exc_info()
- return exc_info[:2] + (exc_info[2].tb_next,)
-
-
def fake_exc_info(exc_info, filename, lineno, tb_back=None):
"""Helper for `translate_exception`."""
exc_type, exc_value, tb = exc_info
:copyright: 2007-2008 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
-from jinja2.utils import generate_lorem_ipsum
+from jinja2.utils import generate_lorem_ipsum, Cycler
# defaults for the parser / lexer
DEFAULT_NAMESPACE = {
'range': xrange,
'dict': lambda **kw: kw,
- 'lipsum': generate_lorem_ipsum
+ 'lipsum': generate_lorem_ipsum,
+ 'cycler': Cycler
}
return LRUCache(size)
+def copy_cache(cache):
+ """Create an empty copy of the given cache."""
+ if cache is None:
+ return Noe
+ elif type(cache) is dict:
+ return {}
+ return LRUCache(cache.capacity)
+
+
def load_extensions(environment, extensions):
"""Load the extensions from the list and bind it to the environment.
Returns a dict of instanciated environments.
if cache_size is not missing:
rv.cache = create_cache(cache_size)
+ else:
+ rv.cache = copy_cache(self.cache)
rv.extensions = {}
for key, value in self.extensions.iteritems():
try:
return Parser(self, source, name, filename).parse()
except TemplateSyntaxError, e:
- from jinja2.debug import translate_syntax_error
- exc_type, exc_value, tb = translate_syntax_error(e)
- raise exc_type, exc_value, tb
+ e.source = source
+ raise e
def lex(self, source, name=None, filename=None):
"""Lex the given sourcecode and return a generator that yields
of the extensions to be applied you have to filter source through
the :meth:`preprocess` method.
"""
- return self.lexer.tokeniter(unicode(source), name, filename)
+ source = unicode(source)
+ try:
+ return self.lexer.tokeniter(source, name, filename)
+ except TemplateSyntaxError, e:
+ e.source = source
+ raise e
def preprocess(self, source, name=None, filename=None):
"""Preprocesses the source with all extensions. This is automatically
else:
parent = dict(self.globals, **vars)
if locals:
+ # if the parent is shared a copy should be created because
+ # we don't want to modify the dict passed
if shared:
parent = dict(parent)
for key, value in locals.iteritems():
"""Raised to tell the user that there is a problem with the template."""
def __init__(self, message, lineno, name=None, filename=None):
- if name is not None:
- extra = '%s, line %d' % (name.encode('utf-8'), lineno)
- else:
- extra = 'line %d' % lineno
- # if the message was provided as unicode we have to encode it
- # to utf-8 explicitly
- if isinstance(message, unicode):
- message = message.encode('utf-8')
- # otherwise make sure it's a in fact valid utf-8
- else:
- message = message.decode('utf-8', 'ignore').encode('utf-8')
- TemplateError.__init__(self, '%s (%s)' % (message, extra))
- self.message = message
+ if not isinstance(message, unicode):
+ message = message.decode('utf-8', 'replace')
+ TemplateError.__init__(self, message.encode('utf-8'))
self.lineno = lineno
self.name = name
self.filename = filename
+ self.source = None
+ self.message = message
+
+ def __unicode__(self):
+ location = 'line %d' % self.lineno
+ name = self.filename or self.name
+ if name:
+ location = 'File "%s", %s' % (name, location)
+ lines = [self.message, ' ' + location]
+
+ # if the source is set, add the line to the output
+ if self.source is not None:
+ try:
+ line = self.source.splitlines()[self.lineno - 1]
+ except IndexError:
+ line = None
+ if line:
+ lines.append(' ' + line.strip())
+
+ return u'\n'.join(lines)
+
+ def __str__(self):
+ return unicode(self).encode('utf-8')
class TemplateAssertionError(TemplateSyntaxError):
{{ mytext|indent(2, true) }}
indent by two spaces and indent the first line too.
"""
- indention = ' ' * width
+ indention = u' ' * width
+ rv = (u'\n' + indention).join(s.splitlines())
if indentfirst:
- return u'\n'.join(indention + line for line in s.splitlines())
- return s.replace('\n', '\n' + indention)
+ rv = indention + rv
+ return rv
def do_truncate(s, length=255, killwords=False, end='...'):
See :ref:`Notes on subscriptions <notes-on-subscriptions>` for more details.
"""
try:
- value = getattr(obj, name)
- except AttributeError:
- return environment.undefined(obj=obj, name=name)
- if environment.sandboxed and not \
- environment.is_safe_attribute(obj, name, value):
- return environment.unsafe_undefined(obj, name)
- return value
+ name = str(name)
+ except UnicodeError:
+ pass
+ else:
+ try:
+ value = getattr(obj, name)
+ except AttributeError:
+ pass
+ else:
+ if environment.sandboxed and not \
+ environment.is_safe_attribute(obj, name, value):
+ return environment.unsafe_undefined(obj, name)
+ return value
+ return environment.undefined(obj=obj, name=name)
FILTERS = {
except ImportError:
from sha import new as sha1
from jinja2.exceptions import TemplateNotFound
-from jinja2.utils import LRUCache
+from jinja2.utils import LRUCache, open_if_exists
def split_template_path(template):
pieces = split_template_path(template)
for searchpath in self.searchpath:
filename = path.join(searchpath, *pieces)
- if not path.isfile(filename):
+ f = open_if_exists(filename)
+ if f is None:
continue
- f = file(filename)
try:
contents = f.read().decode(self.encoding)
finally:
def __init__(self, package_name, package_path='templates',
encoding='utf-8'):
- from pkg_resources import DefaultProvider, ResourceManager, get_provider
+ from pkg_resources import DefaultProvider, ResourceManager, \
+ get_provider
provider = get_provider(package_name)
self.encoding = encoding
self.manager = ResourceManager()
)
-# register the context as mutable mapping if possible
+# register the context as mapping if possible
try:
- from collections import MutableMapping
- MutableMapping.register(Context)
+ from collections import Mapping
+ Mapping.register(Context)
except ImportError:
pass
__int__ = __float__ = __complex__ = _fail_with_undefined_error
def __str__(self):
- return self.__unicode__().encode('utf-8')
+ return unicode(self).encode('utf-8')
def __unicode__(self):
return u''
"""
import re
import sys
+import errno
try:
from thread import allocate_lock
except ImportError:
raise
+def open_if_exists(filename, mode='r'):
+ """Returns a file descriptor for the filename if that file exists,
+ otherwise `None`.
+ """
+ try:
+ return file(filename, mode)
+ except IOError, e:
+ if e.errno not in (errno.ENOENT, errno.EISDIR):
+ raise
+
+
def pformat(obj, verbose=False):
"""Prettyprint an object. Either use the `pretty` library or the
builtin `pprint`.
pass
+class Cycler(object):
+ """A cycle helper for templates."""
+
+ def __init__(self, *items):
+ if not items:
+ raise RuntimeError('at least one item has to be provided')
+ self.items = items
+ self.reset()
+
+ def reset(self):
+ """Resets the cycle."""
+ self.pos = 0
+
+ @property
+ def current(self):
+ """Returns the current item."""
+ return self.items[self.pos]
+
+ def next(self):
+ """Goes one item ahead and returns it."""
+ rv = self.current
+ self.pos = (self.pos + 1) % len(self.items)
+ return rv
+
+
# we have to import it down here as the speedups module imports the
# markup type which is define above.
try:
>>> tmpl = MODULE.env.get_template('syntaxerror.html')
Traceback (most recent call last):
...
- File "loaderres/templates/syntaxerror.html", line 4, in <module>
+TemplateSyntaxError: unknown tag 'endif'
+ File "loaderres/templates/syntaxerror.html", line 4
{% endif %}
-TemplateSyntaxError: unknown tag 'endif' (syntaxerror.html, line 4)
'''
tmpl = env.from_string(INDENT)
text = '\n'.join([' '.join(['foo', 'bar'] * 2)] * 2)
out = tmpl.render(foo=text)
- assert out == 'foo bar foo bar\n foo bar foo bar| ' \
- 'foo bar foo bar\n foo bar foo bar'
+ assert out == ('foo bar foo bar\n foo bar foo bar| '
+ 'foo bar foo bar\n foo bar foo bar')
def test_int(env):
from jinja2.sandbox import SandboxedEnvironment, \
ImmutableSandboxedEnvironment, unsafe
from jinja2 import Markup, escape
-from jinja2.exceptions import SecurityError
+from jinja2.exceptions import SecurityError, TemplateSyntaxError
class PrivateStuff(object):
'''
-test_restricted = '''
->>> env = MODULE.SandboxedEnvironment()
->>> env.from_string("{% for item.attribute in seq %}...{% endfor %}")
-Traceback (most recent call last):
- ...
-TemplateSyntaxError: expected token 'in', got '.' (line 1)
->>> env.from_string("{% for foo, bar.baz in seq %}...{% endfor %}")
-Traceback (most recent call last):
- ...
-TemplateSyntaxError: expected token 'in', got '.' (line 1)
-'''
+def test_restricted():
+ env = SandboxedEnvironment()
+ raises(TemplateSyntaxError, env.from_string,
+ "{% for item.attribute in seq %}...{% endfor %}")
+ raises(TemplateSyntaxError, env.from_string,
+ "{% for foo, bar.baz in seq %}...{% endfor %}")
test_immutable_environment = '''
SecurityError: access to attribute 'clear' of 'dict' object is unsafe.
'''
+
def test_markup_operations():
# adding two strings should escape the unsafe one
unsafe = '<script type="application/x-some-script">alert("foo");</script>'
import gc
from py.test import raises
from jinja2 import escape
+from jinja2.utils import Cycler
from jinja2.exceptions import TemplateSyntaxError
assert tmpl.render(seq=(None, 1, "foo")) == '||1|foo'
tmpl = env.from_string('<{{ none }}>')
assert tmpl.render() == '<>'
+
+
+def test_cycler():
+ items = 1, 2, 3
+ c = Cycler(*items)
+ for item in items + items:
+ assert c.current == item
+ assert c.next() == item
+ c.next()
+ assert c.current == 2
+ c.reset()
+ assert c.current == 1