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
This commit is contained in:
Armin Ronacher 2010-03-14 19:43:47 +01:00
parent 12a316bd5c
commit 8346bd7ec3
9 changed files with 304 additions and 104 deletions

View 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'
]

View 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)

View 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:

View 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

View 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

View 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')

View 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

View 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

View 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`