Biggest change to Jinja since the 1.x migration: added evaluation contexts
authorArmin Ronacher <armin.ronacher@active-4.com>
Sun, 14 Mar 2010 18:43:47 +0000 (19:43 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sun, 14 Mar 2010 18:43:47 +0000 (19:43 +0100)
which make it possible to keep the ahead of time optimizations and provide
dynamic activation and deactivation of autoescaping and other context
specific features.

--HG--
branch : trunk

jinja2/__init__.py
jinja2/compiler.py
jinja2/environment.py
jinja2/ext.py
jinja2/filters.py
jinja2/nodes.py
jinja2/runtime.py
jinja2/testsuite/ext.py
jinja2/utils.py

index 2fc0219b0fadfa813d32f06277f29ad9f84304b4..f944e11b6c332a7cfc4c782633ecdf471e9a3f6d 100644 (file)
@@ -54,9 +54,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',
@@ -66,5 +68,6 @@ __all__ = [
     'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound',
     'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError',
     'ModuleLoader', 'environmentfilter', 'contextfilter', 'Markup', 'escape',
-    'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined'
+    'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined',
+    'evalcontextfilter', 'evalcontextfunction'
 ]
index ccf081175d123438cacdb3b90a65f4a64cbf68f5..5f355a99b6a88fa10b25f29195ab86d1e11b7da9 100644 (file)
@@ -12,6 +12,7 @@ from cStringIO import StringIO
 from itertools import chain
 from copy import deepcopy
 from jinja2 import nodes
+from jinja2.nodes import EvalContext
 from jinja2.visitor import NodeVisitor, NodeTransformer
 from jinja2.exceptions import TemplateAssertionError
 from jinja2.utils import Markup, concat, escape, is_python_keyword, next
@@ -141,7 +142,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.
@@ -211,7 +213,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
@@ -422,7 +424,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):
@@ -437,10 +439,15 @@ class CodeGenerator(NodeVisitor):
 
     def return_buffer_contents(self, frame):
         """Return the buffer contents of the frame."""
-        if self.environment.autoescape:
-            self.writeline('return Markup(concat(%s))' % frame.buffer)
+        self.writeline('return ')
+        if frame.eval_ctx.volatile:
+            self.write('(Markup(concat(%s)) if context.eval_ctx'
+                       '.autoescape else concat(%s))' %
+                       (frame.buffer, frame.buffer))
+        elif frame.eval_ctx.autoescape:
+            self.write('Markup(concat(%s))' % frame.buffer)
         else:
-            self.writeline('return concat(%s)' % frame.buffer)
+            self.write('concat(%s)' % frame.buffer)
 
     def indent(self):
         """Indent by one."""
@@ -750,6 +757,8 @@ class CodeGenerator(NodeVisitor):
 
     def visit_Template(self, node, frame=None):
         assert frame is None, 'no root frame allowed'
+        eval_ctx = EvalContext(self.environment)
+
         from jinja2.runtime import __all__ as exported
         self.writeline('from __future__ import division')
         self.writeline('from jinja2.runtime import ' + ', '.join(exported))
@@ -789,7 +798,7 @@ class CodeGenerator(NodeVisitor):
         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
@@ -818,7 +827,7 @@ 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%s):' % (name, envenv),
@@ -1224,12 +1233,15 @@ 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:
@@ -1267,7 +1279,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(')
@@ -1300,7 +1315,10 @@ 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)(')
+                elif frame.eval_ctx.autoescape:
                     self.write('escape(')
                     close += 1
                 if self.environment.finalize is not None:
@@ -1367,7 +1385,7 @@ class CodeGenerator(NodeVisitor):
             self.write(repr(val))
 
     def visit_TemplateData(self, node, frame):
-        self.write(repr(node.as_const()))
+        self.write(repr(node.as_const(frame.eval_ctx)))
 
     def visit_Tuple(self, node, frame):
         self.write('(')
@@ -1427,8 +1445,14 @@ class CodeGenerator(NodeVisitor):
     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(', ')
@@ -1479,6 +1503,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, ')
 
@@ -1486,7 +1512,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)
@@ -1575,3 +1605,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.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 9145acb5e134ebcecd6bb0959ead94142221eaf8..b70f521a329e0ed135c4c9fd78c895e1c47fdd1c 100644 (file)
@@ -158,8 +158,8 @@ 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
+            If set to true the XML/HTML autoescaping feature is enabled by
+            default.  For more details about auto escaping see
             :class:`~jinja2.utils.Markup`.
 
         `loader`
