Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Various cleanups and added custom cycler.

--HG--
branch : trunk
  • Loading branch information...
commit ccae0559b4ab43be063b34cc0f0d95106173d284 1 parent 673aa88
@mitsuhiko authored
View
2  CHANGES
@@ -37,6 +37,8 @@ Version 2.1
- 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)
View
42 docs/templates.rst
@@ -482,6 +482,9 @@ each time through the loop by using the special `loop.cycle` helper::
<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
@@ -999,6 +1002,7 @@ List of Builtin Tests
.. jinjatests::
+.. _builtin-globals:
List of Global Functions
------------------------
@@ -1038,6 +1042,44 @@ The following functions are available in the global scope by default:
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
----------
View
5 jinja2/__init__.py
@@ -59,8 +59,9 @@
__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'
View
9 jinja2/bccache.py
@@ -24,6 +24,7 @@
from hashlib import sha1
except ImportError:
from sha import new as sha1
+from jinja2.utils import open_if_exists
bc_version = 1
@@ -193,17 +194,15 @@ def _get_cache_filename(self, bucket):
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:
View
18 jinja2/debug.py
@@ -35,24 +35,6 @@ def translate_exception(exc_info):
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
View
5 jinja2/defaults.py
@@ -8,7 +8,7 @@
: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
@@ -29,7 +29,8 @@
DEFAULT_NAMESPACE = {
'range': xrange,
'dict': lambda **kw: kw,
- 'lipsum': generate_lorem_ipsum
+ 'lipsum': generate_lorem_ipsum,
+ 'cycler': Cycler
}
View
25 jinja2/environment.py
@@ -48,6 +48,15 @@ def create_cache(size):
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.
@@ -285,6 +294,8 @@ def overlay(self, block_start_string=missing, block_end_string=missing,
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():
@@ -340,9 +351,8 @@ def parse(self, source, name=None, filename=None):
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
@@ -354,7 +364,12 @@ def lex(self, source, name=None, filename=None):
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
@@ -594,6 +609,8 @@ def new_context(self, vars=None, shared=False, locals=None):
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():
View
39 jinja2/exceptions.py
@@ -26,22 +26,35 @@ class TemplateSyntaxError(TemplateError):
"""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):
View
28 jinja2/filters.py
@@ -339,10 +339,11 @@ def do_indent(s, width=4, indentfirst=False):
{{ 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='...'):
@@ -648,13 +649,20 @@ def do_attr(environment, obj, name):
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 = {
View
9 jinja2/loaders.py
@@ -14,7 +14,7 @@
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):
@@ -142,9 +142,9 @@ def get_source(self, environment, 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:
@@ -171,7 +171,8 @@ class PackageLoader(BaseLoader):
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()
View
8 jinja2/runtime.py
@@ -165,10 +165,10 @@ def __repr__(self):
)
-# 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
@@ -409,7 +409,7 @@ def _fail_with_undefined_error(self, *args, **kwargs):
__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''
View
37 jinja2/utils.py
@@ -10,6 +10,7 @@
"""
import re
import sys
+import errno
try:
from thread import allocate_lock
except ImportError:
@@ -173,6 +174,17 @@ def import_string(import_name, silent=False):
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`.
@@ -648,6 +660,31 @@ def __reversed__(self):
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:
View
4 tests/test_debug.py
@@ -31,7 +31,7 @@
>>> 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)
'''
View
4 tests/test_filters.py
@@ -152,8 +152,8 @@ def test_indent(env):
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):
View
20 tests/test_security.py
@@ -11,7 +11,7 @@
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):
@@ -62,17 +62,12 @@ def __repr__(self):
'''
-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 = '''
@@ -87,6 +82,7 @@ def __repr__(self):
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>'
View
13 tests/test_various.py
@@ -9,6 +9,7 @@
import gc
from py.test import raises
from jinja2 import escape
+from jinja2.utils import Cycler
from jinja2.exceptions import TemplateSyntaxError
@@ -84,3 +85,15 @@ def finalize_none_empty(value):
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
Please sign in to comment.
Something went wrong with that request. Please try again.