:copyright: 2007 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
+import re
import sys
from compiler import ast
from jinja import nodes
from jinja.parser import Parser
from jinja.exceptions import TemplateSyntaxError
from jinja.translators import Translator
-from jinja.utils import translate_exception, capture_generator
+from jinja.utils import translate_exception, capture_generator, \
+ RUNTIME_EXCEPTION_OFFSET
+
+
+#: regular expression for the debug symbols
+_debug_re = re.compile(r'^\s*\# DEBUG\(filename=(?P<filename>.*?), '
+ r'lineno=(?P<lineno>\d+)\)$')
def _to_tuple(args):
Represents a finished template.
"""
- def __init__(self, environment, code, translated_source=None):
+ def __init__(self, environment, code):
self.environment = environment
self.code = code
- self.translated_source = translated_source
self.generate_func = None
+ #: holds debug information
+ self._debug_info = None
+ #: unused in loader environments but used when a template
+ #: is loaded from a string in order to be able to debug those too
+ self._source = None
def dump(self, stream=None):
"""Dump the template into python bytecode."""
if stream is not None:
from marshal import dump
- dump((self.code, self.translated_source), stream)
+ dump(self.code, stream)
else:
from marshal import dumps
- return dumps((self.code, self.translated_source))
+ return dumps(self.code)
def load(environment, data):
"""Load the template from python bytecode."""
if isinstance(data, basestring):
from marshal import loads
- code, src = loads(data)
+ code = loads(data)
else:
from marshal import load
- code, src = load(data)
- return Template(environment, code, src)
+ code = load(data)
+ return Template(environment, code)
load = staticmethod(load)
def render(self, *args, **kwargs):
"""Render a template."""
+ # if there is no generation function we execute the code
+ # in a new namespace and save the generation function and
+ # debug information.
if self.generate_func is None:
ns = {'environment': self.environment}
exec self.code in ns
self.generate_func = ns['generate']
+ self._debug_info = ns['debug_info']
ctx = self.environment.context_class(self.environment, *args, **kwargs)
try:
return capture_generator(self.generate_func(ctx))
except:
+ # debugging system:
+ # on any exception we first get the current exception information
+ # and skip the internal frames (currently either one (python2.5)
+ # or two (python2.4 and lower)). After that we call a function
+ # that creates a new traceback that is easier to debug.
exc_type, exc_value, traceback = sys.exc_info()
- # translate the exception, We skip two frames. One
- # frame that is the "capture_generator" frame, and another
- # one which is the frame of this function
+ for _ in xrange(RUNTIME_EXCEPTION_OFFSET):
+ traceback = traceback.tb_next
traceback = translate_exception(self, exc_type, exc_value,
- traceback.tb_next.tb_next, ctx)
+ traceback, ctx)
raise exc_type, exc_value, traceback
filename = node.filename or '<template>'
source = translator.translate()
return Template(environment,
- compile(source, filename, 'exec'),
- source)
+ compile(source, filename, 'exec'))
process = staticmethod(process)
# -- private helper methods
Return a comment that helds the node informations or None
if there is no need to add a debug comment.
"""
- if node.filename is None:
- return
- rv = '# DEBUG(filename=%s, lineno=%s)' % (
- node.filename,
+ return '# DEBUG(filename=%s, lineno=%s)' % (
+ node.filename or '',
node.lineno
)
- if force or rv != self.last_debug_comment:
- self.last_debug_comment = rv
- return rv
def filter(self, s, filter_nodes):
"""
def reset(self):
self.indention = 0
self.last_cycle_id = 0
- self.last_debug_comment = None
def translate(self):
self.reset()
lines = [
'from __future__ import division\n'
'from jinja.datastructure import Undefined, LoopContext, '
- 'CycleContext, BlockContext\n'
+ 'CycleContext, SuperBlock\n'
'from jinja.utils import buffereater\n'
'%s\n'
'__name__ = %r\n\n'
' if False:',
' yield None'
])
- nodeinfo = self.nodeinfo(item, True)
- if nodeinfo:
- lines.append(self.indent(nodeinfo))
+ lines.append(self.indent(self.nodeinfo(item, True)))
lines.append(self.handle_block(item, idx + 1))
tmp.append('buffereater(%s)' % func_name)
dict_lines.append(' %r: %s' % (
))
lines.append('\nblocks = {\n%s\n}' % ',\n'.join(dict_lines))
- return '\n'.join(lines)
+ # now get the real source lines and map the debugging symbols
+ debug_mapping = []
+ last = None
+ offset = -1
+ sourcelines = ('\n'.join(lines)).splitlines()
+ result = []
+
+ for idx, line in enumerate(sourcelines):
+ m = _debug_re.search(line)
+ if m is not None:
+ d = m.groupdict()
+ this = (d['filename'] or None, int(d['lineno']))
+ # if there is no filename in this debug symbol
+ # if it's the same as the line before we ignore it
+ if this[0] and this != last:
+ debug_mapping.append((idx - offset,) + this)
+ last = this
+ # for each debug symbol the line number and so the offset
+ # changes by one.
+ offset += 1
+ else:
+ result.append(line)
+
+ result.append('\ndebug_info = %r' % debug_mapping)
+ return '\n'.join(result)
def handle_template_text(self, node):
"""
Handle data around nodes.
"""
- nodeinfo = self.nodeinfo(node) or ''
- if nodeinfo:
- nodeinfo = self.indent(nodeinfo) + '\n'
- return nodeinfo + self.indent('yield %r' % node.text)
+ return self.indent(self.nodeinfo(node)) + '\n' +\
+ self.indent('yield %r' % node.text)
def handle_node_list(self, node):
"""
In some situations we might have a node list. It's just
a collection of multiple statements.
"""
- buf = [self.handle_node(n) for n in node]
- if buf:
- nodeinfo = self.nodeinfo(node)
- if nodeinfo:
- buf.insert(0, self.indent(nodeinfo))
- return '\n'.join(buf)
+ return '\n'.join([self.indent(self.nodeinfo(node))] +
+ [self.handle_node(n) for n in node])
def handle_for_loop(self, node):
"""
"""
buf = []
write = lambda x: buf.append(self.indent(x))
- nodeinfo = self.nodeinfo(node)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node))
write('ctx_push()')
# recursive loops
# handle real loop code
self.indention += 1
- nodeinfo = self.nodeinfo(node.body)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node.body))
buf.append(self.handle_node(node.body) or self.indent('pass'))
self.indention -= 1
if node.else_:
write('if not context[\'loop\'].iterated:')
self.indention += 1
- nodeinfo = self.nodeinfo(node.else_)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node.else_))
buf.append(self.handle_node(node.else_) or self.indent('pass'))
self.indention -= 1
"""
buf = []
write = lambda x: buf.append(self.indent(x))
- nodeinfo = self.nodeinfo(node)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node))
for idx, (test, body) in enumerate(node.tests):
write('%sif %s:' % (
idx and 'el' or '',
self.handle_node(test)
))
self.indention += 1
- nodeinfo = self.nodeinfo(body)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(body))
buf.append(self.handle_node(body))
self.indention -= 1
if node.else_ is not None:
write('else:')
self.indention += 1
- nodeinfo = self.nodeinfo(node.else_)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node.else_))
buf.append(self.handle_node(node.else_))
self.indention -= 1
return '\n'.join(buf)
write('if not %r in context.current:' % name)
self.indention += 1
- nodeinfo = self.nodeinfo(node)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node))
if node.seq.__class__ in (ast.Tuple, ast.List):
write('context.current[%r] = CycleContext(%s)' % (
name,
"""
Handle a print statement.
"""
- nodeinfo = self.nodeinfo(node) or ''
- if nodeinfo:
- nodeinfo = self.indent(nodeinfo) + '\n'
- return nodeinfo + self.indent('yield finish_var(%s, context)' %
- self.handle_node(node.variable))
+ return self.indent(self.nodeinfo(node)) + '\n' +\
+ self.indent('yield finish_var(%s, context)' %
+ self.handle_node(node.variable))
def handle_macro(self, node):
"""
write('def macro(*args):')
self.indention += 1
- nodeinfo = self.nodeinfo(node)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node))
if node.arguments:
write('argcount = len(args)')
else:
write('ctx_push()')
- nodeinfo = self.nodeinfo(node.body)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node.body))
buf.append(self.handle_node(node.body))
write('ctx_pop()')
write('if False:')
"""
Handle variable assignments.
"""
- nodeinfo = self.nodeinfo(node) or ''
- if nodeinfo:
- nodeinfo = self.indent(nodeinfo) + '\n'
- return nodeinfo + self.indent('context[%r] = %s' % (
+ return self.indent(self.nodeinfo(node)) + '\n' + \
+ self.indent('context[%r] = %s' % (
node.name,
self.handle_node(node.expr)
))
write('def filtered():')
self.indention += 1
write('ctx_push()')
- nodeinfo = self.nodeinfo(node.body)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node.body))
buf.append(self.handle_node(node.body))
write('ctx_pop()')
write('if False:')
buf = []
write = lambda x: buf.append(self.indent(x))
- write('ctx_push({\'super\': BlockContext(%r, blocks[%r], %r, context)})' % (
- str(node.name),
+ write(self.nodeinfo(node))
+ write('ctx_push({\'super\': SuperBlock(%r, blocks, %r, context)})' % (
str(node.name),
level
))
- nodeinfo = self.nodeinfo(node.body)
- if nodeinfo:
- write(nodeinfo)
+ write(self.nodeinfo(node.body))
buf.append(self.handle_node(node.body))
write('ctx_pop()')
return '\n'.join(buf)
replacements = '{%s}' % ', '.join(replacements)
else:
replacements = 'None'
- nodeinfo = self.nodeinfo(node) or ''
- if nodeinfo:
- nodeinfo = self.indent(nodeinfo) + '\n'
- return nodeinfo + self.indent('yield translate(%r, %r, %r, %s)' % (
+ return self.indent(self.nodeinfo(node)) + '\n' +\
+ self.indent('yield translate(%r, %r, %r, %s)' % (
node.singular,
node.plural,
node.indicator,
from compiler.ast import CallFunc, Name, Const
from jinja.nodes import Trans
from jinja.datastructure import Context, TemplateData
-from jinja.exceptions import SecurityException
+from jinja.exceptions import SecurityException, TemplateNotFound
try:
from collections import deque
#: number of maximal range items
MAX_RANGE = 1000000
-_debug_info_re = re.compile(r'^\s*\# DEBUG\(filename=(.*?), lineno=(.*?)\)$')
-
_integer_re = re.compile('^(\d+)$')
_word_split_re = re.compile(r'(\s+)')
return range(start, stop, step)
+def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
+ """
+ Generate some lorem impsum for the template.
+ """
+ from jinja.constants import LOREM_IPSUM_WORDS
+ from random import choice, random, randrange
+ words = LOREM_IPSUM_WORDS.split()
+ result = []
+
+ for _ in xrange(n):
+ next_capitalized = True
+ last_comma = last_fullstop = 0
+ word = None
+ last = None
+ p = []
+
+ # each paragraph contains out of 20 to 100 words.
+ for idx, _ in enumerate(xrange(randrange(min, max))):
+ while True:
+ word = choice(words)
+ if word != last:
+ last = word
+ break
+ if next_capitalized:
+ word = word.capitalize()
+ next_capitalized = False
+ # add commas
+ if idx - randrange(3, 8) > last_comma:
+ last_comma = idx
+ last_fullstop += 2
+ word += ','
+ # add end of sentences
+ if idx - randrange(10, 20) > last_fullstop:
+ last_comma = last_fullstop = idx
+ word += '.'
+ next_capitalized = True
+ p.append(word)
+
+ # ensure that the paragraph ends with a dot.
+ p = u' '.join(p)
+ if p.endswith(','):
+ p = p[:-1] + '.'
+ elif not p.endswith('.'):
+ p += '.'
+ result.append(p)
+
+ if not html:
+ return u'\n\n'.join(result)
+ return u'\n'.join([u'<p>%s</p>' % escape(x) for x in result])
+
+
# python2.4 and lower has a bug regarding joining of broken generators
+# because of the runtime debugging system we have to keep track of the
+# number of frames to skip. that's what RUNTIME_EXCEPTION_OFFSET is for.
if sys.version_info < (2, 5):
capture_generator = lambda gen: u''.join(tuple(gen))
+ RUNTIME_EXCEPTION_OFFSET = 2
# this should be faster and used in python2.5 and higher
else:
capture_generator = u''.join
+ RUNTIME_EXCEPTION_OFFSET = 1
def buffereater(f):
return wrapped
-def fake_template_exception(exception, filename, lineno, context_or_env):
+def fake_template_exception(exception, filename, lineno, template,
+ context_or_env):
"""
Raise an exception "in a template". Return a traceback
object. This is used for runtime debugging, not compile time.
globals = {
'__name__': filename,
'__file__': filename,
- '__loader__': TracebackLoader(env, filename),
+ '__loader__': TracebackLoader(env, template, filename),
'__exception_to_raise__': exception
}
try:
"""
Translate an exception and return the new traceback.
"""
- sourcelines = template.translated_source.splitlines()
- startpos = traceback.tb_lineno - 1
- filename = None
- # looks like we loaded the template from string. we cannot
- # do anything here.
- if startpos > len(sourcelines):
- return traceback
-
- while startpos > 0:
- m = _debug_info_re.search(sourcelines[startpos])
- if m is not None:
- filename, lineno = m.groups()
- if filename == 'None':
- filename = None
- if lineno != 'None':
- lineno = int(lineno)
+ error_line = traceback.tb_lineno
+ for code_line, tmpl_filename, tmpl_line in template._debug_info[::-1]:
+ if code_line <= error_line:
break
- startpos -= 1
-
- # no traceback information found, reraise unchanged
- if not filename:
+ else:
+ # no debug symbol found. give up
return traceback
- return fake_template_exception(exc_value, filename,
- lineno, context)[2]
+ return fake_template_exception(exc_value, tmpl_filename, tmpl_line,
+ template, context)[2]
def raise_syntax_error(exception, env):
the traceback.
"""
exc_info = fake_template_exception(exception, exception.filename,
- exception.lineno, env)
+ exception.lineno, None, env)
raise exc_info[0], exc_info[1], exc_info[2]
Fake importer that just returns the source of a template.
"""
- def __init__(self, environment, filename):
+ def __init__(self, environment, template, filename):
self.loader = environment.loader
+ self.template = template
self.filename = filename
def get_source(self, impname):
- return self.loader.get_source(self.filename)
+ if self.loader is not None:
+ try:
+ return self.loader.get_source(self.filename)
+ except TemplateNotFound:
+ pass
+ if self.template is not None:
+ return self.template._source or ''
+ return ''
class CacheDict(object):