@@ -493,6 +493,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:
index c3c8eec0bead7a40d4463dfe1a177ea1ed0917da..64d5525adf436a8c33ac53681144262b75630995 100644 (file)
@@ -357,6 +357,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
@@ -529,3 +543,4 @@ i18n = InternationalizationExtension
 do = ExprStmtExtension
 loopcontrols = LoopControlExtension
 with_ = WithExtension
+autoescape = AutoEscapeExtension
index 3da422145ab549b2d262e90218846be9144cb8b5..b7402de160b8b23a33fe6cd47ce2c145c2457c62 100644 (file)
@@ -25,18 +25,24 @@ 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.
+
+    .. 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
 
@@ -48,8 +54,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 +72,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 +92,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 +123,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
 
@@ -212,8 +218,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''):
     """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:
@@ -227,7 +233,7 @@ def do_join(environment, value, d=u''):
             -> 123
     """
     # 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
@@ -309,8 +315,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 +329,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
 
index 424c1cd8e36837e7b4a6160e101353facbd571cb..afc735542c308ae3cce74e316b2b859fef84d9cc 100644 (file)
@@ -67,6 +67,31 @@ class NodeType(type):
         return type.__new__(cls, name, bases, d)
 
 
+class EvalContext(object):
+    """Holds evaluation time information"""
+
+    def __init__(self, environment):
+        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 +337,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,10 +361,11 @@ 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)
         f = _binop_to_func[self.operator]
         try:
-            return f(self.left.as_const(), self.right.as_const())
+            return f(self.left.as_const(eval_ctx), self.right.as_const(eval_ctx))
         except:
             raise Impossible()
 
@@ -353,10 +376,11 @@ 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)
         f = _uaop_to_func[self.operator]
         try:
-            return f(self.node.as_const())
+            return f(self.node.as_const(eval_ctx))
         except:
             raise Impossible()
 
@@ -389,7 +413,7 @@ class Const(Literal):
     """
     fields = ('value',)
 
-    def as_const(self):
+    def as_const(self, eval_ctx=None):
         return self.value
 
     @classmethod
@@ -408,8 +432,8 @@ class TemplateData(Literal):
     """A constant template string."""
     fields = ('data',)
 
-    def as_const(self):
-        if self.environment.autoescape:
+    def as_const(self, eval_ctx=None):
+        if get_eval_context(self, eval_ctx).autoescape:
             return Markup(self.data)
         return self.data
 
@@ -421,8 +445,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 +460,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 +471,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 +500,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 +521,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,20 +533,21 @@ 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())
+                args.extend(self.dyn_args.as_const(eval_ctx))
             except:
                 raise Impossible()
         if self.dyn_kwargs is not None:
             try:
-                kwargs.update(self.dyn_kwargs.as_const())
+                kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
             except:
                 raise Impossible()
         try:
@@ -540,25 +572,30 @@ 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]
+        args = [x.as_const(eval_ctx) for x in self.args]
         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())
+                args.extend(self.dyn_args.as_const(eval_ctx))
             except:
                 raise Impossible()
         if self.dyn_kwargs is not None:
             try:
-                kwargs.update(self.dyn_kwargs.as_const())
+                kwargs.update(self.dyn_kwargs.as_const(eval_ctx))
             except:
                 raise Impossible()
         try:
@@ -571,12 +608,13 @@ 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())
+            return self.environment.getitem(self.node.as_const(eval_ctx),
+                                            self.arg.as_const(eval_ctx))
         except:
             raise Impossible()
 
@@ -590,11 +628,12 @@ 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)
+            eval_ctx = get_eval_context(self, eval_ctx)
+            return self.environment.getattr(self.node.as_const(eval_ctx), arg)
         except:
             raise Impossible()
 
@@ -608,11 +647,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 +662,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,11 +673,12 @@ 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:
@@ -695,16 +737,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):
@@ -769,8 +813,9 @@ 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 ContextReference(Expr):
@@ -790,6 +835,16 @@ class Scope(Stmt):
     fields = ('body',)
 
 
+class EvalContextModifier(Stmt):
+    """Modifies the eval context"""
+    fields = ('options',)
+
+
+class ScopedEvalContextModifier(EvalContextModifier):
+    """Modifies the eval context and reverts it later."""
+    fields = ('body',)
+
+
 # make sure nobody creates custom nodes
 def _failing_new(*args, **kwargs):
     raise TypeError('can\'t create custom node types')
index 244fb5491f9157c2fd84d63f67f71664a7724e5b..318e654def3064d16b09270944b5d42d7ba9fcc6 100644 (file)
@@ -10,6 +10,7 @@
 """
 import sys
 from itertools import chain, imap
