blob: c15519f40a9c6352f442f0b9e508b7a26ac064e5 [file] [log] [blame]
# -*- coding: utf-8 -*-
"""
jinja.datastructure
~~~~~~~~~~~~~~~~~~~
Module that helds several data types used in the template engine.
:copyright: 2007 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
# sets
try:
set
except NameError:
from sets import Set as set
from jinja.exceptions import TemplateRuntimeError
class UndefinedType(object):
"""
An object that does not exist.
"""
__slots__ = ()
def __init__(self):
try:
Undefined
except NameError:
pass
else:
raise TypeError('cannot create %r instances' %
self.__class__.__name__)
def __add__(self, other):
return other
__sub__ = __mul__ = __div__ = __add__
def __getitem__(self, arg):
return self
def __iter__(self):
return iter(int, 0)
def __getattr__(self, arg):
return self
def __nonzero__(self):
return False
def __len__(self):
return 0
def __str__(self):
return ''
def __unicode__(self):
return u''
def __int__(self):
return 0
def __float__(self):
return 1
def __call__(self, *args, **kwargs):
return self
Undefined = UndefinedType()
class FakeTranslator(object):
"""
Default null translator.
"""
def gettext(self, s):
return s
def ngettext(self, s, p, n):
if n == 1:
return s
return p
class Deferred(object):
"""
Object marking an deferred value. Deferred objects are
objects that are called first access in the context.
"""
def __init__(self, factory):
self.factory = factory
def __call__(self, context, name):
return self.factory(context, name)
class Markup(unicode):
"""
Mark a string as safe for XML. If the environment uses the
auto_escape option values marked as `Markup` aren't escaped.
"""
def __repr__(self):
return 'Markup(%s)' % unicode.__repr__(self)
class Context(object):
"""
Dict like object.
"""
def __init__(self, _environment_, *args, **kwargs):
self.environment = _environment_
self._stack = [_environment_.globals, dict(*args, **kwargs), {}]
self.globals, self.initial, self.current = self._stack
# cache object used for filters and tests
self.cache = {}
def get_translator(self):
"""Return the translator for i18n."""
return FakeTranslator()
def pop(self):
"""Pop the last layer from the stack and return it."""
rv = self._stack.pop()
self.current = self._stack[-1]
return rv
def push(self, data=None):
"""Push a new dict or empty layer to the stack and return that layer"""
data = data or {}
self._stack.append(data)
self.current = self._stack[-1]
return data
def to_dict(self):
"""Convert the context into a dict. This skips the globals."""
result = {}
for layer in self._stack[1:]:
for key, value in layer.iteritems():
if key.startswith('::'):
continue
result[key] = value
return result
def __getitem__(self, name):
# don't give access to jinja internal variables
if name.startswith('::'):
return Undefined
# because the stack is usually quite small we better use [::-1]
# which is faster than reversed() somehow.
for d in self._stack[::-1]:
if name in d:
rv = d[name]
if isinstance(rv, Deferred):
rv = rv(self, name)
# never touch the globals!
if d is self.globals:
self.initial[name] = rv
else:
d[name] = rv
return rv
return Undefined
def __setitem__(self, name, value):
self.current[name] = value
def __delitem__(self, name):
if name in self.current:
del self.current[name]
def __repr__(self):
tmp = {}
for d in self._stack:
for key, value in d.iteritems():
tmp[key] = value
return 'Context(%s)' % repr(tmp)
class LoopContext(object):
"""
Simple class that provides special loop variables.
Used by `Environment.iterate`.
"""
jinja_allowed_attributes = ['index', 'index0', 'length', 'parent',
'even', 'odd', 'revindex0', 'revindex',
'first', 'last']
def __init__(self, seq, parent, loop_function):
self.loop_function = loop_function
self.parent = parent
self._stack = []
if seq is not None:
self.push(seq)
def push(self, seq):
"""
Push a sequence to the loop stack. This is used by the
recursive for loop.
"""
if seq in (Undefined, None):
seq = ()
self._stack.append({
'index': -1,
'seq': seq,
'length': len(seq)
})
return self
def pop(self):
"""Remove the last layer from the loop stack."""
return self._stack.pop()
iterated = property(lambda s: s._stack[-1]['index'] > -1)
index0 = property(lambda s: s._stack[-1]['index'])
index = property(lambda s: s._stack[-1]['index'] + 1)
revindex0 = property(lambda s: s._stack[-1]['length'] - s._stack[-1]['index'] - 1)
revindex = property(lambda s: s._stack[-1]['length'] - s._stack[-1]['index'])
length = property(lambda s: s._stack[-1]['length'])
even = property(lambda s: s._stack[-1]['index'] % 2 == 0)
odd = property(lambda s: s._stack[-1]['index'] % 2 == 1)
first = property(lambda s: s._stack[-1]['index'] == 0)
last = property(lambda s: s._stack[-1]['index'] == s._stack[-1]['length'] - 1)
def __iter__(self):
s = self._stack[-1]
for idx, item in enumerate(s['seq']):
s['index'] = idx
yield item
def __len__(self):
return self._stack[-1]['length']
def __call__(self, seq):
if self.loop_function is not None:
return self.loop_function(seq)
raise TemplateRuntimeError('In order to make loops callable you have '
'to define them with the "recursive" '
'modifier.')
def __repr__(self):
if self._stack:
return '<LoopContext %d/%d%s>' % (
self.index,
self.length,
self.loop_function is not None and ' recursive' or ''
)
return '<LoopContext (empty)>'
class CycleContext(object):
"""
Helper class used for cycling.
"""
def __init__(self, seq=None):
self.lineno = -1
# bind the correct helper function based on the constructor signature
if seq is not None:
self.seq = seq
self.length = len(seq)
self.cycle = self.cycle_static
else:
self.cycle = self.cycle_dynamic
def cycle_static(self):
"""Helper function for static cycling."""
self.lineno = (self.lineno + 1) % self.length
return self.seq[self.lineno]
def cycle_dynamic(self, seq):
"""Helper function for dynamic cycling."""
self.lineno = (self.lineno + 1) % len(seq)
return seq[self.lineno]
class TokenStream(object):
"""
A token stream works like a normal generator just that
it supports pushing tokens back to the stream.
"""
def __init__(self, generator):
self._generator = generator
self._pushed = []
self.last = (1, 'initial', '')
def __iter__(self):
return self
def __nonzero__(self):
"""Are we at the end of the tokenstream?"""
if self._pushed:
return True
try:
self.push(self.next())
except StopIteration:
return False
return True
eos = property(lambda x: not x.__nonzero__(), doc=__nonzero__.__doc__)
def next(self):
"""Return the next token from the stream."""
if self._pushed:
rv = self._pushed.pop()
else:
rv = self._generator.next()
self.last = rv
return rv
def look(self):
"""Pop and push a token, return it."""
token = self.next()
self.push(*token)
return token
def fetch_until(self, test, drop_needle=False):
"""Fetch tokens until a function matches."""
try:
while True:
token = self.next()
if test(*token):
if not drop_needle:
self.push(*token)
return
else:
yield token
except StopIteration:
raise IndexError('end of stream reached')
def drop_until(self, test, drop_needle=False):
"""Fetch tokens until a function matches and drop all
tokens."""
for token in self.fetch_until(test, drop_needle):
pass
def push(self, lineno, token, data):
"""Push an yielded token back to the stream."""
self._pushed.append((lineno, token, data))