update jinja2 to avoid some annoying warnings
authorMatthias Braun <matze@braunis.de>
Thu, 3 Nov 2011 10:13:41 +0000 (11:13 +0100)
committerMatthias Braun <matze@braunis.de>
Thu, 3 Nov 2011 15:18:58 +0000 (16:18 +0100)
23 files changed:
scripts/jinja2/__init__.py
scripts/jinja2/_markupsafe/__init__.py [new file with mode: 0644]
scripts/jinja2/_markupsafe/_bundle.py [new file with mode: 0644]
scripts/jinja2/_markupsafe/_constants.py [new file with mode: 0644]
scripts/jinja2/_markupsafe/_native.py [new file with mode: 0644]
scripts/jinja2/_markupsafe/tests.py [new file with mode: 0644]
scripts/jinja2/_speedups.c [deleted file]
scripts/jinja2/bccache.py
scripts/jinja2/compiler.py
scripts/jinja2/constants.py
scripts/jinja2/debug.py
scripts/jinja2/defaults.py
scripts/jinja2/environment.py
scripts/jinja2/ext.py
scripts/jinja2/filters.py
scripts/jinja2/lexer.py
scripts/jinja2/loaders.py
scripts/jinja2/nodes.py
scripts/jinja2/parser.py
scripts/jinja2/runtime.py
scripts/jinja2/sandbox.py
scripts/jinja2/tests.py
scripts/jinja2/utils.py