+from jinja2.nodes import EvalContext
 from jinja2.utils import Markup, partial, soft_unicode, escape, missing, \
      concat, MethodType, FunctionType, internalcode, next
 from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
@@ -106,13 +107,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)
         self.exported_vars = set()
         self.name = name
 
@@ -174,6 +176,8 @@ 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)
@@ -182,6 +186,7 @@ class Context(object):
         """Internal helper function to create a derived context."""
         context = new_context(self.environment, self.name, {},
                               self.parent, True, None, locals)
+        context.eval_ctx = self.eval_ctx
         context.blocks.update((k, list(v)) for k, v in self.blocks.iteritems())
         return context
 
index ddb81eb7c2cb0717a758efa1ec892d332398fef1..09b2b857af15f5d45974e334ad163ce582012928 100644 (file)
@@ -256,8 +256,60 @@ class InternationalizationTestCase(JinjaTestCase):
         ]
 
 
+class AutoEscapeTestCase(JinjaTestCase):
+
+    def test_scoped_setting(self):
+        env = Environment(extensions=['jinja2.ext.autoescape'],
+                          autoescape=True)
+        tmpl = env.from_string('''
+            {{ "<HelloWorld>" }}
+            {% autoescape false %}
+                {{ "<HelloWorld>" }}
+            {% endautoescape %}
+            {{ "<HelloWorld>" }}
+        ''')
+        assert tmpl.render().split() == \
+            [u'&lt;HelloWorld&gt;', u'<HelloWorld>', u'&lt;HelloWorld&gt;']
+
+        env = Environment(extensions=['jinja2.ext.autoescape'],
+                          autoescape=False)
+        tmpl = env.from_string('''
+            {{ "<HelloWorld>" }}
+            {% autoescape true %}
+                {{ "<HelloWorld>" }}
+            {% endautoescape %}
+            {{ "<HelloWorld>" }}
+        ''')
+        assert tmpl.render().split() == \
+            [u'<HelloWorld>', u'&lt;HelloWorld&gt;', u'<HelloWorld>']
+
+    def test_nonvolatile(self):
+        env = Environment(extensions=['jinja2.ext.autoescape'],
+                          autoescape=True)
+        tmpl = env.from_string('{{ {"foo": "<test>"}|xmlattr|escape }}')
+        assert tmpl.render() == ' foo="&lt;test&gt;"'
+        tmpl = env.from_string('{% autoescape false %}{{ {"foo": "<test>"}'
+                               '|xmlattr|escape }}{% endautoescape %}')
+        assert tmpl.render() == ' foo=&#34;&amp;lt;test&amp;gt;&#34;'
+
+    def test_volatile(self):
+        env = Environment(extensions=['jinja2.ext.autoescape'],
+                          autoescape=True)
+        tmpl = env.from_string('{% autoescape foo %}{{ {"foo": "<test>"}'
+                               '|xmlattr|escape }}{% endautoescape %}')
+        assert tmpl.render(foo=False) == ' foo=&#34;&amp;lt;test&amp;gt;&#34;'
+        assert tmpl.render(foo=True) == ' foo="&lt;test&gt;"'
+
+    def test_scoping(self):
+        env = Environment(extensions=['jinja2.ext.autoescape'])
+        tmpl = env.from_string('{% autoescape true %}{% set x = "<x>" %}{{ x }}'
+                               '{% endautoescape %}{{ x }}{{ "<y>" }}')
+        assert tmpl.render(x=1) == '&lt;x&gt;1<y>'
+
+
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(ExtensionsTestCase))
     suite.addTest(unittest.makeSuite(InternationalizationTestCase))
+    suite.addTest(unittest.makeSuite(AutoEscapeTestCase))
     return suite
index 0ba46451d08473e83ff917c33a55666eb98e92da..e00dee21623851bed14085f7d49e9d3a3c85d65f 100644 (file)
@@ -127,6 +127,18 @@ 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.
+
+    .. 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`