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 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 Version 2.0
----------- -----------
(codename jinjavitus, released on July 17th 2008) (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>`. <jinja-extensions>`.
.. autoclass:: Environment([options]) .. autoclass:: Environment([options])
:members: from_string, get_template, join_path, extend :members: from_string, get_template, join_path, extend, compile_expression
.. attribute:: shared .. attribute:: shared

View File

@ -9,6 +9,7 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import sys import sys
from jinja2 import nodes
from jinja2.defaults import * from jinja2.defaults import *
from jinja2.lexer import get_lexer, TokenStream from jinja2.lexer import get_lexer, TokenStream
from jinja2.parser import Parser from jinja2.parser import Parser
@ -16,7 +17,8 @@ from jinja2.optimizer import optimize
from jinja2.compiler import generate from jinja2.compiler import generate
from jinja2.runtime import Undefined, Context from jinja2.runtime import Undefined, Context
from jinja2.exceptions import TemplateSyntaxError 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 # 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), return reduce(lambda s, e: e.preprocess(s, name, filename),
self.extensions.itervalues(), unicode(source)) 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 """Called by the parser to do the preprocessing and filtering
for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`. for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`.
""" """
source = self.preprocess(source, name, filename) 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(): for ext in self.extensions.itervalues():
stream = ext.filter_stream(stream) stream = ext.filter_stream(stream)
if not isinstance(stream, TokenStream): if not isinstance(stream, TokenStream):
@ -407,8 +409,8 @@ class Environment(object):
if isinstance(source, basestring): if isinstance(source, basestring):
source = self.parse(source, name, filename) source = self.parse(source, name, filename)
if self.optimized: if self.optimized:
node = optimize(source, self) source = optimize(source, self)
source = generate(node, self, name, filename) source = generate(source, self, name, filename)
if raw: if raw:
return source return source
if filename is None: if filename is None:
@ -417,6 +419,48 @@ class Environment(object):
filename = filename.encode('utf-8') filename = filename.encode('utf-8')
return compile(source, filename, 'exec') 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): def join_path(self, template, parent):
"""Join a template with the parent. By default all the lookups are """Join a template with the parent. By default all the lookups are
relative to the loader root so this method returns the `template` 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) 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): class TemplateStream(object):
"""A template stream works pretty much like an ordinary python generator """A template stream works pretty much like an ordinary python generator
but it can buffer multiple items to reduce the number of total iterations. 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.""" """Called for strings and template data to normlize it to unicode."""
return newline_re.sub(self.newline_sequence, value) 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. """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) return TokenStream(self.wrap(stream, name, filename), name, filename)
def wrap(self, stream, name=None, filename=None): def wrap(self, stream, name=None, filename=None):
@ -426,7 +426,7 @@ class Lexer(object):
token = operators[value] token = operators[value]
yield Token(lineno, token, 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 """This method tokenizes the text and returns the tokens in a
generator. Use this method if you just want to tokenize a template. generator. Use this method if you just want to tokenize a template.
""" """
@ -434,7 +434,12 @@ class Lexer(object):
pos = 0 pos = 0
lineno = 1 lineno = 1
stack = ['root'] 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) source_length = len(source)
balancing_stack = [] balancing_stack = []

View File

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

View File

@ -23,9 +23,10 @@ class Parser(object):
extensions and can be used to parse expressions or statements. 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.environment = environment
self.stream = environment._tokenize(source, name, filename) self.stream = environment._tokenize(source, name, filename, state)
self.name = name self.name = name
self.filename = filename self.filename = filename
self.closed = False self.closed = False

View File

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

View File

@ -8,7 +8,7 @@
""" """
import gc import gc
from py.test import raises from py.test import raises
from jinja2 import escape from jinja2 import escape, is_undefined
from jinja2.utils import Cycler from jinja2.utils import Cycler
from jinja2.exceptions import TemplateSyntaxError from jinja2.exceptions import TemplateSyntaxError
@ -97,3 +97,14 @@ def test_cycler():
assert c.current == 2 assert c.current == 2
c.reset() c.reset()
assert c.current == 1 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