index ed91fc5..0cf967d 100644 (file)
     :license: BSD, see LICENSE for more details.
 """
 __docformat__ = 'restructuredtext en'
-try:
-    __version__ = __import__('pkg_resources') \
-        .get_distribution('Jinja2').version
-except:
-    __version__ = 'unknown'
+__version__ = '2.6'
 
 # high level interface
 from jinja2.environment import Environment, Template
 
 # loaders
 from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \
-     DictLoader, FunctionLoader, PrefixLoader, ChoiceLoader
+     DictLoader, FunctionLoader, PrefixLoader, ChoiceLoader, \
+     ModuleLoader
 
 # bytecode caches
 from jinja2.bccache import BytecodeCache, FileSystemBytecodeCache, \
@@ -53,9 +50,11 @@ from jinja2.exceptions import TemplateError, UndefinedError, \
      TemplateAssertionError
 
 # decorators and public utilities
-from jinja2.filters import environmentfilter, contextfilter
+from jinja2.filters import environmentfilter, contextfilter, \
+     evalcontextfilter
 from jinja2.utils import Markup, escape, clear_caches, \
-     environmentfunction, contextfunction, is_undefined
+     environmentfunction, evalcontextfunction, contextfunction, \
+     is_undefined
 
 __all__ = [
     'Environment', 'Template', 'BaseLoader', 'FileSystemLoader',
@@ -64,6 +63,7 @@ __all__ = [
     'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined',
     'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound',
     'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError',
-    'environmentfilter', 'contextfilter', 'Markup', 'escape',
-    'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined'
+    'ModuleLoader', 'environmentfilter', 'contextfilter', 'Markup', 'escape',
+    'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined',
+    'evalcontextfilter', 'evalcontextfunction'
 ]
diff --git a/scripts/jinja2/_markupsafe/__init__.py b/scripts/jinja2/_markupsafe/__init__.py
new file mode 100644 (file)
index 0000000..3cea815
--- /dev/null
@@ -0,0 +1,225 @@
+# -*- coding: utf-8 -*-
+"""
+    markupsafe
+    ~~~~~~~~~~
+
+    Implements a Markup string.
+
+    :copyright: (c) 2010 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+import re
+from itertools import imap
+
+
+__all__ = ['Markup', 'soft_unicode', 'escape', 'escape_silent']
+
+
+_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
+_entity_re = re.compile(r'&([^;]+);')
+
+
+class Markup(unicode):
+    r"""Marks a string as being safe for inclusion in HTML/XML output without
+    needing to be escaped.  This implements the `__html__` interface a couple
+    of frameworks and web applications use.  :class:`Markup` is a direct
+    subclass of `unicode` and provides all the methods of `unicode` just that
+    it escapes arguments passed and always returns `Markup`.
+
+    The `escape` function returns markup objects so that double escaping can't
+    happen.
+
+    The constructor of the :class:`Markup` class can be used for three
+    different things:  When passed an unicode object it's assumed to be safe,
+    when passed an object with an HTML representation (has an `__html__`
+    method) that representation is used, otherwise the object passed is
+    converted into a unicode string and then assumed to be safe:
+
+    >>> Markup("Hello <em>World</em>!")
+    Markup(u'Hello <em>World</em>!')
+    >>> class Foo(object):
+    ...  def __html__(self):
+    ...   return '<a href="#">foo</a>'
+    ...
+    >>> Markup(Foo())
+    Markup(u'<a href="#">foo</a>')
+
+    If you want object passed being always treated as unsafe you can use the
+    :meth:`escape` classmethod to create a :class:`Markup` object:
+
+    >>> Markup.escape("Hello <em>World</em>!")
+    Markup(u'Hello &lt;em&gt;World&lt;/em&gt;!')
+
+    Operations on a markup string are markup aware which means that all
+    arguments are passed through the :func:`escape` function:
+
+    >>> em = Markup("<em>%s</em>")
+    >>> em % "foo & bar"
+    Markup(u'<em>foo &amp; bar</em>')
+    >>> strong = Markup("<strong>%(text)s</strong>")
+    >>> strong % {'text': '<blink>hacker here</blink>'}
+    Markup(u'<strong>&lt;blink&gt;hacker here&lt;/blink&gt;</strong>')
+    >>> Markup("<em>Hello</em> ") + "<foo>"
+    Markup(u'<em>Hello</em> &lt;foo&gt;')
+    """
+    __slots__ = ()
+
+    def __new__(cls, base=u'', encoding=None, errors='strict'):
+        if hasattr(base, '__html__'):
+            base = base.__html__()
+        if encoding is None:
+            return unicode.__new__(cls, base)
+        return unicode.__new__(cls, base, encoding, errors)
+
+    def __html__(self):
+        return self
+
+    def __add__(self, other):
+        if hasattr(other, '__html__') or isinstance(other, basestring):
+            return self.__class__(unicode(self) + unicode(escape(other)))
+        return NotImplemented
+
+    def __radd__(self, other):
+        if hasattr(other, '__html__') or isinstance(other, basestring):
+            return self.__class__(unicode(escape(other)) + unicode(self))
+        return NotImplemented
+
+    def __mul__(self, num):
+        if isinstance(num, (int, long)):
+            return self.__class__(unicode.__mul__(self, num))
+        return NotImplemented
+    __rmul__ = __mul__
+
+    def __mod__(self, arg):
+        if isinstance(arg, tuple):
+            arg = tuple(imap(_MarkupEscapeHelper, arg))
+        else:
+            arg = _MarkupEscapeHelper(arg)
+        return self.__class__(unicode.__mod__(self, arg))
+
+    def __repr__(self):
+        return '%s(%s)' % (
+            self.__class__.__name__,
+            unicode.__repr__(self)
+        )
+
+    def join(self, seq):
+        return self.__class__(unicode.join(self, imap(escape, seq)))
+    join.__doc__ = unicode.join.__doc__
+
+    def split(self, *args, **kwargs):
+        return map(self.__class__, unicode.split(self, *args, **kwargs))
+    split.__doc__ = unicode.split.__doc__
+
+    def rsplit(self, *args, **kwargs):
+        return map(self.__class__, unicode.rsplit(self, *args, **kwargs))
+    rsplit.__doc__ = unicode.rsplit.__doc__
+
+    def splitlines(self, *args, **kwargs):
+        return map(self.__class__, unicode.splitlines(self, *args, **kwargs))
+    splitlines.__doc__ = unicode.splitlines.__doc__
+
+    def unescape(self):
+        r"""Unescape markup again into an unicode string.  This also resolves
+        known HTML4 and XHTML entities:
+
+        >>> Markup("Main &raquo; <em>About</em>").unescape()
+        u'Main \xbb <em>About</em>'
+        """
+        from jinja2._markupsafe._constants import HTML_ENTITIES
+        def handle_match(m):
+            name = m.group(1)
+            if name in HTML_ENTITIES:
+                return unichr(HTML_ENTITIES[name])
+            try:
+                if name[:2] in ('#x', '#X'):
+                    return unichr(int(name[2:], 16))
+                elif name.startswith('#'):
+                    return unichr(int(name[1:]))
+            except ValueError:
+                pass
+            return u''
+        return _entity_re.sub(handle_match, unicode(self))
+
+    def striptags(self):
+        r"""Unescape markup into an unicode string and strip all tags.  This
+        also resolves known HTML4 and XHTML entities.  Whitespace is
+        normalized to one:
+
+        >>> Markup("Main &raquo;  <em>About</em>").striptags()
+        u'Main \xbb About'
+        """
+        stripped = u' '.join(_striptags_re.sub('', self).split())
+        return Markup(stripped).unescape()
+
+    @classmethod
+    def escape(cls, s):
+        """Escape the string.  Works like :func:`escape` with the difference
+        that for subclasses of :class:`Markup` this function would return the
+        correct subclass.
+        """
+        rv = escape(s)
+        if rv.__class__ is not cls:
+            return cls(rv)
+        return rv
+
+    def make_wrapper(name):
+        orig = getattr(unicode, name)
+        def func(self, *args, **kwargs):
+            args = _escape_argspec(list(args), enumerate(args))
+            _escape_argspec(kwargs, kwargs.iteritems())
+            return self.__class__(orig(self, *args, **kwargs))
+        func.__name__ = orig.__name__
+        func.__doc__ = orig.__doc__
+        return func
+
+    for method in '__getitem__', 'capitalize', \
+                  'title', 'lower', 'upper', 'replace', 'ljust', \
+                  'rjust', 'lstrip', 'rstrip', 'center', 'strip', \
+                  'translate', 'expandtabs', 'swapcase', 'zfill':
+        locals()[method] = make_wrapper(method)
+
+    # new in python 2.5
+    if hasattr(unicode, 'partition'):
+        partition = make_wrapper('partition'),
+        rpartition = make_wrapper('rpartition')
+
+    # new in python 2.6
+    if hasattr(unicode, 'format'):
+        format = make_wrapper('format')
+
+    # not in python 3
+    if hasattr(unicode, '__getslice__'):
+        __getslice__ = make_wrapper('__getslice__')
+
+    del method, make_wrapper
+
+
+def _escape_argspec(obj, iterable):
+    """Helper for various string-wrapped functions."""
+    for key, value in iterable:
+        if hasattr(value, '__html__') or isinstance(value, basestring):
+            obj[key] = escape(value)
+    return obj
+
+
+class _MarkupEscapeHelper(object):
+    """Helper for Markup.__mod__"""
+
+    def __init__(self, obj):
+        self.obj = obj
+
+    __getitem__ = lambda s, x: _MarkupEscapeHelper(s.obj[x])
+    __str__ = lambda s: str(escape(s.obj))
+    __unicode__ = lambda s: unicode(escape(s.obj))
+    __repr__ = lambda s: str(escape(repr(s.obj)))
+    __int__ = lambda s: int(s.obj)
+    __float__ = lambda s: float(s.obj)
+
+
+# we have to import it down here as the speedups and native
+# modules imports the markup type which is define above.
+try:
+    from jinja2._markupsafe._speedups import escape, escape_silent, soft_unicode
+except ImportError:
+    from jinja2._markupsafe._native import escape, escape_silent, soft_unicode
diff --git a/scripts/jinja2/_markupsafe/_bundle.py b/scripts/jinja2/_markupsafe/_bundle.py
new file mode 100644 (file)
index 0000000..e694faf
--- /dev/null
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja2._markupsafe._bundle
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    This script pulls in markupsafe from a source folder and
+    bundles it with Jinja2.  It does not pull in the speedups
+    module though.
+
+    :copyright: Copyright 2010 by the Jinja team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+import sys
+import os
+import re
+
+
+def rewrite_imports(lines):
+    for idx, line in enumerate(lines):
+        new_line = re.sub(r'(import|from)\s+markupsafe\b',
+                          r'\1 jinja2._markupsafe', line)
+        if new_line != line:
+            lines[idx] = new_line
+
+
+def main():
+    if len(sys.argv) != 2:
+        print 'error: only argument is path to markupsafe'
+        sys.exit(1)
+    basedir = os.path.dirname(__file__)
+    markupdir = sys.argv[1]
+    for filename in os.listdir(markupdir):
+        if filename.endswith('.py'):
+            f = open(os.path.join(markupdir, filename))
+            try:
+                lines = list(f)
+            finally:
+                f.close()
+            rewrite_imports(lines)
+            f = open(os.path.join(basedir, filename), 'w')
+            try:
+                for line in lines:
+                    f.write(line)
+            finally:
+                f.close()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/jinja2/_markupsafe/_constants.py b/scripts/jinja2/_markupsafe/_constants.py
new file mode 100644 (file)
index 0000000..919bf03
--- /dev/null
@@ -0,0 +1,267 @@
+# -*- coding: utf-8 -*-
+"""
+    markupsafe._constants
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Highlevel implementation of the Markup string.
+
+    :copyright: (c) 2010 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+
+HTML_ENTITIES = {
+    'AElig': 198,
+    'Aacute': 193,
+    'Acirc': 194,
+    'Agrave': 192,
+    'Alpha': 913,
+    'Aring': 197,
+    'Atilde': 195,
+    'Auml': 196,
+    'Beta': 914,
+    'Ccedil': 199,
+    'Chi': 935,
+    'Dagger': 8225,
+    'Delta': 916,
+    'ETH': 208,
+    'Eacute': 201,
+    'Ecirc': 202,
+    'Egrave': 200,
+    'Epsilon': 917,
+    'Eta': 919,
+    'Euml': 203,
+    'Gamma': 915,
+    'Iacute': 205,
+    'Icirc': 206,
+    'Igrave': 204,
+    'Iota': 921,
+    'Iuml': 207,
+    'Kappa': 922,
+    'Lambda': 923,
+    'Mu': 924,
+    'Ntilde': 209,
+    'Nu': 925,
+    'OElig': 338,
+    'Oacute': 211,
+    'Ocirc': 212,
+    'Ograve': 210,
+    'Omega': 937,
+    'Omicron': 927,
+    'Oslash': 216,
+    'Otilde': 213,
+    'Ouml': 214,
+    'Phi': 934,
+    'Pi': 928,
+    'Prime': 8243,
+    'Psi': 936,
+    'Rho': 929,
+    'Scaron': 352,
+    'Sigma': 931,
+    'THORN': 222,
+    'Tau': 932,
+    'Theta': 920,
+    'Uacute': 218,
+    'Ucirc': 219,
+    'Ugrave': 217,
+    'Upsilon': 933,
+    'Uuml': 220,
+    'Xi': 926,
+    'Yacute': 221,
+    'Yuml': 376,
+    'Zeta': 918,
+    'aacute': 225,
+    'acirc': 226,
+    'acute': 180,
+    'aelig': 230,
+    'agrave': 224,
+    'alefsym': 8501,
+    'alpha': 945,
+    'amp': 38,
+    'and': 8743,
+    'ang': 8736,
+    'apos': 39,
+    'aring': 229,
+    'asymp': 8776,
+    'atilde': 227,
+    'auml': 228,
+    'bdquo': 8222,
+    'beta': 946,
+    'brvbar': 166,
+    'bull': 8226,
+    'cap': 8745,
+    'ccedil': 231,
+    'cedil': 184,
+    'cent': 162,
+    'chi': 967,
+    'circ': 710,
+    'clubs': 9827,
+    'cong': 8773,
+    'copy': 169,
+    'crarr': 8629,
+    'cup': 8746,
+    'curren': 164,
+    'dArr': 8659,
+    'dagger': 8224,
+    'darr': 8595,
+    'deg': 176,
+    'delta': 948,
+    'diams': 9830,
+    'divide': 247,
+    'eacute': 233,
+    'ecirc': 234,
+    'egrave': 232,
+    'empty': 8709,
+    'emsp': 8195,
+    'ensp': 8194,
+    'epsilon': 949,
+    'equiv': 8801,
+    'eta': 951,
+    'eth': 240,
+    'euml': 235,
+    'euro': 8364,
+    'exist': 8707,
+    'fnof': 402,
+    'forall': 8704,
+    'frac12': 189,
+    'frac14': 188,
+    'frac34': 190,
+    'frasl': 8260,
+    'gamma': 947,
+    'ge': 8805,
+    'gt': 62,
+    'hArr': 8660,
+    'harr': 8596,
+    'hearts': 9829,
+    'hellip': 8230,
+    'iacute': 237,
+    'icirc': 238,
+    'iexcl': 161,
+    'igrave': 236,
+    'image': 8465,
+    'infin': 8734,
+    'int': 8747,
+    'iota': 953,
+    'iquest': 191,
+    'isin': 8712,
+    'iuml': 239,
+    'kappa': 954,
+    'lArr': 8656,
+    'lambda': 955,
+    'lang': 9001,
+    'laquo': 171,
+    'larr': 8592,
+    'lceil': 8968,
+    'ldquo': 8220,
+    'le': 8804,
+    'lfloor': 8970,
+    'lowast': 8727,
+    'loz': 9674,
+    'lrm': 8206,
+    'lsaquo': 8249,
+    'lsquo': 8216,
+    'lt': 60,
+    'macr': 175,
+    'mdash': 8212,
+    'micro': 181,
+    'middot': 183,
+    'minus': 8722,
+    'mu': 956,
+    'nabla': 8711,
+    'nbsp': 160,
+    'ndash': 8211,
+    'ne': 8800,
+    'ni': 8715,
+    'not': 172,
+    'notin': 8713,
+    'nsub': 8836,
+    'ntilde': 241,
+    'nu': 957,
+    'oacute': 243,
+    'ocirc': 244,
+    'oelig': 339,
+    'ograve': 242,
+    'oline': 8254,
+    'omega': 969,
+    'omicron': 959,
+    'oplus': 8853,
+    'or': 8744,
+    'ordf': 170,
+    'ordm': 186,
+    'oslash': 248,
+    'otilde': 245,
+    'otimes': 8855,
+    'ouml': 246,
+    'para': 182,
+    'part': 8706,
+    'permil': 8240,
+    'perp': 8869,
+    'phi': 966,
+    'pi': 960,
+    'piv': 982,
+    'plusmn': 177,
+    'pound': 163,
+    'prime': 8242,
+    'prod': 8719,
+    'prop': 8733,
+    'psi': 968,
+    'quot': 34,
+    'rArr': 8658,
+    'radic': 8730,
+    'rang': 9002,
+    'raquo': 187,
+    'rarr': 8594,
+    'rceil': 8969,
+    'rdquo': 8221,
+    'real': 8476,
+    'reg': 174,
+    'rfloor': 8971,
+    'rho': 961,
+    'rlm': 8207,
+    'rsaquo': 8250,
+    'rsquo': 8217,
+    'sbquo': 8218,
+    'scaron': 353,
+    'sdot': 8901,
+    'sect': 167,
+    'shy': 173,
+    'sigma': 963,
+    'sigmaf': 962,
+    'sim': 8764,
+    'spades': 9824,
+    'sub': 8834,
+    'sube': 8838,
+    'sum': 8721,
+    'sup': 8835,
+    'sup1': 185,
+    'sup2': 178,
+    'sup3': 179,
+    'supe': 8839,
+    'szlig': 223,
+    'tau': 964,
+    'there4': 8756,
+    'theta': 952,
+    'thetasym': 977,
+    'thinsp': 8201,
+    'thorn': 254,
+    'tilde': 732,
+    'times': 215,
+    'trade': 8482,
+    'uArr': 8657,
+    'uacute': 250,
+    'uarr': 8593,
+    'ucirc': 251,
+    'ugrave': 249,
+    'uml': 168,
+    'upsih': 978,
+    'upsilon': 965,
+    'uuml': 252,
+    'weierp': 8472,
+    'xi': 958,
+    'yacute': 253,
+    'yen': 165,
+    'yuml': 255,
+    'zeta': 950,
+    'zwj': 8205,
+    'zwnj': 8204
+}
diff --git a/scripts/jinja2/_markupsafe/_native.py b/scripts/jinja2/_markupsafe/_native.py
new file mode 100644 (file)
index 0000000..7b95828
--- /dev/null
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+"""
+    markupsafe._native
+    ~~~~~~~~~~~~~~~~~~
+
+    Native Python implementation the C module is not compiled.
+
+    :copyright: (c) 2010 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+from jinja2._markupsafe import Markup
+
+
+def escape(s):
+    """Convert the characters &, <, >, ' and " in string s to HTML-safe
+    sequences.  Use this if you need to display text that might contain
+    such characters in HTML.  Marks return value as markup string.
+    """
+    if hasattr(s, '__html__'):
+        return s.__html__()
+    return Markup(unicode(s)
+        .replace('&', '&amp;')
+        .replace('>', '&gt;')
+        .replace('<', '&lt;')
+        .replace("'", '&#39;')
+        .replace('"', '&#34;')
+    )
+
+
+def escape_silent(s):
+    """Like :func:`escape` but converts `None` into an empty
+    markup string.
+    """
+    if s is None:
+        return Markup()
+    return escape(s)
+
+
+def soft_unicode(s):
+    """Make a string unicode if it isn't already.  That way a markup
+    string is not converted back to unicode.
+    """
+    if not isinstance(s, unicode):
+        s = unicode(s)
+    return s
diff --git a/scripts/jinja2/_markupsafe/tests.py b/scripts/jinja2/_markupsafe/tests.py
new file mode 100644 (file)
index 0000000..c1ce394
--- /dev/null
@@ -0,0 +1,80 @@
+import gc
+import unittest
+from jinja2._markupsafe import Markup, escape, escape_silent
+
+
+class MarkupTestCase(unittest.TestCase):
+
+    def test_markup_operations(self):
+        # adding two strings should escape the unsafe one
+        unsafe = '<script type="application/x-some-script">alert("foo");</script>'
+        safe = Markup('<em>username</em>')
+        assert unsafe + safe == unicode(escape(unsafe)) + unicode(safe)
+
+        # string interpolations are safe to use too
+        assert Markup('<em>%s</em>') % '<bad user>' == \
+               '<em>&lt;bad user&gt;</em>'
+        assert Markup('<em>%(username)s</em>') % {
+            'username': '<bad user>'
+        } == '<em>&lt;bad user&gt;</em>'
+
+        # an escaped object is markup too
+        assert type(Markup('foo') + 'bar') is Markup
+
+        # and it implements __html__ by returning itself
+        x = Markup("foo")
+        assert x.__html__() is x
+
+        # it also knows how to treat __html__ objects
+        class Foo(object):
+            def __html__(self):
+                return '<em>awesome</em>'
+            def __unicode__(self):
+                return 'awesome'
+        assert Markup(Foo()) == '<em>awesome</em>'
+        assert Markup('<strong>%s</strong>') % Foo() == \
+               '<strong><em>awesome</em></strong>'
+
+        # escaping and unescaping
+        assert escape('"<>&\'') == '&#34;&lt;&gt;&amp;&#39;'
+        assert Markup("<em>Foo &amp; Bar</em>").striptags() == "Foo & Bar"
+        assert Markup("&lt;test&gt;").unescape() == "<test>"
+
+    def test_all_set(self):
+        import jinja2._markupsafe as markup
+        for item in markup.__all__:
+            getattr(markup, item)
+
+    def test_escape_silent(self):
+        assert escape_silent(None) == Markup()
+        assert escape(None) == Markup(None)
+        assert escape_silent('<foo>') == Markup(u'&lt;foo&gt;')
+
+
+class MarkupLeakTestCase(unittest.TestCase):
+
+    def test_markup_leaks(self):
+        counts = set()
+        for count in xrange(20):
+            for item in xrange(1000):
+                escape("foo")
+                escape("<foo>")
+                escape(u"foo")
+                escape(u"<foo>")
+            counts.add(len(gc.get_objects()))
+        assert len(counts) == 1, 'ouch, c extension seems to leak objects'
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(MarkupTestCase))
+
+    # this test only tests the c extension
+    if not hasattr(escape, 'func_code'):
+        suite.addTest(unittest.makeSuite(MarkupLeakTestCase))
+
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/scripts/jinja2/_speedups.c b/scripts/jinja2/_speedups.c
deleted file mode 100644 (file)
index 64c7dc6..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-/**
- * jinja2._speedups
- * ~~~~~~~~~~~~~~~~
- *
- * This module implements functions for automatic escaping in C for better
- * performance.  Additionally it defines a `tb_set_next` function to patch the
- * debug traceback.  If the speedups module is not compiled a ctypes
- * implementation of `tb_set_next` and Python implementations of the other
- * functions are used.
- *
- * :copyright: (c) 2009 by the Jinja Team.
- * :license: BSD.
- */
-
-#include <Python.h>
-
-#define ESCAPED_CHARS_TABLE_SIZE 63
-#define UNICHR(x) (((PyUnicodeObject*)PyUnicode_DecodeASCII(x, strlen(x), NULL))->str);
-
-#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
-typedef int Py_ssize_t;
-#define PY_SSIZE_T_MAX INT_MAX
-#define PY_SSIZE_T_MIN INT_MIN
-#endif
-
-
-static PyObject* markup;
-static Py_ssize_t escaped_chars_delta_len[ESCAPED_CHARS_TABLE_SIZE];
-static Py_UNICODE *escaped_chars_repl[ESCAPED_CHARS_TABLE_SIZE];
-
-static int
-init_constants(void)
-{
-       PyObject *module;
-       /* happing of characters to replace */
-       escaped_chars_repl['"'] = UNICHR("&#34;");
-       escaped_chars_repl['\''] = UNICHR("&#39;");
-       escaped_chars_repl['&'] = UNICHR("&amp;");
-       escaped_chars_repl['<'] = UNICHR("&lt;");
-       escaped_chars_repl['>'] = UNICHR("&gt;");
-
-       /* lengths of those characters when replaced - 1 */
-       memset(escaped_chars_delta_len, 0, sizeof (escaped_chars_delta_len));
-       escaped_chars_delta_len['"'] = escaped_chars_delta_len['\''] = \
-               escaped_chars_delta_len['&'] = 4;
-       escaped_chars_delta_len['<'] = escaped_chars_delta_len['>'] = 3;
-
-       /* import markup type so that we can mark the return value */
-       module = PyImport_ImportModule("jinja2.utils");
-       if (!module)
-               return 0;
-       markup = PyObject_GetAttrString(module, "Markup");
-       Py_DECREF(module);
-
-       return 1;
-}
-
-static PyObject*
-escape_unicode(PyUnicodeObject *in)
-{
-       PyUnicodeObject *out;
-       Py_UNICODE *inp = in->str;
-       const Py_UNICODE *inp_end = in->str + in->length;
-       Py_UNICODE *next_escp;
-       Py_UNICODE *outp;
-       Py_ssize_t delta=0, erepl=0, delta_len=0;
-
-       /* First we need to figure out how long the escaped string will be */
-       while (*(inp) || inp < inp_end) {
-               if (*inp < ESCAPED_CHARS_TABLE_SIZE && escaped_chars_delta_len[*inp]) {
-                       delta += escaped_chars_delta_len[*inp];
-                       ++erepl;
-               }
-               ++inp;
-       }
-
-       /* Do we need to escape anything at all? */
-       if (!erepl) {
-               Py_INCREF(in);
-               return (PyObject*)in;
-       }
-
-       out = (PyUnicodeObject*)PyUnicode_FromUnicode(NULL, in->length + delta);
-       if (!out)
-               return NULL;
-
-       outp = out->str;
-       inp = in->str;
-       while (erepl-- > 0) {
-               /* look for the next substitution */
-               next_escp = inp;
-               while (next_escp < inp_end) {
-                       if (*next_escp < ESCAPED_CHARS_TABLE_SIZE &&
-                           (delta_len = escaped_chars_delta_len[*next_escp])) {
-                               ++delta_len;
-                               break;
-                       }
-                       ++next_escp;
-               }
-
-               if (next_escp > inp) {
-                       /* copy unescaped chars between inp and next_escp */
-                       Py_UNICODE_COPY(outp, inp, next_escp-inp);
-                       outp += next_escp - inp;
-               }
-
-               /* escape 'next_escp' */
-               Py_UNICODE_COPY(outp, escaped_chars_repl[*next_escp], delta_len);
-               outp += delta_len;
-
-               inp = next_escp + 1;
-       }
-       if (inp < inp_end)
-               Py_UNICODE_COPY(outp, inp, in->length - (inp - in->str));
-
-       return (PyObject*)out;
-}
-
-
-static PyObject*
-escape(PyObject *self, PyObject *text)
-{
-       PyObject *s = NULL, *rv = NULL, *html;
-
-       /* we don't have to escape integers, bools or floats */
-       if (PyInt_CheckExact(text) || PyLong_CheckExact(text) ||
-           PyFloat_CheckExact(text) || PyBool_Check(text) ||
-           text == Py_None)
-               return PyObject_CallFunctionObjArgs(markup, text, NULL);
-
-       /* if the object has an __html__ method that performs the escaping */
-       html = PyObject_GetAttrString(text, "__html__");
-       if (html) {
-               rv = PyObject_CallObject(html, NULL);
-               Py_DECREF(html);
-               return rv;
-       }
-
-       /* otherwise make the object unicode if it isn't, then escape */
-       PyErr_Clear();
-       if (!PyUnicode_Check(text)) {
-               PyObject *unicode = PyObject_Unicode(text);
-               if (!unicode)
-                       return NULL;
-               s = escape_unicode((PyUnicodeObject*)unicode);
-               Py_DECREF(unicode);
-       }
-       else
-               s = escape_unicode((PyUnicodeObject*)text);
-
-       /* convert the unicode string into a markup object. */
-       rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
-       Py_DECREF(s);
-       return rv;
-}
-
-
-static PyObject*
-soft_unicode(PyObject *self, PyObject *s)
-{
-       if (!PyUnicode_Check(s))
-               return PyObject_Unicode(s);
-       Py_INCREF(s);
-       return s;
-}
-
-
-static PyObject*
-tb_set_next(PyObject *self, PyObject *args)
-{
-       PyTracebackObject *tb, *old;
-       PyObject *next;
-
-       if (!PyArg_ParseTuple(args, "O!O:tb_set_next", &PyTraceBack_Type, &tb, &next))
-               return NULL;
-       if (next == Py_None)
-               next = NULL;
-       else if (!PyTraceBack_Check(next)) {
-               PyErr_SetString(PyExc_TypeError,
-                               "tb_set_next arg 2 must be traceback or None");
-               return NULL;
-       }
-       else
-               Py_INCREF(next);
-
-       old = tb->tb_next;
-       tb->tb_next = (PyTracebackObject*)next;
-       Py_XDECREF(old);
-
-       Py_INCREF(Py_None);
-       return Py_None;
-}
-
-
-static PyMethodDef module_methods[] = {
-       {"escape", (PyCFunction)escape, METH_O,
-        "escape(s) -> markup\n\n"
-        "Convert the characters &, <, >, ', and \" in string s to HTML-safe\n"
-        "sequences.  Use this if you need to display text that might contain\n"
-        "such characters in HTML.  Marks return value as markup string."},
-       {"soft_unicode", (PyCFunction)soft_unicode, METH_O,
-        "soft_unicode(object) -> string\n\n"
-         "Make a string unicode if it isn't already.  That way a markup\n"
-         "string is not converted back to unicode."},
-       {"tb_set_next", (PyCFunction)tb_set_next, METH_VARARGS,
-        "Set the tb_next member of a traceback object."},
-       {NULL, NULL, 0, NULL}           /* Sentinel */
-};
-
-
-#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
-#define PyMODINIT_FUNC void
-#endif
-PyMODINIT_FUNC
-init_speedups(void)
-{
-       if (!init_constants())
-               return;
-
-       Py_InitModule3("jinja2._speedups", module_methods, "");
-}
index 1e2236c..0b0ccad 100644 (file)
     :license: BSD.
 """
 from os import path, listdir
+import sys
 import marshal
 import tempfile
 import cPickle as pickle
 import fnmatch
-from cStringIO import StringIO
 try:
     from hashlib import sha1
 except ImportError:
@@ -27,8 +27,36 @@ except ImportError:
 from jinja2.utils import open_if_exists
 
 
-bc_version = 1
-bc_magic = 'j2'.encode('ascii') + pickle.dumps(bc_version, 2)
+# marshal works better on 3.x, one hack less required
+if sys.version_info > (3, 0):
+    from io import BytesIO
+    marshal_dump = marshal.dump
+    marshal_load = marshal.load
+else:
+    from cStringIO import StringIO as BytesIO
+
+    def marshal_dump(code, f):
+        if isinstance(f, file):
+            marshal.dump(code, f)
+        else:
+            f.write(marshal.dumps(code))
+
+    def marshal_load(f):
+        if isinstance(f, file):
+            return marshal.load(f)
+        return marshal.loads(f.read())
+
+
+bc_version = 2
+
+# magic version used to only change with new jinja versions.  With 2.6
+# we change this to also take Python version changes into account.  The
+# reason for this is that Python tends to segfault if fed earlier bytecode
+# versions because someone thought it would be a good idea to reuse opcodes
+# or make Python incompatible with earlier versions.
+bc_magic = 'j2'.encode('ascii') + \
+    pickle.dumps(bc_version, 2) + \
+    pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1])
 
 
 class Bucket(object):
@@ -62,12 +90,7 @@ class Bucket(object):
         if self.checksum != checksum:
             self.reset()
             return
-        # now load the code.  Because marshal is not able to load
-        # from arbitrary streams we have to work around that
-        if isinstance(f, file):
-            self.code = marshal.load(f)
-        else:
-            self.code = marshal.loads(f.read())
+        self.code = marshal_load(f)
 
     def write_bytecode(self, f):
         """Dump the bytecode into the file or file like object passed."""
@@ -75,18 +98,15 @@ class Bucket(object):
             raise TypeError('can\'t write empty bucket')
         f.write(bc_magic)
         pickle.dump(self.checksum, f, 2)
-        if isinstance(f, file):
-            marshal.dump(self.code, f)
-        else:
-            f.write(marshal.dumps(self.code))
+        marshal_dump(self.code, f)
 
     def bytecode_from_string(self, string):
         """Load bytecode from a string."""
-        self.load_bytecode(StringIO(string))
+        self.load_bytecode(BytesIO(string))
 
     def bytecode_to_string(self):
         """Return the bytecode as string."""
-        out = StringIO()
+        out = BytesIO()
         self.write_bytecode(out)
         return out.getvalue()
 
@@ -144,9 +164,10 @@ class BytecodeCache(object):
         """Returns the unique hash key for this template name."""
         hash = sha1(name.encode('utf-8'))
         if filename is not None:
+            filename = '|' + filename
             if isinstance(filename, unicode):
                 filename = filename.encode('utf-8')
-            hash.update('|' + filename)
+            hash.update(filename)
         return hash.hexdigest()
 
     def get_source_checksum(self, source):
index a52b1c7..b21cb38 100644 (file)
@@ -12,7 +12,8 @@ from cStringIO import StringIO
 from itertools import chain
 from copy import deepcopy
 from jinja2 import nodes
-from jinja2.visitor import NodeVisitor, NodeTransformer
+from jinja2.nodes import EvalContext
+from jinja2.visitor import NodeVisitor
 from jinja2.exceptions import TemplateAssertionError
 from jinja2.utils import Markup, concat, escape, is_python_keyword, next
 
@@ -53,11 +54,12 @@ def unoptimize_before_dead_code():
 unoptimize_before_dead_code = bool(unoptimize_before_dead_code().func_closure)
 
 
-def generate(node, environment, name, filename, stream=None):
+def generate(node, environment, name, filename, stream=None,
+             defer_init=False):
     """Generate the python source for a node tree."""
     if not isinstance(node, nodes.Template):
         raise TypeError('Can\'t compile non template nodes')
-    generator = CodeGenerator(environment, name, filename, stream)
+    generator = CodeGenerator(environment, name, filename, stream, defer_init)
     generator.visit(node)
     if stream is None:
         return generator.stream.getvalue()
@@ -125,12 +127,10 @@ class Identifiers(object):
         self.undeclared.discard(name)
         self.declared.add(name)
 
-    def is_declared(self, name, local_only=False):
+    def is_declared(self, name):
         """Check if a name is declared in this or an outer scope."""
         if name in self.declared_locally or name in self.declared_parameter:
             return True
-        if local_only:
-            return False
         return name in self.declared
 
     def copy(self):
@@ -140,7 +140,8 @@ class Identifiers(object):
 class Frame(object):
     """Holds compile time information for us."""
 
-    def __init__(self, parent=None):
+    def __init__(self, eval_ctx, parent=None):
+        self.eval_ctx = eval_ctx
         self.identifiers = Identifiers()
 
         # a toplevel frame is the root + soft frames such as if conditions.
@@ -190,12 +191,12 @@ class Frame(object):
         rv.identifiers.__dict__.update(self.identifiers.__dict__)
         return rv
 
-    def inspect(self, nodes, hard_scope=False):
+    def inspect(self, nodes):
         """Walk the node and check for identifiers.  If the scope is hard (eg:
         enforce on a python level) overrides from outer scopes are tracked
         differently.
         """
-        visitor = FrameIdentifierVisitor(self.identifiers, hard_scope)
+        visitor = FrameIdentifierVisitor(self.identifiers)
         for node in nodes:
             visitor.visit(node)
 
@@ -210,7 +211,7 @@ class Frame(object):
 
     def inner(self):
         """Return an inner frame."""
-        return Frame(self)
+        return Frame(self.eval_ctx, self)
 
     def soft(self):
         """Return a soft frame.  A soft frame may not be modified as
