Various cleanups and added custom cycler.
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 5 Oct 2008 21:08:58 +0000 (23:08 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 5 Oct 2008 21:08:58 +0000 (23:08 +0200)
--HG--
branch : trunk

16 files changed:
CHANGES
docs/templates.rst
jinja2/__init__.py
jinja2/bccache.py
jinja2/debug.py
jinja2/defaults.py
jinja2/environment.py
jinja2/exceptions.py
jinja2/filters.py
jinja2/loaders.py
jinja2/runtime.py
jinja2/utils.py
tests/test_debug.py
tests/test_filters.py
tests/test_security.py
tests/test_various.py

diff --git a/CHANGES b/CHANGES
index 7a5d0c2ed0a77f6d9e5916bd76013a4b69c778c7..c0c7139f99a2ab9bef4254ba1eec9ce0e8631a82 100644 (file)
--- a/CHANGES
+++ b/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)
index 0c01eee7372318ac6d63e9645abdb320e76e6a4e..d797fec047e14ff30e398b3b0bc545ef450e69ee 100644 (file)
@@ -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
 ----------
index 0a3ff6f3f9489c0b24f08bb0cae53f74ea576a94..95b2d5bc9f47869408d921fb65f3ac63ad11642b 100644 (file)
@@ -59,8 +59,9 @@ from jinja2.utils import Markup, escape, clear_caches, \
 __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'
index e11bad5781fde23eeaa700e2ebf2a7c2f309c87e..2c57616ec11083b4b6b54b902d00a03c18dd190b 100644 (file)
@@ -24,6 +24,7 @@ try:
     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 @@ class FileSystemBytecodeCache(BytecodeCache):
         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:
index f503c218a4ae25ff064f3a154aa9b2bb0bbb45bf..53dac4d112309796952945e891a5e42e21343411 100644 (file)
@@ -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
index 1d3be69a9656d4d3c90bd487dc4c59c53028a6ca..61706aea9951ccb223e5d646d1bcc83ea44815a6 100644 (file)
@@ -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 @@ from jinja2.tests import TESTS as DEFAULT_TESTS
 DEFAULT_NAMESPACE = {
     'range':        xrange,
     'dict':         lambda **kw: kw,
-    'lipsum':       generate_lorem_ipsum
+    'lipsum':       generate_lorem_ipsum,
+    'cycler':       Cycler
 }
 
 
index 862a2477d7084e670823ca0559b953a1e8b2f937..519e9ecf5266e290927e2c2f0095f16af010eff2 100644 (file)
@@ -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 @@ class Environment(object):
 
         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 @@ class Environment(object):
         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 @@ class Environment(object):
         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 @@ class Template(object):
         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():
index 154cf44c96df86d1e14f685e9f913025c9a0ebbc..5bfca660d0158198b57c609be95f8472ce2a0208 100644 (file)
@@ -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):
index 78d4cb6899769f53ab499272afa59e1b302cf9e9..bd9a5009569764e97a9b4b6c0e4c7c59b026dfe4 100644 (file)
@@ -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 = {
index b0522cdce134bddce3a055fb276b338172464b7c..8b2221f00f6f0b2ac4a7d154d4b6f33e30166924 100644 (file)
@@ -14,7 +14,7 @@ try:
 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 @@ class FileSystemLoader(BaseLoader):
         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()
index bd7e3057fad66c07f001707251ba7ece2960a024..815b5892e06f86f75e1b843ce0e61f190574502c 100644 (file)
@@ -165,10 +165,10 @@ class Context(object):
         )
 
 
-# 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 @@ class Undefined(object):
     __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''
index f0ae6a971a40c34cade803b24bb2604e4997e7e0..338db4a15f0c6d05ba220dc38acfc868e160e802 100644 (file)
@@ -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 @@ except ImportError:
     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:
index 2363fe29efff298bff1e68a1f52bab77fd4ec74c..7f59445d70cf2b5a37fd4b3ef960294d4683c1cb 100644 (file)
@@ -31,7 +31,7 @@ test_syntax_error = '''
 >>> 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)
 '''
index f70bb4c7be19d5dda9b931b58ff8fc892af11325..a02d2ae579c5e1a8c1a55af5d413fa59687b38ab 100644 (file)
@@ -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):
index 7c812c0942e357beccce07763e96d8cbacd9139a..8ef3edf7980d44f732af1747539cdeae236a5076 100644 (file)
@@ -11,7 +11,7 @@ from jinja2 import Environment
 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 @@ SecurityError: access to attribute '__class__' of 'int' object is unsafe.
 '''
 
 
-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 @@ Traceback (most recent call last):
 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>'
index 21244731f72e2d062f127f50aec320b9cd55036a..aab5e7633ad4e8c5cbd2222ea89e25fc40ac6995 100644 (file)
@@ -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 test_finalizer():
     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