Added support for Environment.compile_expression.

--HG--
branch : trunk
This commit is contained in:
Armin Ronacher 2008-11-02 15:58:14 +01:00
parent 9efe0819a1
commit ba6e25a882
8 changed files with 102 additions and 18 deletions

View File

@ -41,6 +41,9 @@ Version 2.1
- added a joining helper called `joiner`.
- added a `compile_expression` method to the environment that allows compiling
of Jinja expressions into callable Python objects.
Version 2.0
-----------
(codename jinjavitus, released on July 17th 2008)

View File

@ -115,7 +115,7 @@ useful if you want to dig deeper into Jinja2 or :ref:`develop extensions
<jinja-extensions>`.
.. autoclass:: Environment([options])
:members: from_string, get_template, join_path, extend
:members: from_string, get_template, join_path, extend, compile_expression
.. attribute:: shared

View File

@ -9,6 +9,7 @@
:license: BSD, see LICENSE for more details.
"""
import sys
from jinja2 import nodes
from jinja2.defaults import *
from jinja2.lexer import get_lexer, TokenStream
from jinja2.parser import Parser
@ -16,7 +17,8 @@ from jinja2.optimizer import optimize
from jinja2.compiler import generate
from jinja2.runtime import Undefined, Context
from jinja2.exceptions import TemplateSyntaxError
from jinja2.utils import import_string, LRUCache, Markup, missing, concat
from jinja2.utils import import_string, LRUCache, Markup, missing, \
concat, consume
# for direct template usage we have up to ten living environments
@ -379,12 +381,12 @@ class Environment(object):
return reduce(lambda s, e: e.preprocess(s, name, filename),
self.extensions.itervalues(), unicode(source))
def _tokenize(self, source, name, filename=None):
def _tokenize(self, source, name, filename=None, state=None):
"""Called by the parser to do the preprocessing and filtering
for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`.
"""
source = self.preprocess(source, name, filename)
stream = self.lexer.tokenize(source, name, filename)
stream = self.lexer.tokenize(source, name, filename, state)
for ext in self.extensions.itervalues():
stream = ext.filter_stream(stream)
if not isinstance(stream, TokenStream):
@ -407,8 +409,8 @@ class Environment(object):
if isinstance(source, basestring):
source = self.parse(source, name, filename)
if self.optimized:
node = optimize(source, self)
source = generate(node, self, name, filename)
source = optimize(source, self)
source = generate(source, self, name, filename)
if raw:
return source
if filename is None:
@ -417,6 +419,48 @@ class Environment(object):
filename = filename.encode('utf-8')
return compile(source, filename, 'exec')
def compile_expression(self, source, undefined_to_none=True):
"""A handy helper method that returns a callable that accepts keyword
arguments that appear as variables in the expression. If called it
returns the result of the expression.
This is useful if applications want to use the same rules as Jinja
in template "configuration files" or similar situations.
Example usage:
>>> env = Environment()
>>> expr = env.compile_expression('foo == 42')
>>> expr(foo=23)
False
>>> expr(foo=42)
True
Per default the return value is converted to `None` if the
expression returns an undefined value. This can be changed
by setting `undefined_to_none` to `False`.
>>> env.compile_expression('var')() is None
True
>>> env.compile_expression('var', undefined_to_none=False)()
Undefined
**new in Jinja 2.1**
"""
parser = Parser(self, source, state='variable')
try:
expr = parser.parse_expression()
if not parser.stream.eos:
raise TemplateSyntaxError('chunk after expression',
parser.stream.current.lineno,
None, None)
except TemplateSyntaxError, e:
e.source = source
raise e
body = [nodes.Assign(nodes.Name('result', 'store'), expr, lineno=1)]
template = self.from_string(nodes.Template(body, lineno=1))
return TemplateExpression(template, undefined_to_none)
def join_path(self, template, parent):
"""Join a template with the parent. By default all the lookups are
relative to the loader root so this method returns the `template`
@ -699,6 +743,25 @@ class TemplateModule(object):
return '<%s %s>' % (self.__class__.__name__, name)
class TemplateExpression(object):
"""The :meth:`jinja2.Environment.compile_expression` method returns an
instance of this object. It encapsulates the expression-like access
to the template with an expression it wraps.
"""
def __init__(self, template, undefined_to_none):
self._template = template
self._undefined_to_none = undefined_to_none
def __call__(self, *args, **kwargs):
context = self._template.new_context(dict(*args, **kwargs))
consume(self._template.root_render_func(context))
rv = context.vars['result']
if self._undefined_to_none and isinstance(rv, Undefined):
rv = None
return rv
class TemplateStream(object):
"""A template stream works pretty much like an ordinary python generator
but it can buffer multiple items to reduce the number of total iterations.

View File

@ -375,10 +375,10 @@ class Lexer(object):
"""Called for strings and template data to normlize it to unicode."""
return newline_re.sub(self.newline_sequence, value)
def tokenize(self, source, name=None, filename=None):
def tokenize(self, source, name=None, filename=None, state=None):
"""Calls tokeniter + tokenize and wraps it in a token stream.
"""
stream = self.tokeniter(source, name, filename)
stream = self.tokeniter(source, name, filename, state)
return TokenStream(self.wrap(stream, name, filename), name, filename)
def wrap(self, stream, name=None, filename=None):
@ -426,7 +426,7 @@ class Lexer(object):
token = operators[value]
yield Token(lineno, token, value)
def tokeniter(self, source, name, filename=None):
def tokeniter(self, source, name, filename=None, state=None):
"""This method tokenizes the text and returns the tokens in a
generator. Use this method if you just want to tokenize a template.
"""
@ -434,7 +434,12 @@ class Lexer(object):
pos = 0
lineno = 1
stack = ['root']
statetokens = self.rules['root']
if state is not None and state != 'root':
assert state in ('variable', 'block'), 'invalid state'
stack.append(state + '_begin')
else:
state = 'root'
statetokens = self.rules[stack[-1]]
source_length = len(source)
balancing_stack = []

View File

@ -262,11 +262,6 @@ class CallBlock(Stmt):
fields = ('call', 'args', 'defaults', 'body')
class Set(Stmt):
"""Allows defining own variables."""
fields = ('name', 'expr')
class FilterBlock(Stmt):
"""Node for filter sections."""
fields = ('body', 'filter')

View File

@ -23,9 +23,10 @@ class Parser(object):
extensions and can be used to parse expressions or statements.
"""
def __init__(self, environment, source, name=None, filename=None):
def __init__(self, environment, source, name=None, filename=None,
state=None):
self.environment = environment
self.stream = environment._tokenize(source, name, filename)
self.stream = environment._tokenize(source, name, filename, state)
self.name = name
self.filename = filename
self.closed = False

View File

@ -136,6 +136,12 @@ def is_undefined(obj):
return isinstance(obj, Undefined)
def consume(iterable):
"""Consumes an iterable without doing anything with it."""
for event in iterable:
pass
def clear_caches():
"""Jinja2 keeps internal caches for environments and lexers. These are
used so that Jinja2 doesn't have to recreate environments and lexers all

View File

@ -8,7 +8,7 @@
"""
import gc
from py.test import raises
from jinja2 import escape
from jinja2 import escape, is_undefined
from jinja2.utils import Cycler
from jinja2.exceptions import TemplateSyntaxError
@ -97,3 +97,14 @@ def test_cycler():
assert c.current == 2
c.reset()
assert c.current == 1
def test_expressions(env):
expr = env.compile_expression("foo")
assert expr() is None
assert expr(foo=42) == 42
expr2 = env.compile_expression("foo", undefined_to_none=False)
assert is_undefined(expr2())
expr = env.compile_expression("42 + foo")
assert expr(foo=42) == 84