@@ -272,9 +273,8 @@ class UndeclaredNameVisitor(NodeVisitor):
 class FrameIdentifierVisitor(NodeVisitor):
     """A visitor for `Frame.inspect`."""
 
-    def __init__(self, identifiers, hard_scope):
+    def __init__(self, identifiers):
         self.identifiers = identifiers
-        self.hard_scope = hard_scope
 
     def visit_Name(self, node):
         """All assignments to names go through this function."""
@@ -283,15 +283,14 @@ class FrameIdentifierVisitor(NodeVisitor):
         elif node.ctx == 'param':
             self.identifiers.declared_parameter.add(node.name)
         elif node.ctx == 'load' and not \
-             self.identifiers.is_declared(node.name, self.hard_scope):
+             self.identifiers.is_declared(node.name):
             self.identifiers.undeclared.add(node.name)
 
     def visit_If(self, node):
         self.visit(node.test)
         real_identifiers = self.identifiers
 
-        old_names = real_identifiers.declared | \
-                    real_identifiers.declared_locally | \
+        old_names = real_identifiers.declared_locally | \
                     real_identifiers.declared_parameter
 
         def inner_visit(nodes):
@@ -312,7 +311,8 @@ class FrameIdentifierVisitor(NodeVisitor):
 
         # the differences between the two branches are also pulled as
         # undeclared variables
-        real_identifiers.undeclared.update(body.symmetric_difference(else_))
+        real_identifiers.undeclared.update(body.symmetric_difference(else_) -
+                                           real_identifiers.declared)
 
         # remember those that are declared.
         real_identifiers.declared_locally.update(body | else_)
@@ -365,7 +365,8 @@ class CompilerExit(Exception):
 
 class CodeGenerator(NodeVisitor):
 
-    def __init__(self, environment, name, filename, stream=None):
+    def __init__(self, environment, name, filename, stream=None,
+                 defer_init=False):
         if stream is None:
             stream = StringIO()
         self.environment = environment
@@ -373,6 +374,7 @@ class CodeGenerator(NodeVisitor):
         self.filename = filename
         self.stream = stream
         self.created_block_context = False
+        self.defer_init = defer_init
 
         # aliases for imports
         self.import_aliases = {}
@@ -419,7 +421,7 @@ class CodeGenerator(NodeVisitor):
     # -- Various compilation helpers
 
     def fail(self, msg, lineno):
-        """Fail with a `TemplateAssertionError`."""
+        """Fail with a :exc:`TemplateAssertionError`."""
         raise TemplateAssertionError(msg, lineno, self.name, self.filename)
 
     def temporary_identifier(self):
@@ -434,7 +436,16 @@ class CodeGenerator(NodeVisitor):
 
     def return_buffer_contents(self, frame):
         """Return the buffer contents of the frame."""
-        if self.environment.autoescape:
+        if frame.eval_ctx.volatile:
+            self.writeline('if context.eval_ctx.autoescape:')
+            self.indent()
+            self.writeline('return Markup(concat(%s))' % frame.buffer)
+            self.outdent()
+            self.writeline('else:')
+            self.indent()
+            self.writeline('return concat(%s)' % frame.buffer)
+            self.outdent()
+        elif frame.eval_ctx.autoescape:
             self.writeline('return Markup(concat(%s))' % frame.buffer)
         else:
             self.writeline('return concat(%s)' % frame.buffer)
@@ -644,7 +655,7 @@ class CodeGenerator(NodeVisitor):
             children = node.iter_child_nodes()
         children = list(children)
         func_frame = frame.inner()
-        func_frame.inspect(children, hard_scope=True)
+        func_frame.inspect(children)
 
         # variables that are undeclared (accessed before declaration) and
         # declared locally *and* part of an outside scope raise a template
@@ -747,12 +758,18 @@ class CodeGenerator(NodeVisitor):
 
     def visit_Template(self, node, frame=None):
         assert frame is None, 'no root frame allowed'
+        eval_ctx = EvalContext(self.environment, self.name)
+
         from jinja2.runtime import __all__ as exported
         self.writeline('from __future__ import division')
         self.writeline('from jinja2.runtime import ' + ', '.join(exported))
         if not unoptimize_before_dead_code:
             self.writeline('dummy = lambda *x: None')
 
+        # if we want a deferred initialization we cannot move the
+        # environment into a local name
+        envenv = not self.defer_init and ', environment=environment' or ''
+
         # do we have an extends tag at all?  If not, we can save some
         # overhead by just not processing any inheritance code.
         have_extends = node.find(nodes.Extends) is not None
@@ -779,10 +796,10 @@ class CodeGenerator(NodeVisitor):
         self.writeline('name = %r' % self.name)
 
         # generate the root render function.
-        self.writeline('def root(context, environment=environment):', extra=1)
+        self.writeline('def root(context%s):' % envenv, extra=1)
 
         # process the root
-        frame = Frame()
+        frame = Frame(eval_ctx)
         frame.inspect(node.body)
         frame.toplevel = frame.rootlevel = True
         frame.require_output_check = have_extends and not self.has_known_extends
@@ -811,11 +828,11 @@ class CodeGenerator(NodeVisitor):
 
         # at this point we now have the blocks collected and can visit them too.
         for name, block in self.blocks.iteritems():
-            block_frame = Frame()
+            block_frame = Frame(eval_ctx)
             block_frame.inspect(block.body)
             block_frame.block = name
-            self.writeline('def block_%s(context, environment=environment):'
-                           % name, block, 1)
+            self.writeline('def block_%s(context%s):' % (name, envenv),
+                           block, 1)
             self.indent()
             undeclared = find_undeclared(block.body, ('self', 'super'))
             if 'self' in undeclared:
@@ -1069,7 +1086,7 @@ class CodeGenerator(NodeVisitor):
            node.iter_child_nodes(only=('else_', 'test')), ('loop',)):
             self.writeline("l_loop = environment.undefined(%r, name='loop')" %
                 ("'loop' is undefined. the filter section of a loop as well "
-                 "as the else block doesn't have access to the special 'loop'"
+                 "as the else block don't have access to the special 'loop'"
                  " variable of the current loop.  Because there is no parent "
                  "loop it's undefined.  Happened in loop on %s" %
                  self.position(node)))
@@ -1203,8 +1220,6 @@ class CodeGenerator(NodeVisitor):
         else:
             finalize = unicode
 
-        self.newline(node)
-
         # if we are inside a frame that requires output checking, we do so
         outdent_later = False
         if frame.require_output_check:
@@ -1217,18 +1232,21 @@ class CodeGenerator(NodeVisitor):
         body = []
         for child in node.nodes:
             try:
-                const = child.as_const()
+                const = child.as_const(frame.eval_ctx)
             except nodes.Impossible:
                 body.append(child)
                 continue
+            # the frame can't be volatile here, becaus otherwise the
+            # as_const() function would raise an Impossible exception
+            # at that point.
             try:
-                if self.environment.autoescape:
+                if frame.eval_ctx.autoescape:
                     if hasattr(const, '__html__'):
                         const = const.__html__()
                     else:
                         const = escape(const)
                 const = finalize(const)
-            except:
+            except Exception:
                 # if something goes wrong here we evaluate the node
                 # at runtime for easier debugging
                 body.append(child)
@@ -1260,7 +1278,10 @@ class CodeGenerator(NodeVisitor):
                     else:
                         self.newline(item)
                     close = 1
-                    if self.environment.autoescape:
+                    if frame.eval_ctx.volatile:
+                        self.write('(context.eval_ctx.autoescape and'
+                                   ' escape or to_string)(')
+                    elif frame.eval_ctx.autoescape:
                         self.write('escape(')
                     else:
                         self.write('to_string(')
@@ -1293,7 +1314,11 @@ class CodeGenerator(NodeVisitor):
             for argument in arguments:
                 self.newline(argument)
                 close = 0
-                if self.environment.autoescape:
+                if frame.eval_ctx.volatile:
+                    self.write('(context.eval_ctx.autoescape and'
+                               ' escape or to_string)(')
+                    close += 1
+                elif frame.eval_ctx.autoescape:
                     self.write('escape(')
                     close += 1
                 if self.environment.finalize is not None:
@@ -1360,7 +1385,11 @@ class CodeGenerator(NodeVisitor):
             self.write(repr(val))
 
     def visit_TemplateData(self, node, frame):
-        self.write(repr(node.as_const()))
+        try:
+            self.write(repr(node.as_const(frame.eval_ctx)))
+        except nodes.Impossible:
+            self.write('(context.eval_ctx.autoescape and Markup or identity)(%r)'
+                       % node.data)
 
     def visit_Tuple(self, node, frame):
         self.write('(')
@@ -1389,19 +1418,31 @@ class CodeGenerator(NodeVisitor):
             self.visit(item.value, frame)
         self.write('}')
 
-    def binop(operator):
+    def binop(operator, interceptable=True):
         def visitor(self, node, frame):
-            self.write('(')
-            self.visit(node.left, frame)
-            self.write(' %s ' % operator)
-            self.visit(node.right, frame)
+            if self.environment.sandboxed and \
+               operator in self.environment.intercepted_binops:
+                self.write('environment.call_binop(context, %r, ' % operator)
+                self.visit(node.left, frame)
+                self.write(', ')
+                self.visit(node.right, frame)
+            else:
+                self.write('(')
+                self.visit(node.left, frame)
+                self.write(' %s ' % operator)
+                self.visit(node.right, frame)
             self.write(')')
         return visitor
 
-    def uaop(operator):
+    def uaop(operator, interceptable=True):
         def visitor(self, node, frame):
-            self.write('(' + operator)
-            self.visit(node.node, frame)
+            if self.environment.sandboxed and \
+               operator in self.environment.intercepted_unops:
+                self.write('environment.call_unop(context, %r, ' % operator)
+                self.visit(node.node, frame)
+            else:
+                self.write('(' + operator)
+                self.visit(node.node, frame)
             self.write(')')
         return visitor
 
@@ -1412,16 +1453,22 @@ class CodeGenerator(NodeVisitor):
     visit_FloorDiv = binop('//')
     visit_Pow = binop('**')
     visit_Mod = binop('%')
-    visit_And = binop('and')
-    visit_Or = binop('or')
+    visit_And = binop('and', interceptable=False)
+    visit_Or = binop('or', interceptable=False)
     visit_Pos = uaop('+')
     visit_Neg = uaop('-')
-    visit_Not = uaop('not ')
+    visit_Not = uaop('not ', interceptable=False)
     del binop, uaop
 
     def visit_Concat(self, node, frame):
-        self.write('%s((' % (self.environment.autoescape and
-                             'markup_join' or 'unicode_join'))
+        if frame.eval_ctx.volatile:
+            func_name = '(context.eval_ctx.volatile and' \
+                        ' markup_join or unicode_join)'
+        elif frame.eval_ctx.autoescape:
+            func_name = 'markup_join'
+        else:
+            func_name = 'unicode_join'
+        self.write('%s((' % func_name)
         for arg in node.nodes:
             self.visit(arg, frame)
             self.write(', ')
@@ -1472,6 +1519,8 @@ class CodeGenerator(NodeVisitor):
             self.fail('no filter named %r' % node.name, node.lineno)
         if getattr(func, 'contextfilter', False):
             self.write('context, ')
+        elif getattr(func, 'evalcontextfilter', False):
+            self.write('context.eval_ctx, ')
         elif getattr(func, 'environmentfilter', False):
             self.write('environment, ')
 
@@ -1479,7 +1528,11 @@ class CodeGenerator(NodeVisitor):
         # and want to write to the current buffer
         if node.node is not None:
             self.visit(node.node, frame)
-        elif self.environment.autoescape:
+        elif frame.eval_ctx.volatile:
+            self.write('(context.eval_ctx.autoescape and'
+                       ' Markup(concat(%s)) or concat(%s))' %
+                       (frame.buffer, frame.buffer))
+        elif frame.eval_ctx.autoescape:
             self.write('Markup(concat(%s))' % frame.buffer)
         else:
             self.write('concat(%s)' % frame.buffer)
@@ -1540,6 +1593,11 @@ class CodeGenerator(NodeVisitor):
         self.visit(node.expr, frame)
         self.write(')')
 
+    def visit_MarkSafeIfAutoescape(self, node, frame):
+        self.write('(context.eval_ctx.autoescape and Markup or identity)(')
+        self.visit(node.expr, frame)
+        self.write(')')
+
     def visit_EnvironmentAttribute(self, node, frame):
         self.write('environment.' + node.name)
 
@@ -1568,3 +1626,24 @@ class CodeGenerator(NodeVisitor):
         self.pull_locals(scope_frame)
         self.blockvisit(node.body, scope_frame)
         self.pop_scope(aliases, scope_frame)
