From 8346bd7ec3dfef84a873b3413c3ebc524b847f55 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Mar 2010 19:43:47 +0100 Subject: [PATCH] Biggest change to Jinja since the 1.x migration: added evaluation contexts 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 | 9 +- jinja2/compiler.py | 83 ++++++++++++++---- jinja2/environment.py | 5 +- jinja2/ext.py | 15 ++++ jinja2/filters.py | 38 +++++---- jinja2/nodes.py | 185 ++++++++++++++++++++++++++-------------- jinja2/runtime.py | 9 +- jinja2/testsuite/ext.py | 52 +++++++++++ jinja2/utils.py | 12 +++ 9 files changed, 304 insertions(+), 104 deletions(-) diff --git a/jinja2/__init__.py b/jinja2/__init__.py index 2fc0219..f944e11 100644 --- a/jinja2/__init__.py +++ b/jinja2/__init__.py @@ -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' ] diff --git a/jinja2/compiler.py b/jinja2/compiler.py index ccf0811..5f355a9 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -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) diff --git a/jinja2/environment.py b/jinja2/environment.py index 9145acb..b70f521 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -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: diff --git a/jinja2/ext.py b/jinja2/ext.py index c3c8eec..64d5525 100644 --- a/jinja2/ext.py +++ b/jinja2/ext.py @@ -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 diff --git a/jinja2/filters.py b/jinja2/filters.py index 3da4221..b7402de 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -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 diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 424c1cd..afc7355 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -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') diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 244fb54..318e654 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -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 diff --git a/jinja2/testsuite/ext.py b/jinja2/testsuite/ext.py index ddb81eb..09b2b85 100644 --- a/jinja2/testsuite/ext.py +++ b/jinja2/testsuite/ext.py @@ -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(''' + {{ "" }} + {% autoescape false %} + {{ "" }} + {% endautoescape %} + {{ "" }} + ''') + assert tmpl.render().split() == \ + [u'<HelloWorld>', u'', u'<HelloWorld>'] + + env = Environment(extensions=['jinja2.ext.autoescape'], + autoescape=False) + tmpl = env.from_string(''' + {{ "" }} + {% autoescape true %} + {{ "" }} + {% endautoescape %} + {{ "" }} + ''') + assert tmpl.render().split() == \ + [u'', u'<HelloWorld>', u''] + + def test_nonvolatile(self): + env = Environment(extensions=['jinja2.ext.autoescape'], + autoescape=True) + tmpl = env.from_string('{{ {"foo": ""}|xmlattr|escape }}') + assert tmpl.render() == ' foo="<test>"' + tmpl = env.from_string('{% autoescape false %}{{ {"foo": ""}' + '|xmlattr|escape }}{% endautoescape %}') + assert tmpl.render() == ' foo="&lt;test&gt;"' + + def test_volatile(self): + env = Environment(extensions=['jinja2.ext.autoescape'], + autoescape=True) + tmpl = env.from_string('{% autoescape foo %}{{ {"foo": ""}' + '|xmlattr|escape }}{% endautoescape %}') + assert tmpl.render(foo=False) == ' foo="&lt;test&gt;"' + assert tmpl.render(foo=True) == ' foo="<test>"' + + def test_scoping(self): + env = Environment(extensions=['jinja2.ext.autoescape']) + tmpl = env.from_string('{% autoescape true %}{% set x = "" %}{{ x }}' + '{% endautoescape %}{{ x }}{{ "" }}') + assert tmpl.render(x=1) == '<x>1' + + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(ExtensionsTestCase)) suite.addTest(unittest.makeSuite(InternationalizationTestCase)) + suite.addTest(unittest.makeSuite(AutoEscapeTestCase)) return suite diff --git a/jinja2/utils.py b/jinja2/utils.py index 0ba4645..e00dee2 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -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` -- 2.26.2