213 lines
8 KiB
Python
213 lines
8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
jinja.debugger
|
|
~~~~~~~~~~~~~~
|
|
|
|
This module implements helper function Jinja uses to give the users a
|
|
possibility to develop Jinja templates like they would debug python code.
|
|
It seamlessly integreates into the python traceback system, in fact it
|
|
just modifies the trackback stack so that the line numbers are correct
|
|
and the frame information are bound to the context and not the frame of
|
|
the template evaluation loop.
|
|
|
|
To achive this it raises the exception it cought before in an isolated
|
|
namespace at a given line. The locals namespace is set to the current
|
|
template context.
|
|
|
|
The traceback generated by raising that exception is then either returned
|
|
or linked with the former traceback if the `jinja._debugger` module is
|
|
available. Because it's not possible to modify traceback objects from the
|
|
python space this module is needed for this process.
|
|
|
|
If it's not available it just ignores the other frames. Because this can
|
|
lead to actually harder to debug code there is a setting on the jinja
|
|
environment to disable the debugging system.
|
|
|
|
The isolated namespace which is used to raise the exception also contains
|
|
a `__loader__` name that helds a reference to a PEP 302 compatible loader.
|
|
Because there are currently some traceback systems (such as the paste
|
|
evalexception debugger) that do not provide the frame globals when
|
|
retrieving the source from the linecache module, Jinja injects the source
|
|
to the linecache module itself and changes the filename to a URL style
|
|
"virtual filename" so that Jinja doesn't acidentally override other files
|
|
in the linecache.
|
|
|
|
:copyright: 2007 by Armin Ronacher.
|
|
:license: BSD, see LICENSE for more details.
|
|
"""
|
|
|
|
import sys
|
|
from random import randrange
|
|
|
|
# if we have extended debugger support we should really use it
|
|
try:
|
|
from jinja._debugger import *
|
|
has_extended_debugger = True
|
|
except ImportError:
|
|
has_extended_debugger = False
|
|
|
|
# we need the RUNTIME_EXCEPTION_OFFSET to skip the not used frames
|
|
from jinja.utils import reversed, RUNTIME_EXCEPTION_OFFSET
|
|
|
|
|
|
def fake_template_exception(exc_type, exc_value, tb, filename, lineno,
|
|
source, context_or_env, tb_back=None):
|
|
"""
|
|
Raise an exception "in a template". Return a traceback
|
|
object. This is used for runtime debugging, not compile time.
|
|
"""
|
|
# some traceback systems allow to skip frames
|
|
__traceback_hide__ = True
|
|
|
|
# create the namespace which will be the local namespace
|
|
# of the new frame then. Some debuggers show local variables
|
|
# so we better inject the context and not the evaluation loop context.
|
|
from jinja.datastructure import Context
|
|
if isinstance(context_or_env, Context):
|
|
env = context_or_env.environment
|
|
namespace = context_or_env.to_dict()
|
|
else:
|
|
env = context_or_env
|
|
namespace = {}
|
|
|
|
# no unicode for filenames
|
|
if isinstance(filename, unicode):
|
|
filename = filename.encode('utf-8')
|
|
|
|
# generate an jinja unique filename used so that linecache
|
|
# gets data that doesn't interfere with other modules
|
|
if filename is None:
|
|
vfilename = 'jinja://~%d' % randrange(0, 10000)
|
|
filename = '<string>'
|
|
else:
|
|
vfilename = 'jinja://%s' % filename
|
|
|
|
# now create the used loaded and update the linecache
|
|
loader = TracebackLoader(env, source, filename)
|
|
loader.update_linecache(vfilename)
|
|
globals = {
|
|
'__name__': vfilename,
|
|
'__file__': vfilename,
|
|
'__loader__': loader
|
|
}
|
|
|
|
# use the simple debugger to reraise the exception in the
|
|
# line where the error originally occoured
|
|
globals['__exception_to_raise__'] = (exc_type, exc_value)
|
|
offset = '\n' * (lineno - 1)
|
|
code = compile(offset + 'raise __exception_to_raise__[0], '
|
|
'__exception_to_raise__[1]',
|
|
vfilename or '<template>', 'exec')
|
|
try:
|
|
exec code in globals, namespace
|
|
except:
|
|
exc_info = sys.exc_info()
|
|
|
|
# if we have an extended debugger we set the tb_next flag so that
|
|
# we don't loose the higher stack items.
|
|
if has_extended_debugger:
|
|
if tb_back is not None:
|
|
tb_set_next(tb_back, exc_info[2])
|
|
if tb is not None:
|
|
tb_set_next(exc_info[2].tb_next, tb.tb_next)
|
|
|
|
# otherwise just return the exc_info from the simple debugger
|
|
return exc_info
|
|
|
|
|
|
def translate_exception(template, context, exc_type, exc_value, tb):
|
|
"""
|
|
Translate an exception and return the new traceback.
|
|
"""
|
|
# depending on the python version we have to skip some frames to
|
|
# step to get the frame of the current template. The frames before
|
|
# are the toolchain used to render that thing.
|
|
for x in xrange(RUNTIME_EXCEPTION_OFFSET):
|
|
tb = tb.tb_next
|
|
|
|
result_tb = prev_tb = None
|
|
initial_tb = tb
|
|
|
|
# translate all the jinja frames in this traceback
|
|
while tb is not None:
|
|
if tb.tb_frame.f_globals.get('__jinja_template__'):
|
|
debug_info = tb.tb_frame.f_globals['debug_info']
|
|
|
|
# the next thing we do is matching the current error line against the
|
|
# debugging table to get the correct source line. If we can't find the
|
|
# filename and line number we return the traceback object unaltered.
|
|
error_line = tb.tb_lineno
|
|
for code_line, tmpl_filename, tmpl_line in reversed(debug_info):
|
|
if code_line <= error_line:
|
|
source = tb.tb_frame.f_globals['template_source']
|
|
tb = fake_template_exception(exc_type, exc_value, tb,
|
|
tmpl_filename, tmpl_line,
|
|
source, context, prev_tb)[-1]
|
|
break
|
|
if result_tb is None:
|
|
result_tb = tb
|
|
prev_tb = tb
|
|
tb = tb.tb_next
|
|
|
|
# under some conditions we cannot translate any frame. in that
|
|
# situation just return the original traceback.
|
|
return (exc_type, exc_value, result_tb or intial_tb)
|
|
|
|
|
|
def raise_syntax_error(exception, env, source=None):
|
|
"""
|
|
This method raises an exception that includes more debugging
|
|
informations so that debugging works better. Unlike
|
|
`translate_exception` this method raises the exception with
|
|
the traceback.
|
|
"""
|
|
exc_info = fake_template_exception(exception, None, None,
|
|
exception.filename,
|
|
exception.lineno, source, env)
|
|
raise exc_info[0], exc_info[1], exc_info[2]
|
|
|
|
|
|
class TracebackLoader(object):
|
|
"""
|
|
Fake importer that just returns the source of a template. It's just used
|
|
by Jinja interally and you shouldn't use it on your own.
|
|
"""
|
|
|
|
def __init__(self, environment, source, filename):
|
|
self.loader = environment.loader
|
|
self.source = source
|
|
self.filename = filename
|
|
|
|
def update_linecache(self, virtual_filename):
|
|
"""
|
|
Hacky way to let traceback systems know about the
|
|
Jinja template sourcecode. Very hackish indeed.
|
|
"""
|
|
# check for linecache, not every implementation of python
|
|
# might have such an module (this check is pretty senseless
|
|
# because we depend on cpython anway)
|
|
try:
|
|
from linecache import cache
|
|
except ImportError:
|
|
return
|
|
data = self.get_source(None)
|
|
cache[virtual_filename] = (
|
|
len(data),
|
|
None,
|
|
data.splitlines(True),
|
|
virtual_filename
|
|
)
|
|
|
|
def get_source(self, impname):
|
|
"""Return the source as bytestring."""
|
|
source = ''
|
|
if self.source is not None:
|
|
source = self.source
|
|
elif self.loader is not None:
|
|
try:
|
|
source = self.loader.get_source(self.filename)
|
|
except TemplateNotFound:
|
|
pass
|
|
if isinstance(source, unicode):
|
|
source = source.encode('utf-8')
|
|
return source
|