oohEmbed/app/jinja/datastructure.py
2009-07-18 11:32:16 +08:00

708 lines
20 KiB
Python

# -*- 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.
"""
from jinja.exceptions import TemplateSyntaxError, TemplateRuntimeError
_missing = object()
def contextcallable(f):
"""
Mark a function context callable.
"""
f.jinja_context_callable = True
return f
def unsafe(f):
"""
Mark function as unsafe.
"""
f.jinja_unsafe_call = True
return f
def make_undefined(implementation):
"""
Creates an undefined singleton based on a given implementation.
It performs some tests that make sure the undefined type implements
everything it should.
"""
self = object.__new__(implementation)
self.__reduce__()
return self
class AbstractUndefinedType(object):
"""
Base class for any undefined type.
"""
__slots__ = ()
def __init__(self):
raise TypeError('cannot create %r instances' %
self.__class__.__name__)
def __setattr__(self, name, value):
raise AttributeError('%r object has no attribute %r' % (
self.__class__.__name__,
name
))
def __eq__(self, other):
return self is other
def __ne__(self, other):
return self is not other
def __copy__(self):
return self
__deepcopy__ = __copy__
def __repr__(self):
return 'Undefined'
def __reduce__(self):
raise TypeError('undefined objects have to provide a __reduce__')
class SilentUndefinedType(AbstractUndefinedType):
"""
An object that does not exist.
"""
__slots__ = ()
def __add__(self, other):
"""Any operator returns the operand."""
return other
__sub__ = __mul__ = __div__ = __rsub__ = __rmul__ = __div__ = __mod__ =\
__radd__ = __rmod__ = __add__
def __getitem__(self, arg):
"""Getting any item returns `Undefined`"""
return self
def __iter__(self):
"""Iterating over `Undefined` returns an empty iterator."""
if False:
yield None
def __getattr__(self, arg):
"""Getting any attribute returns `Undefined`"""
return self
def __nonzero__(self):
"""`Undefined` is considered boolean `False`"""
return False
def __len__(self):
"""`Undefined` is an empty sequence"""
return 0
def __str__(self):
"""The string representation is empty."""
return ''
def __unicode__(self):
"""The unicode representation is empty."""
return u''
def __int__(self):
"""Converting `Undefined` to an integer ends up in ``0``"""
return 0
def __float__(self):
"""Converting `Undefined` to an float ends up in ``0.0``"""
return 0.0
def __call__(self, *args, **kwargs):
"""Calling `Undefined` returns `Undefined`"""
return self
def __reduce__(self):
"""Helper for pickle."""
return 'SilentUndefined'
class ComplainingUndefinedType(AbstractUndefinedType):
"""
An object that does not exist.
"""
__slots__ = ()
def __len__(self):
"""Getting the length raises error."""
raise TemplateRuntimeError('Operated on undefined object')
def __iter__(self):
"""Iterating over `Undefined` raises an error."""
raise TemplateRuntimeError('Iterated over undefined object')
def __nonzero__(self):
"""`Undefined` is considered boolean `False`"""
return False
def __str__(self):
"""The string representation raises an error."""
raise TemplateRuntimeError('Undefined object rendered')
def __unicode__(self):
"""The unicode representation raises an error."""
self.__str__()
def __call__(self, *args, **kwargs):
"""Calling `Undefined` returns `Undefined`"""
raise TemplateRuntimeError('Undefined object called')
def __reduce__(self):
"""Helper for pickle."""
return 'ComplainingUndefined'
#: the singleton instances for the undefined objects
SilentUndefined = make_undefined(SilentUndefinedType)
ComplainingUndefined = make_undefined(ComplainingUndefinedType)
#: jinja 1.0 compatibility
Undefined = SilentUndefined
UndefinedType = SilentUndefinedType
class FakeTranslator(object):
"""
Default null translator.
"""
def gettext(self, s):
"""
Translate a singular string.
"""
return s
def ngettext(self, s, p, n):
"""
Translate a plural string.
"""
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.environment, context, name)
class Markup(unicode):
"""
Compatibility for Pylons and probably some other frameworks.
It's only used in Jinja environments with `auto_escape` set
to true.
"""
def __html__(self):
return unicode(self)
class TemplateData(Markup):
"""
Subclass of unicode to mark objects that are coming from the
template. The autoescape filter can use that.
"""
# import these here because those modules import Deferred and Undefined
# from this module.
try:
# try to use the c implementation of the base context if available
from jinja._speedups import BaseContext
except ImportError:
# if there is no c implementation we go with a native python one
from jinja._native import BaseContext
class Context(BaseContext):
"""
Dict like object containing the variables for the template.
"""
def __init__(self, *args, **kwargs):
environment = args[0]
if not kwargs and len(args) == 2 and isinstance(args[1], dict):
base = args[1]
else:
base = dict(*args[1:], **kwargs)
super(Context, self).__init__(environment.undefined_singleton,
environment.globals, base)
self._translate_func = None
self.cache = {}
self.environment = environment
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 set_nonlocal(self, name, value):
"""
Set a value in an outer scope.
"""
for layer in self.stack[:0:-1]:
if name in layer:
layer[name] = value
return
self.initial[name] = value
def translate_func(self):
"""
The translation function for this context. It takes
4 parameters. The singular string, the optional plural one,
The name of the variable in the replacements dict and the
replacements dict. This is only used by the i18n system
internally the simplified version (just one argument) is
available in the template for the user too.
"""
if self._translate_func is not None:
return self._translate_func
translator = self.environment.get_translator(self)
gettext = translator.gettext
ngettext = translator.ngettext
def translate(s, p=None, n=None, r=None):
if p is None:
s = gettext(s)
else:
s = ngettext(s, p, r[n])
# apply replacement substitution only if replacements
# are given. This is the case for {% trans %}...{% endtrans %}
# but for the "_()" syntax and a trans tag without a body.
if r is not None:
return s % r
return s
translate.__doc__ = Context.translate_func.__doc__
self._translate_func = translate
return translate
translate_func = property(translate_func, doc=translate_func.__doc__)
def __repr__(self):
"""
String representation of the context.
"""
return 'Context(%r)' % self.to_dict()
def __pretty__(self, p, cycle):
if cycle:
return p.text('Context({...})')
p.begin_group(9, 'Context({')
for idx, (key, value) in enumerate(self.to_dict().iteritems()):
if idx:
p.text(',')
p.breakable()
p.pretty(key)
p.text(': ')
p.pretty(value)
p.end_group(9, '})')
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 loop_function is None:
self.push(seq)
def push(self, seq):
"""
Push a sequence to the loop stack. This is used by the
recursive for loop.
"""
# iteration over None is catched, but we don't catch iteration
# over undefined because that behavior is handled in the
# undefined singleton
if seq is None:
seq = ()
length = 0
else:
try:
length = len(seq)
except (AttributeError, TypeError):
seq = list(seq)
length = len(seq)
self._stack.append({
'index': -1,
'seq': seq,
'length': length
})
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 == 1)
odd = property(lambda s: s._stack[-1]['index'] % 2 == 0)
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.pos = -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.pos = (self.pos + 1) % self.length
return self.seq[self.pos]
def cycle_dynamic(self, seq):
"""Helper function for dynamic cycling."""
self.pos = pos = (self.pos + 1) % len(seq)
return seq[pos]
class SuperBlock(object):
"""
Helper class for ``{{ super() }}``.
"""
jinja_allowed_attributes = ['name']
def __init__(self, name, blocks, level, context):
self.name = name
self.context = context
if name in blocks:
self.stack = blocks[name]
self.level = level
else:
self.stack = None
def __call__(self, offset=1):
if self.stack is not None:
level = self.level + (offset - 1)
if level < len(self.stack):
return self.stack[level](self.context)
raise TemplateRuntimeError('no super block for %r' % self.name)
def __repr__(self):
return '<SuperBlock %r>' % self.name
class StateTest(object):
"""
Wrapper class for basic lambdas in order to simplify
debugging in the parser. It also provides static helper
functions that replace some lambda expressions
"""
def __init__(self, func, msg):
self.func = func
self.msg = msg
def __call__(self, token):
return self.func(token)
def expect_token(*types, **kw):
"""Scans until one of the given tokens is found."""
msg = kw.pop('msg', None)
if kw:
raise TypeError('unexpected keyword argument %r' % iter(kw).next())
if len(types) == 1:
if msg is None:
msg = "expected '%s'" % types[0]
return StateTest(lambda t: t.type == types[0], msg)
if msg is None:
msg = 'expected one of %s' % ', '.join(["'%s'" % type
for type in types])
return StateTest(lambda t: t.type in types, msg)
expect_token = staticmethod(expect_token)
class Token(object):
"""
Token class.
"""
__slots__ = ('lineno', 'type', 'value')
def __init__(self, lineno, type, value):
self.lineno = lineno
self.type = intern(str(type))
self.value = value
def __str__(self):
from jinja.lexer import keywords, reverse_operators
if self.type in keywords:
return self.type
elif self.type in reverse_operators:
return reverse_operators[self.type]
return self.value
def __repr__(self):
return 'Token(%r, %r, %r)' % (
self.lineno,
self.type,
self.value
)
class TokenStreamIterator(object):
"""
The iterator for tokenstreams. Iterate over the stream
until the eof token is reached.
"""
def __init__(self, stream):
self._stream = stream
def __iter__(self):
return self
def next(self):
token = self._stream.current
if token.type == 'eof':
self._stream.close()
raise StopIteration()
self._stream.next()
return token
class TokenStream(object):
"""
A token stream wraps a generator and supports pushing tokens back.
It also provides some functions to expect tokens and similar stuff.
Important note: Do never push more than one token back to the
stream. Although the stream object won't stop you
from doing so, the behavior is undefined. Multiple
pushed tokens are only used internally!
"""
def __init__(self, generator, filename):
self._next = generator.next
self._pushed = []
self.current = Token(1, 'initial', '')
self.filename = filename
self.next()
def __iter__(self):
return TokenStreamIterator(self)
def bound(self):
"""Return True if the token stream is bound to a parser."""
return self.parser is not None
bound = property(bound, doc=bound.__doc__)
def lineno(self):
"""The current line number."""
return self.current.lineno
lineno = property(lineno, doc=lineno.__doc__)
def __nonzero__(self):
"""Are we at the end of the tokenstream?"""
return bool(self._pushed) or self.current.type != 'eof'
eos = property(lambda x: not x.__nonzero__(), doc=__nonzero__.__doc__)
def look(self):
"""See what's the next token."""
if self._pushed:
return self._pushed[-1]
old_token = self.current
self.next()
new_token = self.current
self.current = old_token
self.push(new_token)
return new_token
def push(self, token):
"""Push a token back to the stream."""
self._pushed.append(token)
def skip(self, n):
"""Got n tokens ahead."""
for x in xrange(n):
self.next()
def shift(self, token):
"""
Push one token into the stream.
"""
old_current = self.current
self.next()
self.push(self.current)
self.push(old_current)
self.push(token)
self.next()
def next(self):
"""Go one token ahead."""
if self._pushed:
self.current = self._pushed.pop()
elif self.current.type != 'eof':
try:
self.current = self._next()
except StopIteration:
self.close()
def read_whitespace(self):
"""Read all the whitespace, up to the next tag."""
lineno = self.current.lineno
buf = []
while self.current.type == 'data' and not \
self.current.value.strip():
buf.append(self.current.value)
self.next()
if buf:
return Token(lineno, 'data', u''.join(buf))
def close(self):
"""Close the stream."""
self.current = Token(self.current.lineno, 'eof', '')
self._next = None
def expect(self, token_type, token_value=_missing):
"""Expect a given token type and return it"""
if self.current.type != token_type:
raise TemplateSyntaxError("expected token %r, got %r" %
(token_type, self.current.type),
self.current.lineno,
self.filename)
elif token_value is not _missing and \
self.current.value != token_value:
raise TemplateSyntaxError("expected %r, got %r" %
(token_value, self.current.value),
self.current.lineno,
self.filename)
try:
return self.current
finally:
self.next()
class TemplateStream(object):
"""
Wraps a genererator for outputing template streams.
"""
def __init__(self, gen):
self._gen = gen
self._next = gen.next
self.buffered = False
def disable_buffering(self):
"""
Disable the output buffering.
"""
self._next = self._gen.next
self.buffered = False
def enable_buffering(self, size=5):
"""
Enable buffering. Buffer `size` items before
yielding them.
"""
if size <= 1:
raise ValueError('buffer size too small')
self.buffered = True
def buffering_next():
buf = []
c_size = 0
push = buf.append
next = self._gen.next
try:
while True:
item = next()
if item:
push(item)
c_size += 1
if c_size >= size:
raise StopIteration()
except StopIteration:
if not c_size:
raise
return u''.join(buf)
self._next = buffering_next
def __iter__(self):
return self
def next(self):
return self._next()