+
+    def visit_EvalContextModifier(self, node, frame):
+        for keyword in node.options:
+            self.writeline('context.eval_ctx.%s = ' % keyword.key)
+            self.visit(keyword.value, frame)
+            try:
+                val = keyword.value.as_const(frame.eval_ctx)
+            except nodes.Impossible:
+                frame.eval_ctx.volatile = True
+            else:
+                setattr(frame.eval_ctx, keyword.key, val)
+
+    def visit_ScopedEvalContextModifier(self, node, frame):
+        old_ctx_name = self.temporary_identifier()
+        safed_ctx = frame.eval_ctx.save()
+        self.writeline('%s = context.eval_ctx.save()' % old_ctx_name)
+        self.visit_EvalContextModifier(node, frame)
+        for child in node.body:
+            self.visit(child, frame)
+        frame.eval_ctx.revert(safed_ctx)
+        self.writeline('context.eval_ctx.revert(%s)' % old_ctx_name)
index d83e44b..cab203c 100644 (file)
@@ -30,261 +30,3 @@ sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor
 tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices
 ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus
 viverra volutpat vulputate'''
-
-
-#: a dict of all html entities + apos
-HTML_ENTITIES = {
-    'AElig': 198,
-    'Aacute': 193,
-    'Acirc': 194,
-    'Agrave': 192,
-    'Alpha': 913,
-    'Aring': 197,
-    'Atilde': 195,
-    'Auml': 196,
-    'Beta': 914,
-    'Ccedil': 199,
-    'Chi': 935,
-    'Dagger': 8225,
-    'Delta': 916,
-    'ETH': 208,
-    'Eacute': 201,
-    'Ecirc': 202,
-    'Egrave': 200,
-    'Epsilon': 917,
-    'Eta': 919,
-    'Euml': 203,
-    'Gamma': 915,
-    'Iacute': 205,
-    'Icirc': 206,
-    'Igrave': 204,
-    'Iota': 921,
-    'Iuml': 207,
-    'Kappa': 922,
-    'Lambda': 923,
-    'Mu': 924,
-    'Ntilde': 209,
-    'Nu': 925,
-    'OElig': 338,
-    'Oacute': 211,
-    'Ocirc': 212,
-    'Ograve': 210,
-    'Omega': 937,
-    'Omicron': 927,
-    'Oslash': 216,
-    'Otilde': 213,
-    'Ouml': 214,
-    'Phi': 934,
-    'Pi': 928,
-    'Prime': 8243,
-    'Psi': 936,
-    'Rho': 929,
-    'Scaron': 352,
-    'Sigma': 931,
-    'THORN': 222,
-    'Tau': 932,
-    'Theta': 920,
-    'Uacute': 218,
-    'Ucirc': 219,
-    'Ugrave': 217,
-    'Upsilon': 933,
-    'Uuml': 220,
-    'Xi': 926,
-    'Yacute': 221,
-    'Yuml': 376,
-    'Zeta': 918,
-    'aacute': 225,
-    'acirc': 226,
-    'acute': 180,
-    'aelig': 230,
-    'agrave': 224,
-    'alefsym': 8501,
-    'alpha': 945,
-    'amp': 38,
-    'and': 8743,
-    'ang': 8736,
-    'apos': 39,
-    'aring': 229,
-    'asymp': 8776,
-    'atilde': 227,
-    'auml': 228,
-    'bdquo': 8222,
-    'beta': 946,
-    'brvbar': 166,
-    'bull': 8226,
-    'cap': 8745,
-    'ccedil': 231,
-    'cedil': 184,
-    'cent': 162,
-    'chi': 967,
-    'circ': 710,
-    'clubs': 9827,
-    'cong': 8773,
-    'copy': 169,
-    'crarr': 8629,
-    'cup': 8746,
-    'curren': 164,
-    'dArr': 8659,
-    'dagger': 8224,
-    'darr': 8595,
-    'deg': 176,
-    'delta': 948,
-    'diams': 9830,
-    'divide': 247,
-    'eacute': 233,
-    'ecirc': 234,
-    'egrave': 232,
-    'empty': 8709,
-    'emsp': 8195,
-    'ensp': 8194,
-    'epsilon': 949,
-    'equiv': 8801,
-    'eta': 951,
-    'eth': 240,
-    'euml': 235,
-    'euro': 8364,
-    'exist': 8707,
-    'fnof': 402,
-    'forall': 8704,
-    'frac12': 189,
-    'frac14': 188,
-    'frac34': 190,
-    'frasl': 8260,
-    'gamma': 947,
-    'ge': 8805,
-    'gt': 62,
-    'hArr': 8660,
-    'harr': 8596,
-    'hearts': 9829,
-    'hellip': 8230,
-    'iacute': 237,
-    'icirc': 238,
-    'iexcl': 161,
-    'igrave': 236,
-    'image': 8465,
-    'infin': 8734,
-    'int': 8747,
-    'iota': 953,
-    'iquest': 191,
-    'isin': 8712,
-    'iuml': 239,
-    'kappa': 954,
-    'lArr': 8656,
-    'lambda': 955,
-    'lang': 9001,
-    'laquo': 171,
-    'larr': 8592,
-    'lceil': 8968,
-    'ldquo': 8220,
-    'le': 8804,
-    'lfloor': 8970,
-    'lowast': 8727,
-    'loz': 9674,
-    'lrm': 8206,
-    'lsaquo': 8249,
-    'lsquo': 8216,
-    'lt': 60,
-    'macr': 175,
-    'mdash': 8212,
-    'micro': 181,
-    'middot': 183,
-    'minus': 8722,
-    'mu': 956,
-    'nabla': 8711,
-    'nbsp': 160,
-    'ndash': 8211,
-    'ne': 8800,
-    'ni': 8715,
-    'not': 172,
-    'notin': 8713,
-    'nsub': 8836,
-    'ntilde': 241,
-    'nu': 957,
-    'oacute': 243,
-    'ocirc': 244,
-    'oelig': 339,
-    'ograve': 242,
-    'oline': 8254,
-    'omega': 969,
-    'omicron': 959,
-    'oplus': 8853,
-    'or': 8744,
-    'ordf': 170,
-    'ordm': 186,
-    'oslash': 248,
-    'otilde': 245,
-    'otimes': 8855,
-    'ouml': 246,
-    'para': 182,
-    'part': 8706,
-    'permil': 8240,
-    'perp': 8869,
-    'phi': 966,
-    'pi': 960,
-    'piv': 982,
-    'plusmn': 177,
-    'pound': 163,
-    'prime': 8242,
-    'prod': 8719,
-    'prop': 8733,
-    'psi': 968,
-    'quot': 34,
-    'rArr': 8658,
-    'radic': 8730,
-    'rang': 9002,
-    'raquo': 187,
-    'rarr': 8594,
-    'rceil': 8969,
-    'rdquo': 8221,
-    'real': 8476,
-    'reg': 174,
-    'rfloor': 8971,
-    'rho': 961,
-    'rlm': 8207,
-    'rsaquo': 8250,
-    'rsquo': 8217,
-    'sbquo': 8218,
-    'scaron': 353,
-    'sdot': 8901,
-    'sect': 167,
-    'shy': 173,
-    'sigma': 963,
-    'sigmaf': 962,
-    'sim': 8764,
-    'spades': 9824,
-    'sub': 8834,
-    'sube': 8838,
-    'sum': 8721,
-    'sup': 8835,
-    'sup1': 185,
-    'sup2': 178,
-    'sup3': 179,
-    'supe': 8839,
-    'szlig': 223,
-    'tau': 964,
-    'there4': 8756,
-    'theta': 952,
-    'thetasym': 977,
-    'thinsp': 8201,
-    'thorn': 254,
-    'tilde': 732,
-    'times': 215,
-    'trade': 8482,
-    'uArr': 8657,
-    'uacute': 250,
-    'uarr': 8593,
-    'ucirc': 251,
-    'ugrave': 249,
-    'uml': 168,
-    'upsih': 978,
-    'upsilon': 965,
-    'uuml': 252,
-    'weierp': 8472,
-    'xi': 958,
-    'yacute': 253,
-    'yen': 165,
-    'yuml': 255,
-    'zeta': 950,
-    'zwj': 8205,
-    'zwnj': 8204
-}
index c2bd07b..2af2222 100644 (file)
 """
 import sys
 import traceback
+from types import TracebackType
 from jinja2.utils import CodeType, missing, internal_code
 from jinja2.exceptions import TemplateSyntaxError
 
+# on pypy we can take advantage of transparent proxies
+try:
+    from __pypy__ import tproxy
+except ImportError:
+    tproxy = None
+
 
 # how does the raise helper look like?
 try:
@@ -30,17 +37,22 @@ class TracebackFrameProxy(object):
 
     def __init__(self, tb):
         self.tb = tb
+        self._tb_next = None
 
-    def _set_tb_next(self, next):
-        if tb_set_next is not None:
-            tb_set_next(self.tb, next and next.tb or None)
-        self._tb_next = next
-
-    def _get_tb_next(self):
+    @property
+    def tb_next(self):
         return self._tb_next
 
-    tb_next = property(_get_tb_next, _set_tb_next)
-    del _get_tb_next, _set_tb_next
+    def set_next(self, next):
+        if tb_set_next is not None:
+            try:
+                tb_set_next(self.tb, next and next.tb or None)
+            except Exception:
+                # this function can fail due to all the hackery it does
+                # on various python implementations.  We just catch errors
+                # down and ignore them if necessary.
+                pass
+        self._tb_next = next
 
     @property
     def is_jinja_frame(self):
@@ -50,6 +62,20 @@ class TracebackFrameProxy(object):
         return getattr(self.tb, name)
 
 
+def make_frame_proxy(frame):
+    proxy = TracebackFrameProxy(frame)
+    if tproxy is None:
+        return proxy
+    def operation_handler(operation, *args, **kwargs):
+        if operation in ('__getattribute__', '__getattr__'):
+            return getattr(proxy, args[0])
+        elif operation == '__setattr__':
+            proxy.__setattr__(*args, **kwargs)
+        else:
+            return getattr(proxy, operation)(*args, **kwargs)
+    return tproxy(TracebackType, operation_handler)
+
+
 class ProcessedTraceback(object):
     """Holds a Jinja preprocessed traceback for priting or reraising."""
 
@@ -59,14 +85,13 @@ class ProcessedTraceback(object):
         self.exc_value = exc_value
         self.frames = frames
 
-    def chain_frames(self):
-        """Chains the frames.  Requires ctypes or the speedups extension."""
+        # newly concatenate the frames (which are proxies)
         prev_tb = None
         for tb in self.frames:
             if prev_tb is not None:
-                prev_tb.tb_next = tb
+                prev_tb.set_next(tb)
             prev_tb = tb
-        prev_tb.tb_next = None
+        prev_tb.set_next(None)
 
     def render_as_text(self, limit=None):
         """Return a string with the traceback."""
@@ -95,7 +120,12 @@ class ProcessedTraceback(object):
     @property
     def standard_exc_info(self):
         """Standard python exc_info for re-raising"""
-        return self.exc_type, self.exc_value, self.frames[0].tb
+        tb = self.frames[0]
+        # the frame will be an actual traceback (or transparent proxy) if
+        # we are on pypy or a python implementation with support for tproxy
+        if type(tb) is not TracebackType:
+            tb = tb.tb
+        return self.exc_type, self.exc_value, tb
 
 
 def make_traceback(exc_info, source_hint=None):
@@ -152,7 +182,7 @@ def translate_exception(exc_info, initial_skip=0):
             tb = fake_exc_info(exc_info[:2] + (tb,), template.filename,
                                lineno)[2]
 
-        frames.append(TracebackFrameProxy(tb))
+        frames.append(make_frame_proxy(tb))
         tb = next
 
     # if we don't have any exceptions in the frames left, we have to
@@ -161,10 +191,7 @@ def translate_exception(exc_info, initial_skip=0):
     if not frames:
         raise exc_info[0], exc_info[1], exc_info[2]
 
-    traceback = ProcessedTraceback(exc_info[0], exc_info[1], frames)
-    if tb_set_next is not None:
-        traceback.chain_frames()
-    return traceback
+    return ProcessedTraceback(exc_info[0], exc_info[1], frames)
 
 
 def fake_exc_info(exc_info, filename, lineno):
@@ -239,7 +266,8 @@ def fake_exc_info(exc_info, filename, lineno):
 def _init_ugly_crap():
     """This function implements a few ugly things so that we can patch the
     traceback objects.  The function returned allows resetting `tb_next` on
-    any python traceback object.
+    any python traceback object.  Do not attempt to use this on non cpython
+    interpreters
     """
     import ctypes
     from types import TracebackType
@@ -259,7 +287,7 @@ def _init_ugly_crap():
     ]
 
     # python with trace
-    if object.__basicsize__ != ctypes.sizeof(_PyObject):
+    if hasattr(sys, 'getobjects'):
         class _PyObject(ctypes.Structure):
             pass
         _PyObject._fields_ = [
@@ -297,12 +325,15 @@ def _init_ugly_crap():
     return tb_set_next
 
 
-# try to get a tb_set_next implementation
-try:
-    from jinja2._speedups import tb_set_next
-except ImportError:
+# try to get a tb_set_next implementation if we don't have transparent
+# proxies.
+tb_set_next = None
+if tproxy is None:
     try:
-        tb_set_next = _init_ugly_crap()
-    except:
-        tb_set_next = None
-del _init_ugly_crap
+        from jinja2._debugsupport import tb_set_next
+    except ImportError:
+        try:
+            tb_set_next = _init_ugly_crap()
+        except:
+            pass
+    del _init_ugly_crap
index 96d20b2..d2d4544 100644 (file)
@@ -24,17 +24,11 @@ TRIM_BLOCKS = False
 NEWLINE_SEQUENCE = '\n'
 
 
-try:
-    range_func = xrange
-except NameError:
-    range_func = range
-
-
 # default filters, tests and namespace
 from jinja2.filters import FILTERS as DEFAULT_FILTERS
 from jinja2.tests import TESTS as DEFAULT_TESTS
 DEFAULT_NAMESPACE = {
-    'range':        range_func,
+    'range':        xrange,
     'dict':         lambda **kw: kw,
     'lipsum':       generate_lorem_ipsum,
     'cycler':       Cycler,
index e3c64e3..7a9a59f 100644 (file)
@@ -8,6 +8,7 @@
     :copyright: (c) 2010 by the Jinja Team.
     :license: BSD, see LICENSE for more details.
 """
+import os
 import sys
 from jinja2 import nodes
 from jinja2.defaults import *
@@ -157,9 +158,15 @@ class Environment(object):
             `None` implicitly into an empty string here.
 
         `autoescape`
-            If set to true the XML/HTML autoescaping feature is enabled.
-            For more details about auto escaping see
-            :class:`~jinja2.utils.Markup`.
+            If set to true the XML/HTML autoescaping feature is enabled by
+            default.  For more details about auto escaping see
+            :class:`~jinja2.utils.Markup`.  As of Jinja 2.4 this can also
+            be a callable that is passed the template name and has to
+            return `True` or `False` depending on autoescape should be
+            enabled by default.
+
+            .. versionchanged:: 2.4
+               `autoescape` can now be a function
 
         `loader`
             The template loader for this environment.
@@ -189,7 +196,8 @@ class Environment(object):
 
     #: if this environment is sandboxed.  Modifying this variable won't make
     #: the environment sandboxed though.  For a real sandboxed environment
-    #: have a look at jinja2.sandbox
+    #: have a look at jinja2.sandbox.  This flag alone controls the code
+    #: generation by the compiler.
     sandboxed = False
 
     #: True if the environment is just an overlay
@@ -272,6 +280,13 @@ class Environment(object):
 
         _environment_sanity_check(self)
 
+    def add_extension(self, extension):
+        """Adds an extension after the environment was created.
+
+        .. versionadded:: 2.5
+        """
+        self.extensions.update(load_extensions(self, [extension]))
+
     def extend(self, **attributes):
         """Add the items to the instance of the environment if they do not exist
         yet.  This is used by :ref:`extensions <writing-extensions>` to register
@@ -321,12 +336,17 @@ class Environment(object):
         for key, value in self.extensions.iteritems():
             rv.extensions[key] = value.bind(rv)
         if extensions is not missing:
-            rv.extensions.update(load_extensions(extensions))
+            rv.extensions.update(load_extensions(rv, extensions))
 
         return _environment_sanity_check(rv)
 
     lexer = property(get_lexer, doc="The lexer for this environment.")
 
+    def iter_extensions(self):
+        """Iterates over the extensions by priority."""
+        return iter(sorted(self.extensions.values(),
+                           key=lambda x: x.priority))
+
     def getitem(self, obj, argument):
         """Get an item or attribute of an object but prefer the item."""
         try:
@@ -335,7 +355,7 @@ class Environment(object):
             if isinstance(argument, basestring):
                 try:
                     attr = str(argument)
-                except:
+                except Exception:
                     pass
                 else:
                     try:
@@ -400,7 +420,7 @@ class Environment(object):
         because there you usually only want the actual source tokenized.
         """
         return reduce(lambda s, e: e.preprocess(s, name, filename),
-                      self.extensions.itervalues(), unicode(source))
+                      self.iter_extensions(), unicode(source))
 
     def _tokenize(self, source, name, filename=None, state=None):
         """Called by the parser to do the preprocessing and filtering
@@ -408,14 +428,31 @@ class Environment(object):
         """
         source = self.preprocess(source, name, filename)
         stream = self.lexer.tokenize(source, name, filename, state)
-        for ext in self.extensions.itervalues():
+        for ext in self.iter_extensions():
             stream = ext.filter_stream(stream)
             if not isinstance(stream, TokenStream):
                 stream = TokenStream(stream, name, filename)
         return stream
 
+    def _generate(self, source, name, filename, defer_init=False):
+        """Internal hook that can be overriden to hook a different generate
+        method in.
+
+        .. versionadded:: 2.5
+        """
+        return generate(source, self, name, filename, defer_init=defer_init)
+
+    def _compile(self, source, filename):
+        """Internal hook that can be overriden to hook a different compile
+        method in.
+
+        .. versionadded:: 2.5
+        """
+        return compile(source, filename, 'exec')
+
     @internalcode
-    def compile(self, source, name=None, filename=None, raw=False):
+    def compile(self, source, name=None, filename=None, raw=False,
+                defer_init=False):
         """Compile a node or template source code.  The `name` parameter is
         the load name of the template after it was joined using
         :meth:`join_path` if necessary, not the filename on the file system.
@@ -427,6 +464,13 @@ class Environment(object):
         parameter is `True` the return value will be a string with python
         code equivalent to the bytecode returned otherwise.  This method is
         mainly used internally.
+
+        `defer_init` is use internally to aid the module code generator.  This
+        causes the generated code to be able to import without the global
+        environment variable to be set.
+
+        .. versionadded:: 2.4
+           `defer_init` parameter added.
         """
         source_hint = None
         try:
@@ -435,14 +479,15 @@ class Environment(object):
                 source = self._parse(source, name, filename)
             if self.optimized:
                 source = optimize(source, self)
-            source = generate(source, self, name, filename)
+            source = self._generate(source, name, filename,
+                                    defer_init=defer_init)
             if raw:
                 return source
             if filename is None:
                 filename = '<template>'
             else:
                 filename = _encode_filename(filename)
-            return compile(source, filename, 'exec')
+            return self._compile(source, filename)
         except TemplateSyntaxError:
             exc_info = sys.exc_info()
         self.handle_exception(exc_info, source_hint=source)
@@ -483,6 +528,7 @@ class Environment(object):
                 raise TemplateSyntaxError('chunk after expression',
                                           parser.stream.current.lineno,
                                           None, None)
+            expr.set_environment(self)
         except TemplateSyntaxError:
             exc_info = sys.exc_info()
         if exc_info is not None:
@@ -491,6 +537,116 @@ class Environment(object):
         template = self.from_string(nodes.Template(body, lineno=1))
         return TemplateExpression(template, undefined_to_none)
 
+    def compile_templates(self, target, extensions=None, filter_func=None,
+                          zip='deflated', log_function=None,
+                          ignore_errors=True, py_compile=False):
+        """Finds all the templates the loader can find, compiles them
+        and stores them in `target`.  If `zip` is `None`, instead of in a
+        zipfile, the templates will be will be stored in a directory.
+        By default a deflate zip algorithm is used, to switch to
+        the stored algorithm, `zip` can be set to ``'stored'``.
+
+        `extensions` and `filter_func` are passed to :meth:`list_templates`.
+        Each template returned will be compiled to the target folder or
+        zipfile.
+
+        By default template compilation errors are ignored.  In case a
+        log function is provided, errors are logged.  If you want template
+        syntax errors to abort the compilation you can set `ignore_errors`
+        to `False` and you will get an exception on syntax errors.
+
+        If `py_compile` is set to `True` .pyc files will be written to the
+        target instead of standard .py files.
+
+        .. versionadded:: 2.4
+        """
+        from jinja2.loaders import ModuleLoader
+
+        if log_function is None:
+            log_function = lambda x: None
+
+        if py_compile:
+            import imp, marshal
+            py_header = imp.get_magic() + \
+                u'\xff\xff\xff\xff'.encode('iso-8859-15')
+
+        def write_file(filename, data, mode):
+            if zip:
+                info = ZipInfo(filename)
+                info.external_attr = 0755 << 16L
+                zip_file.writestr(info, data)
+            else:
+                f = open(os.path.join(target, filename), mode)
+                try:
+                    f.write(data)
+                finally:
+                    f.close()
+
+        if zip is not None:
+            from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED, ZIP_STORED
+            zip_file = ZipFile(target, 'w', dict(deflated=ZIP_DEFLATED,
+                                                 stored=ZIP_STORED)[zip])
+            log_function('Compiling into Zip archive "%s"' % target)
+        else:
+            if not os.path.isdir(target):
+                os.makedirs(target)
+            log_function('Compiling into folder "%s"' % target)
+
+        try:
+            for name in self.list_templates(extensions, filter_func):
+                source, filename, _ = self.loader.get_source(self, name)
+                try:
+                    code = self.compile(source, name, filename, True, True)
+                except TemplateSyntaxError, e:
+                    if not ignore_errors:
+                        raise
+                    log_function('Could not compile "%s": %s' % (name, e))
+                    continue
+
+                filename = ModuleLoader.get_module_filename(name)
+
+                if py_compile:
+                    c = self._compile(code, _encode_filename(filename))
+                    write_file(filename + 'c', py_header +
+                               marshal.dumps(c), 'wb')
+                    log_function('Byte-compiled "%s" as %s' %
+                                 (name, filename + 'c'))
+                else:
+                    write_file(filename, code, 'w')
+                    log_function('Compiled "%s" as %s' % (name, filename))
+        finally:
+            if zip:
+                zip_file.close()
+
+        log_function('Finished compiling templates')
+
+    def list_templates(self, extensions=None, filter_func=None):
+        """Returns a list of templates for this environment.  This requires
+        that the loader supports the loader's
+        :meth:`~BaseLoader.list_templates` method.
+
+        If there are other files in the template folder besides the
+        actual templates, the returned list can be filtered.  There are two
+        ways: either `extensions` is set to a list of file extensions for
+        templates, or a `filter_func` can be provided which is a callable that
+        is passed a template name and should return `True` if it should end up
+        in the result list.
+
+        If the loader does not support that, a :exc:`TypeError` is raised.
+
+        .. versionadded:: 2.4
+        """
+        x = self.loader.list_templates()
+        if extensions is not None:
+            if filter_func is not None:
+                raise TypeError('either extensions or filter_func '
+                                'can be passed, but not both')
+            filter_func = lambda x: '.' in x and \
+                                    x.rsplit('.', 1)[1] in extensions
+        if filter_func is not None:
+            x = filter(filter_func, x)
+        return x
+
     def handle_exception(self, exc_info=None, rendered=False, source_hint=None):
         """Exception handling helper.  This is used internally to either raise
         rewritten exceptions or return a rendered traceback for the template.
@@ -551,7 +707,13 @@ class Environment(object):
 
         If the template does not exist a :exc:`TemplateNotFound` exception is
         raised.
+
+        .. versionchanged:: 2.4
+           If `name` is a :class:`Template` object it is returned from the
+           function unchanged.
         """
+        if isinstance(name, Template):
+            return name
         if parent is not None:
             name = self.join_path(name, parent)
         return self._load_template(name, self.make_globals(globals))
@@ -563,12 +725,18 @@ class Environment(object):
         raise a :exc:`TemplatesNotFound` exception.
 
         .. versionadded:: 2.3
+
+        .. versionchanged:: 2.4
+           If `names` contains a :class:`Template` object it is returned
+           from the function unchanged.
         """
         if not names:
             raise TemplatesNotFound(message=u'Tried to select from an empty list '
                                             u'of templates.')
         globals = self.make_globals(globals)
         for name in names:
+            if isinstance(name, Template):
+                return name
             if parent is not None:
                 name = self.join_path(name, parent)
             try:
@@ -580,14 +748,16 @@ class Environment(object):
     @internalcode
     def get_or_select_template(self, template_name_or_list,
                                parent=None, globals=None):
-        """
-        Does a typecheck and dispatches to :meth:`select_template` if an
-        iterable of template names is given, otherwise to :meth:`get_template`.
+        """Does a typecheck and dispatches to :meth:`select_template`
+        if an iterable of template names is given, otherwise to
+        :meth:`get_template`.
 
         .. versionadded:: 2.3
         """
         if isinstance(template_name_or_list, basestring):
             return self.get_template(template_name_or_list, parent, globals)
+        elif isinstance(template_name_or_list, Template):
+            return template_name_or_list
         return self.select_template(template_name_or_list, parent, globals)
 
     def from_string(self, source, globals=None, template_class=None):
@@ -665,16 +835,31 @@ class Template(object):
         """Creates a template object from compiled code and the globals.  This
         is used by the loaders and environment to create a template object.
         """
-        t = object.__new__(cls)
         namespace = {
-            'environment':          environment,
-            '__jinja_template__':   t
+            'environment':  environment,
+            '__file__':     code.co_filename
         }
         exec code in namespace
+        rv = cls._from_namespace(environment, namespace, globals)
+        rv._uptodate = uptodate
+        return rv
+
+    @classmethod
+    def from_module_dict(cls, environment, module_dict, globals):
+        """Creates a template object from a module.  This is used by the
+        module loader to create a template object.
+
+        .. versionadded:: 2.4
+        """
+        return cls._from_namespace(environment, module_dict, globals)
+
+    @classmethod
+    def _from_namespace(cls, environment, namespace, globals):
+        t = object.__new__(cls)
         t.environment = environment
         t.globals = globals
         t.name = namespace['name']
-        t.filename = code.co_filename
+        t.filename = namespace['__file__']
         t.blocks = namespace['blocks']
 
         # render function and module
@@ -683,7 +868,11 @@ class Template(object):
 
         # debug and loader helpers
         t._debug_info = namespace['debug_info']
-        t._uptodate = uptodate
+        t._uptodate = None
+
+        # store the reference
+        namespace['environment'] = environment
+        namespace['__jinja_template__'] = t
 
         return t
 
@@ -700,7 +889,7 @@ class Template(object):
         vars = dict(*args, **kwargs)
         try:
             return concat(self.root_render_func(self.new_context(vars)))
-        except:
+        except Exception:
             exc_info = sys.exc_info()
         return self.environment.handle_exception(exc_info, True)
 
@@ -722,7 +911,7 @@ class Template(object):
         try:
             for event in self.root_render_func(self.new_context(vars)):
                 yield event
-        except:
+        except Exception:
             exc_info = sys.exc_info()
         else:
             return
index c3c8eec..5ba6efd 100644 (file)
@@ -13,7 +13,7 @@
 from collections import deque
 from jinja2 import nodes
 from jinja2.defaults import *
-from jinja2.environment import get_spontaneous_environment
+from jinja2.environment import Environment
 from jinja2.runtime import Undefined, concat
 from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError
 from jinja2.utils import contextfunction, import_string, Markup, next
@@ -57,6 +57,13 @@ class Extension(object):
     #: if this extension parses this is the list of tags it's listening to.
     tags = set()
 
+    #: the priority of that extension.  This is especially useful for
+    #: extensions that preprocess values.  A lower value means higher
+    #: priority.
+    #:
+    #: .. versionadded:: 2.4
+    priority = 100
+
     def __init__(self, environment):
         self.environment = environment
 
@@ -96,7 +103,9 @@ class Extension(object):
 
     def attr(self, name, lineno=None):
         """Return an attribute node for the current extension.  This is useful
-        to pass constants on extensions to generated template code::
+        to pass constants on extensions to generated template code.
+
+        ::
 
             self.attr('_my_attribute', lineno=lineno)
         """
@@ -116,8 +125,29 @@ class Extension(object):
 
 
 @contextfunction
-def _gettext_alias(context, string):
-    return context.resolve('gettext')(string)
+def _gettext_alias(__context, *args, **kwargs):
+    return __context.call(__context.resolve('gettext'), *args, **kwargs)
+
+
+def _make_new_gettext(func):
+    @contextfunction
+    def gettext(__context, __string, **variables):
+        rv = __context.call(func, __string)
+        if __context.eval_ctx.autoescape:
+            rv = Markup(rv)
+        return rv % variables
+    return gettext
+
+
+def _make_new_ngettext(func):
+    @contextfunction
+    def ngettext(__context, __singular, __plural, __num, **variables):
+        variables.setdefault('num', __num)
+        rv = __context.call(func, __singular, __plural, __num)
+        if __context.eval_ctx.autoescape:
+            rv = Markup(rv)
+        return rv % variables
+    return ngettext
 
 
 class InternationalizationExtension(Extension):
@@ -137,23 +167,37 @@ class InternationalizationExtension(Extension):
         environment.extend(
             install_gettext_translations=self._install,
             install_null_translations=self._install_null,
+            install_gettext_callables=self._install_callables,
             uninstall_gettext_translations=self._uninstall,
-            extract_translations=self._extract
+            extract_translations=self._extract,
+            newstyle_gettext=False
         )
 
-    def _install(self, translations):
+    def _install(self, translations, newstyle=None):
         gettext = getattr(translations, 'ugettext', None)
         if gettext is None:
             gettext = translations.gettext
         ngettext = getattr(translations, 'ungettext', None)
         if ngettext is None:
             ngettext = translations.ngettext
-        self.environment.globals.update(gettext=gettext, ngettext=ngettext)
+        self._install_callables(gettext, ngettext, newstyle)
+
+    def _install_null(self, newstyle=None):
+        self._install_callables(
+            lambda x: x,
+            lambda s, p, n: (n != 1 and (p,) or (s,))[0],
+            newstyle
+        )
 
-    def _install_null(self):
+    def _install_callables(self, gettext, ngettext, newstyle=None):
+        if newstyle is not None:
+            self.environment.newstyle_gettext = newstyle
+        if self.environment.newstyle_gettext:
+            gettext = _make_new_gettext(gettext)
+            ngettext = _make_new_ngettext(ngettext)
         self.environment.globals.update(
-            gettext=lambda x: x,
-            ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0]
+            gettext=gettext,
+            ngettext=ngettext
         )
 
     def _uninstall(self, translations):
@@ -168,6 +212,7 @@ class InternationalizationExtension(Extension):
     def parse(self, parser):
         """Parse a translatable tag."""
         lineno = next(parser.stream).lineno
+        num_called_num = False
 
         # find all the variables referenced.  Additionally a variable can be
         # defined in the body of the trans block too, but this is checked at
@@ -194,8 +239,10 @@ class InternationalizationExtension(Extension):
                 variables[name.value] = var = parser.parse_expression()
             else:
                 variables[name.value] = var = nodes.Name(name.value, 'load')
+
             if plural_expr is None:
                 plural_expr = var
+                num_called_num = name.value == 'num'
 
         parser.stream.expect('block_end')
 
@@ -209,6 +256,7 @@ class InternationalizationExtension(Extension):
             referenced.update(singular_names)
             if plural_expr is None:
                 plural_expr = nodes.Name(singular_names[0], 'load')
+                num_called_num = singular_names[0] == 'num'
 
         # if we have a pluralize block, we parse that too
         if parser.stream.current.test('name:pluralize'):
@@ -221,6 +269,7 @@ class InternationalizationExtension(Extension):
                                 name.value, name.lineno,
                                 exc=TemplateAssertionError)
                 plural_expr = variables[name.value]
+                num_called_num = name.value == 'num'
             parser.stream.expect('block_end')
             plural_names, plural = self._parse_block(parser, False)
             next(parser.stream)
@@ -233,24 +282,14 @@ class InternationalizationExtension(Extension):
             if var not in variables:
                 variables[var] = nodes.Name(var, 'load')
 
-        # no variables referenced?  no need to escape
-        if not referenced:
-            singular = singular.replace('%%', '%')
-            if plural:
-                plural = plural.replace('%%', '%')
-
         if not have_plural:
             plural_expr = None
         elif plural_expr is None:
             parser.fail('pluralize without variables', lineno)
 
-        if variables:
-            variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y)
-                                    for x, y in variables.items()])
-        else:
-            variables = None
-
-        node = self._make_node(singular, plural, variables, plural_expr)
+        node = self._make_node(singular, plural, variables, plural_expr,
+                               bool(referenced),
+                               num_called_num and have_plural)
         node.set_lineno(lineno)
         return node
 
@@ -286,8 +325,16 @@ class InternationalizationExtension(Extension):
 
         return referenced, concat(buf)
 
-    def _make_node(self, singular, plural, variables, plural_expr):
+    def _make_node(self, singular, plural, variables, plural_expr,
+                   vars_referenced, num_called_num):
         """Generates a useful node from the data provided."""
+        # no variables referenced?  no need to escape for old style
+        # gettext invocations only if there are vars.
+        if not vars_referenced and not self.environment.newstyle_gettext:
+            singular = singular.replace('%%', '%')
+            if plural:
+                plural = plural.replace('%%', '%')
+
         # singular only:
         if plural_expr is None:
             gettext = nodes.Name('gettext', 'load')
@@ -303,13 +350,27 @@ class InternationalizationExtension(Extension):
                 plural_expr
             ], [], None, None)
 
-        # mark the return value as safe if we are in an
-        # environment with autoescaping turned on
-        if self.environment.autoescape:
-            node = nodes.MarkSafe(node)
-
-        if variables:
-            node = nodes.Mod(node, variables)
+        # in case newstyle gettext is used, the method is powerful
+        # enough to handle the variable expansion and autoescape
+        # handling itself
+        if self.environment.newstyle_gettext:
+            for key, value in variables.iteritems():
+                # the function adds that later anyways in case num was
+                # called num, so just skip it.
+                if num_called_num and key == 'num':
+                    continue
+                node.kwargs.append(nodes.Keyword(key, value))
+
+        # otherwise do that here
+        else:
+            # mark the return value as safe if we are in an
+            # environment with autoescaping turned on
+            node = nodes.MarkSafeIfAutoescape(node)
+            if variables:
+                node = nodes.Mod(node, nodes.Dict([
+                    nodes.Pair(nodes.Const(key), value)
+                    for key, value in variables.items()
+                ]))
         return nodes.Output([node])
 
 
@@ -357,6 +418,20 @@ class WithExtension(Extension):
         return node
 
 
+class AutoEscapeExtension(Extension):
+    """Changes auto escape rules for a scope."""
+    tags = set(['autoescape'])
+
+    def parse(self, parser):
+        node = nodes.ScopedEvalContextModifier(lineno=next(parser.stream).lineno)
+        node.options = [
+            nodes.Keyword('autoescape', parser.parse_expression())
+        ]
+        node.body = parser.parse_statements(('name:endautoescape',),
+                                            drop_needle=True)
+        return nodes.Scope([node])
+
+
 def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS,
                      babel_style=True):
     """Extract localizable strings from the given template node.  Per
@@ -473,6 +548,10 @@ def babel_extract(fileobj, keywords, comment_tags, options):
        gettext call in one line of code and the matching comment in the
        same line or the line before.
 
+    .. versionchanged:: 2.5.1
+       The `newstyle_gettext` flag can be set to `True` to enable newstyle
+       gettext calls.
+
     :param fileobj: the file-like object the messages should be extracted from
     :param keywords: a list of keywords (i.e. function names) that should be
                      recognized as translation functions
@@ -491,7 +570,10 @@ def babel_extract(fileobj, keywords, comment_tags, options):
     if InternationalizationExtension not in extensions:
         extensions.add(InternationalizationExtension)
 
-    environment = get_spontaneous_environment(
+    def getbool(options, key, default=False):
+        options.get(key, str(default)).lower() in ('1', 'on', 'yes', 'true')
+
+    environment = Environment(
         options.get('block_start_string', BLOCK_START_STRING),
         options.get('block_end_string', BLOCK_END_STRING),
         options.get('variable_start_string', VARIABLE_START_STRING),
@@ -500,17 +582,15 @@ def babel_extract(fileobj, keywords, comment_tags, options):
         options.get('comment_end_string', COMMENT_END_STRING),
         options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX,
         options.get('line_comment_prefix') or LINE_COMMENT_PREFIX,
-        str(options.get('trim_blocks', TRIM_BLOCKS)).lower() in \
-            ('1', 'on', 'yes', 'true'),
+        getbool(options, 'trim_blocks', TRIM_BLOCKS),
         NEWLINE_SEQUENCE, frozenset(extensions),
-        # fill with defaults so that environments are shared
-        # with other spontaneus environments.  The rest of the
-        # arguments are optimizer, undefined, finalize, autoescape,
-        # loader, cache size, auto reloading setting and the
-        # bytecode cache
-        True, Undefined, None, False, None, 0, False, None
+        cache_size=0,
+        auto_reload=False
     )
 
+    if getbool(options, 'newstyle_gettext'):
+        environment.newstyle_gettext = True
+
     source = fileobj.read().decode(options.get('encoding', 'utf-8'))
     try:
         node = environment.parse(source)
@@ -529,3 +609,4 @@ i18n = InternationalizationExtension
 do = ExprStmtExtension
 loopcontrols = LoopControlExtension
 with_ = WithExtension
+autoescape = AutoEscapeExtension
index 3da4221..1ef47f9 100644 (file)
@@ -25,22 +25,44 @@ def contextfilter(f):
     """Decorator for marking context dependent filters. The current
     :class:`Context` will be passed as first argument.
     """
-    if getattr(f, 'environmentfilter', False):
-        raise TypeError('filter already marked as environment filter')
     f.contextfilter = True
     return f
 
 
+def evalcontextfilter(f):
+    """Decorator for marking eval-context dependent filters.  An eval
+    context object is passed as first argument.  For more information
+    about the eval context, see :ref:`eval-context`.
+
+    .. versionadded:: 2.4
+    """
+    f.evalcontextfilter = True
+    return f
+
+
 def environmentfilter(f):
     """Decorator for marking evironment dependent filters.  The current
     :class:`Environment` is passed to the filter as first argument.
     """
-    if getattr(f, 'contextfilter', False):
-        raise TypeError('filter already marked as context filter')
     f.environmentfilter = True
     return f
 
 
+def make_attrgetter(environment, attribute):
+    """Returns a callable that looks up the given attribute from a
+    passed object with the rules of the environment.  Dots are allowed
+    to access attributes of attributes.
+    """
+    if not isinstance(attribute, basestring) or '.' not in attribute:
+        return lambda x: environment.getitem(x, attribute)
+    attribute = attribute.split('.')
+    def attrgetter(item):
+        for part in attribute:
+            item = environment.getitem(item, part)
+        return item
+    return attrgetter
+
+
 def do_forceescape(value):
     """Enforce HTML escaping.  This will probably double escape variables."""
     if hasattr(value, '__html__'):
@@ -48,8 +70,8 @@ def do_forceescape(value):
     return escape(unicode(value))
 
 
-@environmentfilter
-def do_replace(environment, s, old, new, count=None):
+@evalcontextfilter
+def do_replace(eval_ctx, s, old, new, count=None):
     """Return a copy of the value with all occurrences of a substring
     replaced with a new one. The first argument is the substring
     that should be replaced, the second is the replacement string.
@@ -66,7 +88,7 @@ def do_replace(environment, s, old, new, count=None):
     """
     if count is None:
         count = -1
-    if not environment.autoescape:
+    if not eval_ctx.autoescape:
         return unicode(s).replace(unicode(old), unicode(new), count)
     if hasattr(old, '__html__') or hasattr(new, '__html__') and \
        not hasattr(s, '__html__'):
@@ -86,8 +108,8 @@ def do_lower(s):
     return soft_unicode(s).lower()
 
 
-@environmentfilter
-def do_xmlattr(_environment, d, autospace=True):
+@evalcontextfilter
+def do_xmlattr(_eval_ctx, d, autospace=True):
     """Create an SGML/XML attribute string based on the items in a dict.
     All values that are neither `none` nor `undefined` are automatically
     escaped:
@@ -117,7 +139,7 @@ def do_xmlattr(_environment, d, autospace=True):
     )
     if autospace and rv:
         rv = u' ' + rv
-    if _environment.autoescape:
+    if _eval_ctx.autoescape:
         rv = Markup(rv)
     return rv
 
@@ -169,16 +191,33 @@ def do_dictsort(value, case_sensitive=False, by='key'):
     return sorted(value.items(), key=sort_func)
 
 
-def do_sort(value, case_sensitive=False):
-    """Sort an iterable.  If the iterable is made of strings the second
-    parameter can be used to control the case sensitiveness of the
-    comparison which is disabled by default.
+@environmentfilter
+def do_sort(environment, value, reverse=False, case_sensitive=False,
+            attribute=None):
+    """Sort an iterable.  Per default it sorts ascending, if you pass it
+    true as first argument it will reverse the sorting.
+
+    If the iterable is made of strings the third parameter can be used to
+    control the case sensitiveness of the comparison which is disabled by
+    default.
 
     .. sourcecode:: jinja
 
         {% for item in iterable|sort %}
             ...
         {% endfor %}
+
+    It is also possible to sort by an attribute (for example to sort
+    by the date of an object) by specifying the `attribute` parameter:
+
+    .. sourcecode:: jinja
+
+        {% for item in iterable|sort(attribute='date') %}
+            ...
+        {% endfor %}
+
+    .. versionchanged:: 2.6
+       The `attribute` parameter was added.
     """
     if not case_sensitive:
         def sort_func(item):
@@ -187,7 +226,11 @@ def do_sort(value, case_sensitive=False):
             return item
     else:
         sort_func = None
-    return sorted(seq, key=sort_func)
+    if attribute is not None:
+        getter = make_attrgetter(environment, attribute)
+        def sort_func(item, processor=sort_func or (lambda x: x)):
+            return processor(getter(item))
+    return sorted(value, key=sort_func, reverse=reverse)
 
 
 def do_default(value, default_value=u'', boolean=False):
@@ -212,8 +255,8 @@ def do_default(value, default_value=u'', boolean=False):
     return value
 
 
-@environmentfilter
-def do_join(environment, value, d=u''):
+@evalcontextfilter
+def do_join(eval_ctx, value, d=u'', attribute=None):
     """Return a string which is the concatenation of the strings in the
     sequence. The separator between elements is an empty string per
     default, you can define it with the optional parameter:
@@ -225,9 +268,21 @@ def do_join(environment, value, d=u''):
 
         {{ [1, 2, 3]|join }}
             -> 123
+
+    It is also possible to join certain attributes of an object:
+
+    .. sourcecode:: jinja
+
+        {{ users|join(', ', attribute='username') }}
+
+    .. versionadded:: 2.6
+       The `attribute` parameter was added.
     """
+    if attribute is not None:
+        value = imap(make_attrgetter(eval_ctx.environment, attribute), value)
+
     # no automatic escaping?  joining is a lot eaiser then
-    if not environment.autoescape:
+    if not eval_ctx.autoescape:
         return unicode(d).join(imap(unicode, value))
 
     # if the delimiter doesn't have an html representation we check
@@ -283,21 +338,33 @@ def do_random(environment, seq):
 
 
 def do_filesizeformat(value, binary=False):
-    """Format the value like a 'human-readable' file size (i.e. 13 KB,
-    4.1 MB, 102 bytes, etc).  Per default decimal prefixes are used (mega,
-    giga, etc.), if the second parameter is set to `True` the binary
-    prefixes are used (mebi, gibi).
+    """Format the value like a 'human-readable' file size (i.e. 13 kB,
+    4.1 MB, 102 Bytes, etc).  Per default decimal prefixes are used (Mega,
+    Giga, etc.), if the second parameter is set to `True` the binary
+    prefixes are used (Mebi, Gibi).
     """
     bytes = float(value)
     base = binary and 1024 or 1000
-    middle = binary and 'i' or ''
-    if bytes < base:
-        return "%d Byte%s" % (bytes, bytes != 1 and 's' or '')
-    elif bytes < base * base:
-        return "%.1f K%sB" % (bytes / base, middle)
-    elif bytes < base * base * base:
-        return "%.1f M%sB" % (bytes / (base * base), middle)
-    return "%.1f G%sB" % (bytes / (base * base * base), middle)
+    prefixes = [
+        (binary and "KiB" or "kB"),
+        (binary and "MiB" or "MB"),
+        (binary and "GiB" or "GB"),
+        (binary and "TiB" or "TB"),
+        (binary and "PiB" or "PB"),
+        (binary and "EiB" or "EB"),
+        (binary and "ZiB" or "ZB"),
+        (binary and "YiB" or "YB")
+    ]
+    if bytes == 1:
+        return "1 Byte"
+    elif bytes < base:
+        return "%d Bytes" % bytes
+    else:
+        for i, prefix in enumerate(prefixes):
+            unit = base * base ** (i + 1)
+            if bytes < unit:
+                return "%.1f %s" % ((bytes / unit), prefix)
+        return "%.1f %s" % ((bytes / unit), prefix)
 
 
 def do_pprint(value, verbose=False):
@@ -309,8 +376,8 @@ def do_pprint(value, verbose=False):
     return pformat(value, verbose=verbose)
 
 
-@environmentfilter
-def do_urlize(environment, value, trim_url_limit=None, nofollow=False):
+@evalcontextfilter
+def do_urlize(eval_ctx, value, trim_url_limit=None, nofollow=False):
     """Converts URLs in plain text into clickable links.
 
     If you pass the filter an additional integer it will shorten the urls
@@ -323,7 +390,7 @@ def do_urlize(environment, value, trim_url_limit=None, nofollow=False):
             links are shortened to 40 chars and defined with rel="nofollow"
     """
     rv = urlize(value, trim_url_limit, nofollow)
-    if environment.autoescape:
+    if eval_ctx.autoescape:
         rv = Markup(rv)
     return rv
 
@@ -376,8 +443,8 @@ def do_truncate(s, length=255, killwords=False, end='...'):
     result.append(end)
     return u' '.join(result)
 
-
-def do_wordwrap(s, width=79, break_long_words=True):
+@environmentfilter
+def do_wordwrap(environment, s, width=79, break_long_words=True):
     """
     Return a copy of the string passed to the filter wrapped after
     ``79`` characters.  You can override this default using the first
@@ -385,7 +452,7 @@ def do_wordwrap(s, width=79, break_long_words=True):
     split words apart if they are longer than `width`.
     """
     import textwrap
-    return u'\n'.join(textwrap.wrap(s, width=width, expand_tabs=False,
+    return environment.newline_sequence.join(textwrap.wrap(s, width=width, expand_tabs=False,
                                    replace_whitespace=False,
                                    break_long_words=break_long_words))
 
@@ -545,23 +612,10 @@ def do_round(value, precision=0, method='common'):
     """
     if not method in ('common', 'ceil', 'floor'):
         raise FilterArgumentError('method must be common, ceil or floor')
-    if precision < 0:
-        raise FilterArgumentError('precision must be a postive integer '
-                                  'or zero.')
     if method == 'common':
         return round(value, precision)
     func = getattr(math, method)
-    if precision:
-        return func(value * 10 * precision) / (10 * precision)
-    else:
-        return func(value)
-
-
-def do_sort(value, reverse=False):
-    """Sort a sequence. Per default it sorts ascending, if you pass it
-    true as first argument it will reverse the sorting.
-    """
-    return sorted(value, reverse=reverse)
+    return func(value * (10 ** precision)) / (10 ** precision)
 
 
 @environmentfilter
@@ -598,8 +652,12 @@ def do_groupby(environment, value, attribute):
     As you can see the item we're grouping by is stored in the `grouper`
     attribute and the `list` contains all the objects that have this grouper
     in common.
+
+    .. versionchanged:: 2.6
+       It's now possible to use dotted notation to group by the child
+       attribute of another attribute.
     """
-    expr = lambda x: environment.getitem(x, attribute)
+    expr = make_attrgetter(environment, attribute)
     return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr)))
 
 
@@ -612,6 +670,27 @@ class _GroupTuple(tuple):
         return tuple.__new__(cls, (key, list(value)))
 
 
+@environmentfilter
+def do_sum(environment, iterable, attribute=None, start=0):
+    """Returns the sum of a sequence of numbers plus the value of parameter
+    'start' (which defaults to 0).  When the sequence is empty it returns
+    start.
+
+    It is also possible to sum up only certain attributes:
+
+    .. sourcecode:: jinja
+
+        Total: {{ items|sum(attribute='price') }}
+
+    .. versionchanged:: 2.6
+       The `attribute` parameter was added to allow suming up over
+       attributes.  Also the `start` parameter was moved on to the right.
+    """
+    if attribute is not None:
+        iterable = imap(make_attrgetter(environment, attribute), iterable)
+    return sum(iterable, start)
+
+
 def do_list(value):
     """Convert the value into a list.  If it was a string the returned list
     will be a list of characters.
@@ -713,10 +792,9 @@ FILTERS = {
     'striptags':            do_striptags,
     'slice':                do_slice,
     'batch':                do_batch,
-    'sum':                  sum,
+    'sum':                  do_sum,
     'abs':                  abs,
     'round':                do_round,
-    'sort':                 do_sort,
     'groupby':              do_groupby,
     'safe':                 do_mark_safe,
     'xmlattr':              do_xmlattr
index 98d85d9..0d3f696 100644 (file)
@@ -432,9 +432,10 @@ class Lexer(object):
             'root': [
                 # directives
                 (c('(.*?)(?:%s)' % '|'.join(
-                    [r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*%s)' % (
+                    [r'(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*(?:\-%s\s*|%s))' % (
                         e(environment.block_start_string),
                         e(environment.block_start_string),
+                        e(environment.block_end_string),
                         e(environment.block_end_string)
                     )] + [
                         r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, r)
index cfc738b..419a9c8 100644 (file)
@@ -8,6 +8,10 @@
     :copyright: (c) 2010 by the Jinja Team.
     :license: BSD, see LICENSE for more details.
 """
+import os
+import sys
+import weakref
+from types import ModuleType
 from os import path
 try:
     from hashlib import sha1
@@ -59,6 +63,12 @@ class BaseLoader(object):
                 return source, path, lambda: mtime == getmtime(path)
     """
 
+    #: if set to `False` it indicates that the loader cannot provide access
+    #: to the source of templates.
+    #:
+    #: .. versionadded:: 2.4
+    has_source_access = True
+
     def get_source(self, environment, template):
         """Get the template source, filename and reload helper for a template.
         It's passed the environment and template name and has to return a
@@ -77,8 +87,17 @@ class BaseLoader(object):
         old state somewhere (for example in a closure).  If it returns `False`
         the template will be reloaded.
         """
+        if not self.has_source_access:
+            raise RuntimeError('%s cannot provide access to the source' %
+                               self.__class__.__name__)
         raise TemplateNotFound(template)
 
+    def list_templates(self):
+        """Iterates over all templates.  If the loader does not support that
+        it should raise a :exc:`TypeError` which is the default behavior.
+        """
+        raise TypeError('this loader cannot iterate over all templates')
+
     @internalcode
     def load(self, environment, name, globals=None):
         """Loads a template.  This method looks up the template in the cache
@@ -160,6 +179,20 @@ class FileSystemLoader(BaseLoader):
             return contents, filename, uptodate
         raise TemplateNotFound(template)
 
+    def list_templates(self):
+        found = set()
+        for searchpath in self.searchpath:
+            for dirpath, dirnames, filenames in os.walk(searchpath):
+                for filename in filenames:
+                    template = os.path.join(dirpath, filename) \
+                        [len(searchpath):].strip(os.path.sep) \
+                                          .replace(os.path.sep, '/')
+                    if template[:2] == './':
+                        template = template[2:]
+                    if template not in found:
+                        found.add(template)
+        return sorted(found)
+
 
 class PackageLoader(BaseLoader):
     """Load templates from python eggs or packages.  It is constructed with
@@ -206,6 +239,25 @@ class PackageLoader(BaseLoader):
         source = self.provider.get_resource_string(self.manager, p)
         return source.decode(self.encoding), filename, uptodate
 
+    def list_templates(self):
+        path = self.package_path
+        if path[:2] == './':
+            path = path[2:]
+        elif path == '.':
+            path = ''
+        offset = len(path)
+        results = []
+        def _walk(path):
+            for filename in self.provider.resource_listdir(path):
+                fullname = path + '/' + filename
+                if self.provider.resource_isdir(fullname):
+                    _walk(fullname)
+                else:
+                    results.append(fullname[offset:].lstrip('/'))
+        _walk(path)
+        results.sort()
+        return results
+
 
 class DictLoader(BaseLoader):
     """Loads a template from a python dict.  It's passed a dict of unicode
@@ -225,6 +277,9 @@ class DictLoader(BaseLoader):
             return source, None, lambda: source != self.mapping.get(template)
         raise TemplateNotFound(template)
 
+    def list_templates(self):
+        return sorted(self.mapping)
+
 
 class FunctionLoader(BaseLoader):
     """A loader that is passed a function which does the loading.  The
@@ -288,6 +343,13 @@ class PrefixLoader(BaseLoader):
             # (the one that includes the prefix)
             raise TemplateNotFound(template)
 
+    def list_templates(self):
+        result = []
+        for prefix, loader in self.mapping.iteritems():
+            for template in loader.list_templates():
+                result.append(prefix + self.delimiter + template)
+        return result
+
 
 class ChoiceLoader(BaseLoader):
     """This loader works like the `PrefixLoader` just that no prefix is
@@ -313,3 +375,76 @@ class ChoiceLoader(BaseLoader):
             except TemplateNotFound:
                 pass
         raise TemplateNotFound(template)
+
+    def list_templates(self):
+        found = set()
+        for loader in self.loaders:
+            found.update(loader.list_templates())
+        return sorted(found)
+
+
+class _TemplateModule(ModuleType):
+    """Like a normal module but with support for weak references"""
+
+
+class ModuleLoader(BaseLoader):
+    """This loader loads templates from precompiled templates.
+
+    Example usage:
+
+    >>> loader = ChoiceLoader([
+    ...     ModuleLoader('/path/to/compiled/templates'),
+    ...     FileSystemLoader('/path/to/templates')
+    ... ])
+
+    Templates can be precompiled with :meth:`Environment.compile_templates`.
+    """
+
+    has_source_access = False
+
+    def __init__(self, path):
+        package_name = '_jinja2_module_templates_%x' % id(self)
+
+        # create a fake module that looks for the templates in the
+        # path given.
+        mod = _TemplateModule(package_name)
+        if isinstance(path, basestring):
+            path = [path]
+        else:
+            path = list(path)
+        mod.__path__ = path
+
+        sys.modules[package_name] = weakref.proxy(mod,
+            lambda x: sys.modules.pop(package_name, None))
+
+        # the only strong reference, the sys.modules entry is weak
+        # so that the garbage collector can remove it once the
+        # loader that created it goes out of business.
+        self.module = mod
+        self.package_name = package_name
+
+    @staticmethod
+    def get_template_key(name):
+        return 'tmpl_' + sha1(name.encode('utf-8')).hexdigest()
+
+    @staticmethod
+    def get_module_filename(name):
+        return ModuleLoader.get_template_key(name) + '.py'
+
+    @internalcode
+    def load(self, environment, name, globals=None):
+        key = self.get_template_key(name)
+        module = '%s.%s' % (self.package_name, key)
+        mod = getattr(self.module, module, None)
+        if mod is None:
+            try:
+                mod = __import__(module, None, None, ['root'])
+            except ImportError:
+                raise TemplateNotFound(name)
+
+            # remove the entry from sys.modules, we only want the attribute
+            # on the module object we have stored on the loader.
+            sys.modules.pop(module, None)
+
+        return environment.template_class.from_module_dict(
+            environment, mod.__dict__, globals)
index 40ea285..f9da1da 100644 (file)
 import operator
 from itertools import chain, izip
 from collections import deque
-from jinja2.utils import Markup
+from jinja2.utils import Markup, MethodType, FunctionType
+
+
+#: the types we support for context functions
+_context_function_types = (FunctionType, MethodType)
 
 
 _binop_to_func = {
@@ -67,6 +71,37 @@ class NodeType(type):
         return type.__new__(cls, name, bases, d)
 
 
+class EvalContext(object):
+    """Holds evaluation time information.  Custom attributes can be attached
+    to it in extensions.
+    """
+
+    def __init__(self, environment, template_name=None):
+        self.environment = environment
+        if callable(environment.autoescape):
+            self.autoescape = environment.autoescape(template_name)
+        else:
+            self.autoescape = environment.autoescape
+        self.volatile = False
+
+    def save(self):
+        return self.__dict__.copy()
+
+    def revert(self, old):
+        self.__dict__.clear()
+        self.__dict__.update(old)
+
+
+def get_eval_context(node, ctx):
+    if ctx is None:
+        if node.environment is None:
+            raise RuntimeError('if no eval context is passed, the '
+                               'node must have an attached '
+                               'environment.')
+        return EvalContext(node.environment)
+    return ctx
+
+
 class Node(object):
     """Baseclass for all Jinja2 nodes.  There are a number of nodes available
     of different types.  There are three major types:
@@ -312,19 +347,16 @@ class Expr(Node):
     """Baseclass for all expressions."""
     abstract = True
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
         """Return the value of the expression as constant or raise
-        :exc:`Impossible` if this was not possible:
+        :exc:`Impossible` if this was not possible.
 
-        >>> Add(Const(23), Const(42)).as_const()
-        65
-        >>> Add(Const(23), Name('var', 'load')).as_const()
-        Traceback (most recent call last):
-          ...
-        Impossible
+        An :class:`EvalContext` can be provided, if none is given
+        a default context is created which requires the nodes to have
+        an attached environment.
 
-        This requires the `environment` attribute of all nodes to be
-        set to the environment that created the nodes.
+        .. versionchanged:: 2.4
+           the `eval_ctx` parameter was added.
         """
         raise Impossible()
 
@@ -339,11 +371,16 @@ class BinExpr(Expr):
     operator = None
     abstract = True
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        # intercepted operators cannot be folded at compile time
+        if self.environment.sandboxed and \
+           self.operator in self.environment.intercepted_binops:
+            raise Impossible()
         f = _binop_to_func[self.operator]
         try:
-            return f(self.left.as_const(), self.right.as_const())
-        except:
+            return f(self.left.as_const(eval_ctx), self.right.as_const(eval_ctx))
+        except Exception:
             raise Impossible()
 
 
@@ -353,11 +390,16 @@ class UnaryExpr(Expr):
     operator = None
     abstract = True
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        # intercepted operators cannot be folded at compile time
+        if self.environment.sandboxed and \
+           self.operator in self.environment.intercepted_unops:
+            raise Impossible()
         f = _uaop_to_func[self.operator]
         try:
-            return f(self.node.as_const())
-        except:
+            return f(self.node.as_const(eval_ctx))
+        except Exception:
             raise Impossible()
 
 
@@ -389,7 +431,7 @@ class Const(Literal):
     """
     fields = ('value',)
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
         return self.value
 
     @classmethod
@@ -408,8 +450,11 @@ class TemplateData(Literal):
     """A constant template string."""
     fields = ('data',)
 
-    def as_const(self):
-        if self.environment.autoescape:
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if eval_ctx.volatile:
+            raise Impossible()
+        if eval_ctx.autoescape:
             return Markup(self.data)
         return self.data
 
@@ -421,8 +466,9 @@ class Tuple(Literal):
     """
     fields = ('items', 'ctx')
 
-    def as_const(self):
-        return tuple(x.as_const() for x in self.items)
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return tuple(x.as_const(eval_ctx) for x in self.items)
 
     def can_assign(self):
         for item in self.items:
@@ -435,8 +481,9 @@ class List(Literal):
     """Any list literal such as ``[1, 2, 3]``"""
     fields = ('items',)
 
-    def as_const(self):
-        return [x.as_const() for x in self.items]
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return [x.as_const(eval_ctx) for x in self.items]
 
 
 class Dict(Literal):
@@ -445,24 +492,27 @@ class Dict(Literal):
     """
     fields = ('items',)
 
-    def as_const(self):
-        return dict(x.as_const() for x in self.items)
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return dict(x.as_const(eval_ctx) for x in self.items)
 
 
 class Pair(Helper):
     """A key, value pair for dicts."""
     fields = ('key', 'value')
 
-    def as_const(self):
-        return self.key.as_const(), self.value.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return self.key.as_const(eval_ctx), self.value.as_const(eval_ctx)
 
 
 class Keyword(Helper):
     """A key, value pair for keyword arguments where key is a string."""
     fields = ('key', 'value')
 
-    def as_const(self):
-        return self.key, self.value.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return self.key, self.value.as_const(eval_ctx)
 
 
 class CondExpr(Expr):
@@ -471,15 +521,16 @@ class CondExpr(Expr):
     """
     fields = ('test', 'expr1', 'expr2')
 
-    def as_const(self):
-        if self.test.as_const():
-            return self.expr1.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if self.test.as_const(eval_ctx):
+            return self.expr1.as_const(eval_ctx)
 
         # if we evaluate to an undefined object, we better do that at runtime
         if self.expr2 is None:
             raise Impossible()
 
-        return self.expr2.as_const()
+        return self.expr2.as_const(eval_ctx)
 
 
 class Filter(Expr):
@@ -491,8 +542,9 @@ class Filter(Expr):
     """
     fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
 
-    def as_const(self, obj=None):
-        if self.node is obj is None:
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if eval_ctx.volatile or self.node is None:
             raise Impossible()
         # we have to be careful here because we call filter_ below.
         # if this variable would be called filter, 2to3 would wrap the
@@ -502,25 +554,26 @@ class Filter(Expr):
         filter_ = self.environment.filters.get(self.name)
         if filter_ is None or getattr(filter_, 'contextfilter', False):
             raise Impossible()
-        if obj is None:
-            obj = self.node.as_const()
-        args = [x.as_const() for x in self.args]
-        if getattr(filter_, 'environmentfilter', False):
+        obj = self.node.as_const(eval_ctx)
+        args = [x.as_const(eval_ctx) for x in self.args]
+        if getattr(filter_, 'evalcontextfilter', False):
+            args.insert(0, eval_ctx)
+        elif getattr(filter_, 'environmentfilter', False):
             args.insert(0, self.environment)
-        kwargs = dict(x.as_const() for x in self.kwargs)
+        kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs)
         if self.dyn_args is not None:
             try:
-                args.extend(self.dyn_args.as_const())
-            except:
+                args.extend(self.dyn_args.as_const(eval_ctx))
+            except Exception:
                 raise Impossible()
         if self.dyn_kwargs is not None:
             try:
-                kwargs.update(self.dyn_kwargs.as_const())
-            except:
+                kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
+            except Exception:
                 raise Impossible()
         try:
             return filter_(obj, *args, **kwargs)
-        except:
+        except Exception:
             raise Impossible()
 
 
@@ -540,30 +593,36 @@ class Call(Expr):
     """
     fields = ('node', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs')
 
-    def as_const(self):
-        obj = self.node.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if eval_ctx.volatile:
+            raise Impossible()
+        obj = self.node.as_const(eval_ctx)
 
         # don't evaluate context functions
-        args = [x.as_const() for x in self.args]
-        if getattr(obj, 'contextfunction', False):
-            raise Impossible()
-        elif getattr(obj, 'environmentfunction', False):
-            args.insert(0, self.environment)
+        args = [x.as_const(eval_ctx) for x in self.args]
+        if isinstance(obj, _context_function_types):
+            if getattr(obj, 'contextfunction', False):
+                raise Impossible()
+            elif getattr(obj, 'evalcontextfunction', False):
+                args.insert(0, eval_ctx)
+            elif getattr(obj, 'environmentfunction', False):
+                args.insert(0, self.environment)
 
-        kwargs = dict(x.as_const() for x in self.kwargs)
+        kwargs = dict(x.as_const(eval_ctx) for x in self.kwargs)
         if self.dyn_args is not None:
             try:
-                args.extend(self.dyn_args.as_const())
-            except:
+                args.extend(self.dyn_args.as_const(eval_ctx))
+            except Exception:
                 raise Impossible()
         if self.dyn_kwargs is not None:
             try:
-                kwargs.update(self.dyn_kwargs.as_const())
-            except:
+                kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
+            except Exception:
                 raise Impossible()
         try:
             return obj(*args, **kwargs)
-        except:
+        except Exception:
             raise Impossible()
 
 
@@ -571,13 +630,14 @@ class Getitem(Expr):
     """Get an attribute or item from an expression and prefer the item."""
     fields = ('node', 'arg', 'ctx')
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
         if self.ctx != 'load':
             raise Impossible()
         try:
-            return self.environment.getitem(self.node.as_const(),
-                                            self.arg.as_const())
-        except:
+            return self.environment.getitem(self.node.as_const(eval_ctx),
+                                            self.arg.as_const(eval_ctx))
+        except Exception:
             raise Impossible()
 
     def can_assign(self):
@@ -590,12 +650,14 @@ class Getattr(Expr):
     """
     fields = ('node', 'attr', 'ctx')
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
         if self.ctx != 'load':
             raise Impossible()
         try:
-            return self.environment.getattr(self.node.as_const(), arg)
-        except:
+            eval_ctx = get_eval_context(self, eval_ctx)
+            return self.environment.getattr(self.node.as_const(eval_ctx),
+                                            self.attr)
+        except Exception:
             raise Impossible()
 
     def can_assign(self):
@@ -608,11 +670,12 @@ class Slice(Expr):
     """
     fields = ('start', 'stop', 'step')
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
         def const(obj):
             if obj is None:
-                return obj
-            return obj.as_const()
+                return None
+            return obj.as_const(eval_ctx)
         return slice(const(self.start), const(self.stop), const(self.step))
 
 
@@ -622,8 +685,9 @@ class Concat(Expr):
     """
     fields = ('nodes',)
 
-    def as_const(self):
-        return ''.join(unicode(x.as_const()) for x in self.nodes)
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return ''.join(unicode(x.as_const(eval_ctx)) for x in self.nodes)
 
 
 class Compare(Expr):
@@ -632,14 +696,15 @@ class Compare(Expr):
     """
     fields = ('expr', 'ops')
 
-    def as_const(self):
-        result = value = self.expr.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        result = value = self.expr.as_const(eval_ctx)
         try:
             for op in self.ops:
-                new_value = op.expr.as_const()
+                new_value = op.expr.as_const(eval_ctx)
                 result = _cmpop_to_func[op.op](value, new_value)
                 value = new_value
-        except:
+        except Exception:
             raise Impossible()
         return result
 
@@ -695,16 +760,18 @@ class And(BinExpr):
     """Short circuited AND."""
     operator = 'and'
 
-    def as_const(self):
-        return self.left.as_const() and self.right.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return self.left.as_const(eval_ctx) and self.right.as_const(eval_ctx)
 
 
 class Or(BinExpr):
     """Short circuited OR."""
     operator = 'or'
 
-    def as_const(self):
-        return self.left.as_const() or self.right.as_const()
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return self.left.as_const(eval_ctx) or self.right.as_const(eval_ctx)
 
 
 class Not(UnaryExpr):
@@ -756,7 +823,7 @@ class InternalName(Expr):
     yourself but the parser provides a
     :meth:`~jinja2.parser.Parser.free_identifier` method that creates
     a new identifier for you.  This identifier is not available from the
-    template and is not treated specially by the compiler.
+    template and is not threated specially by the compiler.
     """
     fields = ('name',)
 
@@ -769,12 +836,40 @@ class MarkSafe(Expr):
     """Mark the wrapped expression as safe (wrap it as `Markup`)."""
     fields = ('expr',)
 
-    def as_const(self):
-        return Markup(self.expr.as_const())
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        return Markup(self.expr.as_const(eval_ctx))
+
+
+class MarkSafeIfAutoescape(Expr):
+    """Mark the wrapped expression as safe (wrap it as `Markup`) but
+    only if autoescaping is active.
+
+    .. versionadded:: 2.5
+    """
+    fields = ('expr',)
+
+    def as_const(self, eval_ctx=None):
+        eval_ctx = get_eval_context(self, eval_ctx)
+        if eval_ctx.volatile:
+            raise Impossible()
+        expr = self.expr.as_const(eval_ctx)
+        if eval_ctx.autoescape:
+            return Markup(expr)
+        return expr
 
 
 class ContextReference(Expr):
-    """Returns the current template context."""
+    """Returns the current template context.  It can be used like a
+    :class:`Name` node, with a ``'load'`` ctx and will return the
+    current :class:`~jinja2.runtime.Context` object.
+
+    Here an example that assigns the current template name to a
+    variable named `foo`::
+
+        Assign(Name('foo', ctx='store'),
+               Getattr(ContextReference(), 'name'))
+    """
 
 
 class Continue(Stmt):
@@ -790,6 +885,25 @@ class Scope(Stmt):
     fields = ('body',)
 
 
+class EvalContextModifier(Stmt):
+    """Modifies the eval context.  For each option that should be modified,
+    a :class:`Keyword` has to be added to the :attr:`options` list.
+
+    Example to change the `autoescape` setting::
+
+        EvalContextModifier(options=[Keyword('autoescape', Const(True))])
+    """
+    fields = ('options',)
+
+
+class ScopedEvalContextModifier(EvalContextModifier):
+    """Modifies the eval context and reverts it later.  Works exactly like
+    :class:`EvalContextModifier` but will only modify the
+    :class:`~jinja2.nodes.EvalContext` for nodes in the :attr:`body`.
+    """
+    fields = ('body',)
+
+
 # make sure nobody creates custom nodes
 def _failing_new(*args, **kwargs):
     raise TypeError('can\'t create custom node types')
index 08c8976..e036db0 100644 (file)
@@ -34,7 +34,7 @@ class Parser(object):
         self.filename = filename
         self.closed = False
         self.extensions = {}
-        for extension in environment.extensions.itervalues():
+        for extension in environment.iter_extensions():
             for tag in extension.tags:
                 self.extensions[tag] = extension.parse
         self._last_identifier = 0
@@ -370,7 +370,7 @@ class Parser(object):
                 target = self.parse_tuple(simplified=True,
                                           extra_end_rules=extra_end_rules)
             else:
-                target = self.parse_primary(with_postfix=False)
+                target = self.parse_primary()
             target.set_ctx('store')
         if not target.can_assign():
             self.fail('can\'t assign to %r' % target.__class__.
@@ -525,20 +525,23 @@ class Parser(object):
             lineno = self.stream.current.lineno
         return left
 
-    def parse_unary(self):
+    def parse_unary(self, with_filter=True):
         token_type = self.stream.current.type
         lineno = self.stream.current.lineno
         if token_type == 'sub':
             next(self.stream)
-            node = self.parse_unary()
-            return nodes.Neg(node, lineno=lineno)
-        if token_type == 'add':
+            node = nodes.Neg(self.parse_unary(False), lineno=lineno)
+        elif token_type == 'add':
             next(self.stream)
-            node = self.parse_unary()
-            return nodes.Pos(node, lineno=lineno)
-        return self.parse_primary()
+            node = nodes.Pos(self.parse_unary(False), lineno=lineno)
+        else:
+            node = self.parse_primary()
+        node = self.parse_postfix(node)
+        if with_filter:
+            node = self.parse_filter_expr(node)
+        return node
 
-    def parse_primary(self, with_postfix=True):
+    def parse_primary(self):
         token = self.stream.current
         if token.type == 'name':
             if token.value in ('true', 'false', 'True', 'False'):
@@ -570,8 +573,6 @@ class Parser(object):
             node = self.parse_dict()
         else:
             self.fail("unexpected '%s'" % describe_token(token), token.lineno)
-        if with_postfix:
-            node = self.parse_postfix(node)
         return node
 
     def parse_tuple(self, simplified=False, with_condexpr=True,
@@ -596,7 +597,7 @@ class Parser(object):
         """
         lineno = self.stream.current.lineno
         if simplified:
-            parse = lambda: self.parse_primary(with_postfix=False)
+            parse = self.parse_primary
         elif with_condexpr:
             parse = self.parse_expression
         else:
@@ -661,12 +662,25 @@ class Parser(object):
             token_type = self.stream.current.type
             if token_type == 'dot' or token_type == 'lbracket':
                 node = self.parse_subscript(node)
+            # calls are valid both after postfix expressions (getattr
+            # and getitem) as well as filters and tests
             elif token_type == 'lparen':
                 node = self.parse_call(node)
-            elif token_type == 'pipe':
+            else:
+                break
+        return node
+
+    def parse_filter_expr(self, node):
+        while 1:
+            token_type = self.stream.current.type
+            if token_type == 'pipe':
                 node = self.parse_filter(node)
             elif token_type == 'name' and self.stream.current.value == 'is':
                 node = self.parse_test(node)
+            # calls are valid both after postfix expressions (getattr
+            # and getitem) as well as filters and tests
+            elif token_type == 'lparen':
+                node = self.parse_call(node)
             else:
                 break
         return node
index be5b09a..a4a47a2 100644 (file)
@@ -8,10 +8,10 @@
     :copyright: (c) 2010 by the Jinja Team.
     :license: BSD.
 """
-import sys
 from itertools import chain, imap
+from jinja2.nodes import EvalContext, _context_function_types
 from jinja2.utils import Markup, partial, soft_unicode, escape, missing, \
-     concat, MethodType, FunctionType, internalcode, next
+     concat, internalcode, next, object_type_repr
 from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
      TemplateNotFound
 
@@ -19,18 +19,17 @@ from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
 # these variables are exported to the template runtime
 __all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup',
            'TemplateRuntimeError', 'missing', 'concat', 'escape',
-           'markup_join', 'unicode_join', 'to_string',
+           'markup_join', 'unicode_join', 'to_string', 'identity',
            'TemplateNotFound']
 
-
-#: the types we support for context functions
-_context_function_types = (FunctionType, MethodType)
-
 #: the name of the function that is used to convert something into
 #: a string.  2to3 will adopt that automatically and the generated
 #: code can take advantage of it.
 to_string = unicode
 
+#: the identity function.  Useful for certain things in the environment
+identity = lambda x: x
+
 
 def markup_join(seq):
     """Concatenation that escapes if necessary and converts to unicode."""
@@ -76,8 +75,6 @@ class TemplateReference(object):
 
     def __getitem__(self, name):
         blocks = self.__context.blocks[name]
-        wrap = self.__context.environment.autoescape and \
-               Markup or (lambda x: x)
         return BlockReference(name, self.__context, blocks, 0)
 
     def __repr__(self):
@@ -106,13 +103,14 @@ class Context(object):
     method that doesn't fail with a `KeyError` but returns an
     :class:`Undefined` object for missing variables.
     """
-    __slots__ = ('parent', 'vars', 'environment', 'exported_vars', 'name',
-                 'blocks', '__weakref__')
+    __slots__ = ('parent', 'vars', 'environment', 'eval_ctx', 'exported_vars',
+                 'name', 'blocks', '__weakref__')
 
     def __init__(self, environment, parent, name, blocks):
         self.parent = parent
         self.vars = {}
         self.environment = environment
+        self.eval_ctx = EvalContext(self.environment, name)
         self.exported_vars = set()
         self.name = name
 
@@ -174,14 +172,23 @@ class Context(object):
         if isinstance(__obj, _context_function_types):
             if getattr(__obj, 'contextfunction', 0):
                 args = (__self,) + args
+            elif getattr(__obj, 'evalcontextfunction', 0):
+                args = (__self.eval_ctx,) + args
             elif getattr(__obj, 'environmentfunction', 0):
                 args = (__self.environment,) + args
-        return __obj(*args, **kwargs)
+        try:
+            return __obj(*args, **kwargs)
+        except StopIteration:
+            return __self.environment.undefined('value was undefined because '
+                                                'a callable raised a '
+                                                'StopIteration exception')
 
     def derived(self, locals=None):
         """Internal helper function to create a derived context."""
         context = new_context(self.environment, self.name, {},
                               self.parent, True, None, locals)
+        context.vars.update(self.vars)
+        context.eval_ctx = self.eval_ctx
         context.blocks.update((k, list(v)) for k, v in self.blocks.iteritems())
         return context
 
@@ -252,7 +259,7 @@ class BlockReference(object):
     @internalcode
     def __call__(self):
         rv = concat(self._stack[self._depth](self._context))
-        if self._context.environment.autoescape:
+        if self._context.eval_ctx.autoescape:
             rv = Markup(rv)
         return rv
 
@@ -301,7 +308,8 @@ class LoopContext(object):
 
     # a nifty trick to enhance the error message if someone tried to call
     # the the loop without or with too many arguments.
-    __call__ = loop; del loop
+    __call__ = loop
+    del loop
 
     @property
     def length(self):
@@ -340,7 +348,7 @@ class LoopContextIterator(object):
 
 
 class Macro(object):
-    """Wraps a macro."""
+    """Wraps a macro function."""
 
     def __init__(self, environment, func, name, arguments, defaults,
                  catch_kwargs, catch_varargs, caller):
@@ -356,20 +364,24 @@ class Macro(object):
 
     @internalcode
     def __call__(self, *args, **kwargs):
-        arguments = []
-        for idx, name in enumerate(self.arguments):
-            try:
-                value = args[idx]
-            except:
+        # try to consume the positional arguments
+        arguments = list(args[:self._argument_count])
+        off = len(arguments)
+
+        # if the number of arguments consumed is not the number of
+        # arguments expected we start filling in keyword arguments
+        # and defaults.
+        if off != self._argument_count:
+            for idx, name in enumerate(self.arguments[len(arguments):]):
                 try:
                     value = kwargs.pop(name)
-                except:
+                except KeyError:
                     try:
-                        value = self.defaults[idx - self._argument_count]
-                    except:
+                        value = self.defaults[idx - self._argument_count + off]
+                    except IndexError:
                         value = self._environment.undefined(
                             'parameter %r was not provided' % name, name=name)
-            arguments.append(value)
+                arguments.append(value)
 
         # it's important that the order of these arguments does not change
         # if not also changed in the compiler's `function_scoping` method.
@@ -416,7 +428,7 @@ class Undefined(object):
     __slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name',
                  '_undefined_exception')
 
-    def __init__(self, hint=None, obj=None, name=None, exc=UndefinedError):
+    def __init__(self, hint=None, obj=missing, name=None, exc=UndefinedError):
         self._undefined_hint = hint
         self._undefined_obj = obj
         self._undefined_name = name
@@ -428,27 +440,33 @@ class Undefined(object):
         `UndefinedError` on call.
         """
         if self._undefined_hint is None:
-            if self._undefined_obj is None:
+            if self._undefined_obj is missing:
                 hint = '%r is undefined' % self._undefined_name
             elif not isinstance(self._undefined_name, basestring):
-                hint = '%r object has no element %r' % (
-                    self._undefined_obj.__class__.__name__,
+                hint = '%s has no element %r' % (
+                    object_type_repr(self._undefined_obj),
                     self._undefined_name
                 )
             else:
-                hint = '%r object has no attribute %r' % (
-                    self._undefined_obj.__class__.__name__,
+                hint = '%r has no attribute %r' % (
+                    object_type_repr(self._undefined_obj),
                     self._undefined_name
                 )
         else:
             hint = self._undefined_hint
         raise self._undefined_exception(hint)
 
+    @internalcode
+    def __getattr__(self, name):
+        if name[:2] == '__':
+            raise AttributeError(name)
+        return self._fail_with_undefined_error()
+
     __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
     __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \
     __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \
-    __getattr__ = __getitem__ = __lt__ = __le__ = __gt__ = __ge__ = \
-    __int__ = __float__ = __complex__ = __pow__ = __rpow__ = \
+    __getitem__ = __lt__ = __le__ = __gt__ = __ge__ = __int__ = \
+    __float__ = __complex__ = __pow__ = __rpow__ = \
         _fail_with_undefined_error
 
     def __str__(self):
@@ -492,10 +510,10 @@ class DebugUndefined(Undefined):
 
     def __unicode__(self):
         if self._undefined_hint is None:
-            if self._undefined_obj is None:
+            if self._undefined_obj is missing:
                 return u'{{ %s }}' % self._undefined_name
             return '{{ no such element: %s[%r] }}' % (
-                self._undefined_obj.__class__.__name__,
+                object_type_repr(self._undefined_obj),
                 self._undefined_name
             )
         return u'{{ undefined value printed: %s }}' % self._undefined_hint
@@ -522,7 +540,7 @@ class StrictUndefined(Undefined):
     """
     __slots__ = ()
     __iter__ = __unicode__ = __str__ = __len__ = __nonzero__ = __eq__ = \
-        __ne__ = Undefined._fail_with_undefined_error
+        __ne__ = __bool__ = Undefined._fail_with_undefined_error
 
 
 # remove remaining slots attributes, after the metaclass did the magic they
index 7497195..a1cbb29 100644 (file)
@@ -13,7 +13,6 @@
     :license: BSD.
 """
 import operator
-from jinja2.runtime import Undefined
 from jinja2.environment import Environment
 from jinja2.exceptions import SecurityError
 from jinja2.utils import FunctionType, MethodType, TracebackType, CodeType, \
@@ -99,8 +98,9 @@ def safe_range(*args):
 
 
 def unsafe(f):
-    """
-    Mark a function or method as unsafe::
+    """Marks a function or method as unsafe.
+
+    ::
 
         @unsafe
         def delete(self):
@@ -182,9 +182,81 @@ class SandboxedEnvironment(Environment):
     """
     sandboxed = True
 
+    #: default callback table for the binary operators.  A copy of this is
+    #: available on each instance of a sandboxed environment as
+    #: :attr:`binop_table`
+    default_binop_table = {
+        '+':        operator.add,
+        '-':        operator.sub,
+        '*':        operator.mul,
+        '/':        operator.truediv,
+        '//':       operator.floordiv,
+        '**':       operator.pow,
+        '%':        operator.mod
+    }
+
+    #: default callback table for the unary operators.  A copy of this is
+    #: available on each instance of a sandboxed environment as
+    #: :attr:`unop_table`
+    default_unop_table = {
+        '+':        operator.pos,
+        '-':        operator.neg
+    }
+
+    #: a set of binary operators that should be intercepted.  Each operator
+    #: that is added to this set (empty by default) is delegated to the
+    #: :meth:`call_binop` method that will perform the operator.  The default
+    #: operator callback is specified by :attr:`binop_table`.
+    #:
+    #: The following binary operators are interceptable:
+    #: ``//``, ``%``, ``+``, ``*``, ``-``, ``/``, and ``**``
+    #:
+    #: The default operation form the operator table corresponds to the
+    #: builtin function.  Intercepted calls are always slower than the native
+    #: operator call, so make sure only to intercept the ones you are
+    #: interested in.
+    #:
+    #: .. versionadded:: 2.6
+    intercepted_binops = frozenset()
+
+    #: a set of unary operators that should be intercepted.  Each operator
+    #: that is added to this set (empty by default) is delegated to the
+    #: :meth:`call_unop` method that will perform the operator.  The default
+    #: operator callback is specified by :attr:`unop_table`.
+    #:
+    #: The following unary operators are interceptable: ``+``, ``-``
+    #:
+    #: The default operation form the operator table corresponds to the
+    #: builtin function.  Intercepted calls are always slower than the native
+    #: operator call, so make sure only to intercept the ones you are
+    #: interested in.
+    #:
+    #: .. versionadded:: 2.6
+    intercepted_unops = frozenset()
+
+    def intercept_unop(self, operator):
+        """Called during template compilation with the name of a unary
+        operator to check if it should be intercepted at runtime.  If this
+        method returns `True`, :meth:`call_unop` is excuted for this unary
+        operator.  The default implementation of :meth:`call_unop` will use
+        the :attr:`unop_table` dictionary to perform the operator with the
+        same logic as the builtin one.
+
+        The following unary operators are interceptable: ``+`` and ``-``
+
+        Intercepted calls are always slower than the native operator call,
+        so make sure only to intercept the ones you are interested in.
+
+        .. versionadded:: 2.6
+        """
+        return False
+
+
     def __init__(self, *args, **kwargs):
         Environment.__init__(self, *args, **kwargs)
         self.globals['range'] = safe_range
+        self.binop_table = self.default_binop_table.copy()
+        self.unop_table = self.default_unop_table.copy()
 
     def is_safe_attribute(self, obj, attr, value):
         """The sandboxed environment will call this method to check if the
@@ -201,9 +273,27 @@ class SandboxedEnvironment(Environment):
         True.  Override this method to alter the behavior, but this won't
         affect the `unsafe` decorator from this module.
         """
-        return not (getattr(obj, 'unsafe_callable', False) or \
+        return not (getattr(obj, 'unsafe_callable', False) or
                     getattr(obj, 'alters_data', False))
 
+    def call_binop(self, context, operator, left, right):
+        """For intercepted binary operator calls (:meth:`intercepted_binops`)
+        this function is executed instead of the builtin operator.  This can
+        be used to fine tune the behavior of certain operators.
+
+        .. versionadded:: 2.6
+        """
+        return self.binop_table[operator](left, right)
+
+    def call_unop(self, context, operator, arg):
+        """For intercepted unary operator calls (:meth:`intercepted_unops`)
+        this function is executed instead of the builtin operator.  This can
+        be used to fine tune the behavior of certain operators.
+
+        .. versionadded:: 2.6
+        """
+        return self.unop_table[operator](arg)
+
     def getitem(self, obj, argument):
         """Subscribe an object from sandboxed code."""
         try:
@@ -212,7 +302,7 @@ class SandboxedEnvironment(Environment):
             if isinstance(argument, basestring):
                 try:
                     attr = str(argument)
-                except:
+                except Exception:
                     pass
                 else:
                     try:
index d257eca..50510b0 100644 (file)
 import re
 from jinja2.runtime import Undefined
 
+try:
+    from collections import Mapping as MappingType
+except ImportError:
+    import UserDict
+    MappingType = (UserDict.UserDict, UserDict.DictMixin, dict)
+
 # nose, nothing here to test
 __test__ = False
 
@@ -83,6 +89,14 @@ def test_string(value):
     return isinstance(value, basestring)
 
 
+def test_mapping(value):
+    """Return true if the object is a mapping (dict etc.).
+
+    .. versionadded:: 2.6
+    """
+    return isinstance(value, MappingType)
+
+
 def test_number(value):
     """Return true if the variable is a number."""
     return isinstance(value, (int, long, float, complex))
@@ -137,6 +151,7 @@ TESTS = {
     'lower':            test_lower,
     'upper':            test_upper,
     'string':           test_string,
+    'mapping':          test_mapping,
     'number':           test_number,
     'sequence':         test_sequence,
     'iterable':         test_iterable,
index c24d7aa..49e9e9a 100644 (file)
@@ -53,7 +53,7 @@ except TypeError, _error:
         def concat(gen):
             try:
                 return _concat(list(gen))
-            except:
+            except Exception:
                 # this hack is needed so that the current frame
                 # does not show up in the traceback.
                 exc_type, exc_value, tb = sys.exc_info()
@@ -127,6 +127,19 @@ def contextfunction(f):
     return f
 
 
+def evalcontextfunction(f):
+    """This decoraotr can be used to mark a function or method as an eval
+    context callable.  This is similar to the :func:`contextfunction`
+    but instead of passing the context, an evaluation context object is
+    passed.  For more information about the eval context, see
+    :ref:`eval-context`.
+
+    .. versionadded:: 2.4
+    """
+    f.evalcontextfunction = True
+    return f
+
+
 def environmentfunction(f):
     """This decorator can be used to mark a function or method as environment
     callable.  This decorator works exactly like the :func:`contextfunction`
@@ -214,6 +227,23 @@ def open_if_exists(filename, mode='rb'):
             raise
 
 
+def object_type_repr(obj):
+    """Returns the name of the object's type.  For some recognized
+    singletons the name of the object is returned instead. (For
+    example for `None` and `Ellipsis`).
+    """
+    if obj is None:
+        return 'None'
+    elif obj is Ellipsis:
+        return 'Ellipsis'
+    # __builtin__ in 2.x, builtins in 3.x
+    if obj.__class__.__module__ in ('__builtin__', 'builtins'):
+        name = obj.__class__.__name__
+    else:
+        name = obj.__class__.__module__ + '.' + obj.__class__.__name__
+    return '%s object' % name
+
+
 def pformat(obj, verbose=False):
     """Prettyprint an object.  Either use the `pretty` library or the
     builtin `pprint`.
@@ -319,205 +349,6 @@ def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
     return Markup(u'\n'.join(u'<p>%s</p>' % escape(x) for x in result))
 
 
-class Markup(unicode):
-    r"""Marks a string as being safe for inclusion in HTML/XML output without
-    needing to be escaped.  This implements the `__html__` interface a couple
-    of frameworks and web applications use.  :class:`Markup` is a direct
-    subclass of `unicode` and provides all the methods of `unicode` just that
-    it escapes arguments passed and always returns `Markup`.
-
-    The `escape` function returns markup objects so that double escaping can't
-    happen.  If you want to use autoescaping in Jinja just enable the
-    autoescaping feature in the environment.
-
-    The constructor of the :class:`Markup` class can be used for three
-    different things:  When passed an unicode object it's assumed to be safe,
-    when passed an object with an HTML representation (has an `__html__`
-    method) that representation is used, otherwise the object passed is
-    converted into a unicode string and then assumed to be safe:
-
-    >>> Markup("Hello <em>World</em>!")
-    Markup(u'Hello <em>World</em>!')
-    >>> class Foo(object):
-    ...  def __html__(self):
-    ...   return '<a href="#">foo</a>'
-    ...
-    >>> Markup(Foo())
-    Markup(u'<a href="#">foo</a>')
-
-    If you want object passed being always treated as unsafe you can use the
-    :meth:`escape` classmethod to create a :class:`Markup` object:
-
-    >>> Markup.escape("Hello <em>World</em>!")
-    Markup(u'Hello &lt;em&gt;World&lt;/em&gt;!')
-
-    Operations on a markup string are markup aware which means that all
-    arguments are passed through the :func:`escape` function:
-
-    >>> em = Markup("<em>%s</em>")
-    >>> em % "foo & bar"
-    Markup(u'<em>foo &amp; bar</em>')
-    >>> strong = Markup("<strong>%(text)s</strong>")
-    >>> strong % {'text': '<blink>hacker here</blink>'}
-    Markup(u'<strong>&lt;blink&gt;hacker here&lt;/blink&gt;</strong>')
-    >>> Markup("<em>Hello</em> ") + "<foo>"
-    Markup(u'<em>Hello</em> &lt;foo&gt;')
-    """
-    __slots__ = ()
-
-    def __new__(cls, base=u'', encoding=None, errors='strict'):
-        if hasattr(base, '__html__'):
-            base = base.__html__()
-        if encoding is None:
-            return unicode.__new__(cls, base)
-        return unicode.__new__(cls, base, encoding, errors)
-
-    def __html__(self):
-        return self
-
-    def __add__(self, other):
-        if hasattr(other, '__html__') or isinstance(other, basestring):
-            return self.__class__(unicode(self) + unicode(escape(other)))
-        return NotImplemented
-
-    def __radd__(self, other):
-        if hasattr(other, '__html__') or isinstance(other, basestring):
-            return self.__class__(unicode(escape(other)) + unicode(self))
-        return NotImplemented
-
-    def __mul__(self, num):
-        if isinstance(num, (int, long)):
-            return self.__class__(unicode.__mul__(self, num))
-        return NotImplemented
-    __rmul__ = __mul__
-
-    def __mod__(self, arg):
-        if isinstance(arg, tuple):
-            arg = tuple(imap(_MarkupEscapeHelper, arg))
-        else:
-            arg = _MarkupEscapeHelper(arg)
-        return self.__class__(unicode.__mod__(self, arg))
-
-    def __repr__(self):
-        return '%s(%s)' % (
-            self.__class__.__name__,
-            unicode.__repr__(self)
-        )
-
-    def join(self, seq):
-        return self.__class__(unicode.join(self, imap(escape, seq)))
-    join.__doc__ = unicode.join.__doc__
-
-    def split(self, *args, **kwargs):
-        return map(self.__class__, unicode.split(self, *args, **kwargs))
-    split.__doc__ = unicode.split.__doc__
-
-    def rsplit(self, *args, **kwargs):
-        return map(self.__class__, unicode.rsplit(self, *args, **kwargs))
-    rsplit.__doc__ = unicode.rsplit.__doc__
-
-    def splitlines(self, *args, **kwargs):
-        return map(self.__class__, unicode.splitlines(self, *args, **kwargs))
-    splitlines.__doc__ = unicode.splitlines.__doc__
-
-    def unescape(self):
-        r"""Unescape markup again into an unicode string.  This also resolves
-        known HTML4 and XHTML entities:
-
-        >>> Markup("Main &raquo; <em>About</em>").unescape()
-        u'Main \xbb <em>About</em>'
-        """
-        from jinja2.constants import HTML_ENTITIES
-        def handle_match(m):
-            name = m.group(1)
-            if name in HTML_ENTITIES:
-                return unichr(HTML_ENTITIES[name])
-            try:
-                if name[:2] in ('#x', '#X'):
-                    return unichr(int(name[2:], 16))
-                elif name.startswith('#'):
-                    return unichr(int(name[1:]))
-            except ValueError:
-                pass
-            return u''
-        return _entity_re.sub(handle_match, unicode(self))
-
-    def striptags(self):
-        r"""Unescape markup into an unicode string and strip all tags.  This
-        also resolves known HTML4 and XHTML entities.  Whitespace is
-        normalized to one:
-
-        >>> Markup("Main &raquo;  <em>About</em>").striptags()
-        u'Main \xbb About'
-        """
-        stripped = u' '.join(_striptags_re.sub('', self).split())
-        return Markup(stripped).unescape()
-
-    @classmethod
-    def escape(cls, s):
-        """Escape the string.  Works like :func:`escape` with the difference
-        that for subclasses of :class:`Markup` this function would return the
-        correct subclass.
-        """
-        rv = escape(s)
-        if rv.__class__ is not cls:
-            return cls(rv)
-        return rv
-
-    def make_wrapper(name):
-        orig = getattr(unicode, name)
-        def func(self, *args, **kwargs):
-            args = _escape_argspec(list(args), enumerate(args))
-            _escape_argspec(kwargs, kwargs.iteritems())
-            return self.__class__(orig(self, *args, **kwargs))
-        func.__name__ = orig.__name__
-        func.__doc__ = orig.__doc__
-        return func
-
-    for method in '__getitem__', 'capitalize', \
-                  'title', 'lower', 'upper', 'replace', 'ljust', \
-                  'rjust', 'lstrip', 'rstrip', 'center', 'strip', \
-                  'translate', 'expandtabs', 'swapcase', 'zfill':
-        locals()[method] = make_wrapper(method)
-
-    # new in python 2.5
-    if hasattr(unicode, 'partition'):
-        partition = make_wrapper('partition'),
-        rpartition = make_wrapper('rpartition')
-
-    # new in python 2.6
-    if hasattr(unicode, 'format'):
-        format = make_wrapper('format')
-
-    # not in python 3
-    if hasattr(unicode, '__getslice__'):
-        __getslice__ = make_wrapper('__getslice__')
-
-    del method, make_wrapper
-
-
-def _escape_argspec(obj, iterable):
-    """Helper for various string-wrapped functions."""
-    for key, value in iterable:
-        if hasattr(value, '__html__') or isinstance(value, basestring):
-            obj[key] = escape(value)
-    return obj
-
-
-class _MarkupEscapeHelper(object):
-    """Helper for Markup.__mod__"""
-
-    def __init__(self, obj):
-        self.obj = obj
-
-    __getitem__ = lambda s, x: _MarkupEscapeHelper(s.obj[x])
-    __str__ = lambda s: str(escape(s.obj))
-    __unicode__ = lambda s: unicode(escape(s.obj))
-    __repr__ = lambda s: str(escape(repr(s.obj)))
-    __int__ = lambda s: int(s.obj)
-    __float__ = lambda s: float(s.obj)
-
-
 class LRUCache(object):
     """A simple LRU Cache implementation."""
 
@@ -746,33 +577,14 @@ class Joiner(object):
         return self.sep
 
 
-# we have to import it down here as the speedups module imports the
-# markup type which is define above.
+# try markupsafe first, if that fails go with Jinja2's bundled version
+# of markupsafe.  Markupsafe was previously Jinja2's implementation of
+# the Markup object but was moved into a separate package in a patchleve
+# release
 try:
-    from jinja2._speedups import escape, soft_unicode
+    from markupsafe import Markup, escape, soft_unicode
 except ImportError:
-    def escape(s):
-        """Convert the characters &, <, >, ' and " in string s to HTML-safe
-        sequences.  Use this if you need to display text that might contain
-        such characters in HTML.  Marks return value as markup string.
-        """
-        if hasattr(s, '__html__'):
-            return s.__html__()
-        return Markup(unicode(s)
-            .replace('&', '&amp;')
-            .replace('>', '&gt;')
-            .replace('<', '&lt;')
-            .replace("'", '&#39;')
-            .replace('"', '&#34;')
-        )
-
-    def soft_unicode(s):
-        """Make a string unicode if it isn't already.  That way a markup
-        string is not converted back to unicode.
-        """
-        if not isinstance(s, unicode):
-            s = unicode(s)
-        return s
+    from jinja2._markupsafe import Markup, escape, soft_unicode
 
 
 # partials