| # -*- coding: utf-8 -*- |
| """ |
| jinja2.debug |
| ~~~~~~~~~~~~ |
| |
| Implements the debug interface for Jinja. This module does some pretty |
| ugly stuff with the Python traceback system in order to achieve tracebacks |
| with correct line numbers, locals and contents. |
| |
| :copyright: (c) 2010 by the Jinja Team. |
| :license: BSD, see LICENSE for more details. |
| """ |
| import sys |
| import traceback |
| from types import TracebackType |
| from jinja2.utils import missing, internal_code |
| from jinja2.exceptions import TemplateSyntaxError |
| from jinja2._compat import iteritems, reraise, code_type |
| |
| # on pypy we can take advantage of transparent proxies |
| try: |
| from __pypy__ import tproxy |
| except ImportError: |
| tproxy = None |
| |
| |
| # how does the raise helper look like? |
| try: |
| exec("raise TypeError, 'foo'") |
| except SyntaxError: |
| raise_helper = 'raise __jinja_exception__[1]' |
| except TypeError: |
| raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]' |
| |
| |
| class TracebackFrameProxy(object): |
| """Proxies a traceback frame.""" |
| |
| def __init__(self, tb): |
| self.tb = tb |
| self._tb_next = None |
| |
| @property |
| def tb_next(self): |
| return self._tb_next |
| |
| def set_next(self, next): |
| if tb_set_next is not None: |
| try: |
| tb_set_next(self.tb, next and next.tb or None) |
| except Exception: |
| # this function can fail due to all the hackery it does |
| # on various python implementations. We just catch errors |
| # down and ignore them if necessary. |
| pass |
| self._tb_next = next |
| |
| @property |
| def is_jinja_frame(self): |
| return '__jinja_template__' in self.tb.tb_frame.f_globals |
| |
| def __getattr__(self, name): |
| return getattr(self.tb, name) |
| |
| |
| def make_frame_proxy(frame): |
| proxy = TracebackFrameProxy(frame) |
| if tproxy is None: |
| return proxy |
| def operation_handler(operation, *args, **kwargs): |
| if operation in ('__getattribute__', '__getattr__'): |
| return getattr(proxy, args[0]) |
| elif operation == '__setattr__': |
| proxy.__setattr__(*args, **kwargs) |
| else: |
| return getattr(proxy, operation)(*args, **kwargs) |
| return tproxy(TracebackType, operation_handler) |
| |
| |
| class ProcessedTraceback(object): |
| """Holds a Jinja preprocessed traceback for printing or reraising.""" |
| |
| def __init__(self, exc_type, exc_value, frames): |
| assert frames, 'no frames for this traceback?' |
| self.exc_type = exc_type |
| self.exc_value = exc_value |
| self.frames = frames |
| |
| # newly concatenate the frames (which are proxies) |
| prev_tb = None |
| for tb in self.frames: |
| if prev_tb is not None: |
| prev_tb.set_next(tb) |
| prev_tb = tb |
| prev_tb.set_next(None) |
| |
| def render_as_text(self, limit=None): |
| """Return a string with the traceback.""" |
| lines = traceback.format_exception(self.exc_type, self.exc_value, |
| self.frames[0], limit=limit) |
| return ''.join(lines).rstrip() |
| |
| def render_as_html(self, full=False): |
| """Return a unicode string with the traceback as rendered HTML.""" |
| from jinja2.debugrenderer import render_traceback |
| return u'%s\n\n<!--\n%s\n-->' % ( |
| render_traceback(self, full=full), |
| self.render_as_text().decode('utf-8', 'replace') |
| ) |
| |
| @property |
| def is_template_syntax_error(self): |
| """`True` if this is a template syntax error.""" |
| return isinstance(self.exc_value, TemplateSyntaxError) |
| |
| @property |
| def exc_info(self): |
| """Exception info tuple with a proxy around the frame objects.""" |
| return self.exc_type, self.exc_value, self.frames[0] |
| |
| @property |
| def standard_exc_info(self): |
| """Standard python exc_info for re-raising""" |
| tb = self.frames[0] |
| # the frame will be an actual traceback (or transparent proxy) if |
| # we are on pypy or a python implementation with support for tproxy |
| if type(tb) is not TracebackType: |
| tb = tb.tb |
| return self.exc_type, self.exc_value, tb |
| |
| |
| def make_traceback(exc_info, source_hint=None): |
| """Creates a processed traceback object from the exc_info.""" |
| exc_type, exc_value, tb = exc_info |
| if isinstance(exc_value, TemplateSyntaxError): |
| exc_info = translate_syntax_error(exc_value, source_hint) |
| initial_skip = 0 |
| else: |
| initial_skip = 1 |
| return translate_exception(exc_info, initial_skip) |
| |
| |
| def translate_syntax_error(error, source=None): |
| """Rewrites a syntax error to please traceback systems.""" |
| error.source = source |
| error.translated = True |
| exc_info = (error.__class__, error, None) |
| filename = error.filename |
| if filename is None: |
| filename = '<unknown>' |
| return fake_exc_info(exc_info, filename, error.lineno) |
| |
| |
| def translate_exception(exc_info, initial_skip=0): |
| """If passed an exc_info it will automatically rewrite the exceptions |
| all the way down to the correct line numbers and frames. |
| """ |
| tb = exc_info[2] |
| frames = [] |
| |
| # skip some internal frames if wanted |
| for x in range(initial_skip): |
| if tb is not None: |
| tb = tb.tb_next |
| initial_tb = tb |
| |
| while tb is not None: |
| # skip frames decorated with @internalcode. These are internal |
| # calls we can't avoid and that are useless in template debugging |
| # output. |
| if tb.tb_frame.f_code in internal_code: |
| tb = tb.tb_next |
| continue |
| |
| # save a reference to the next frame if we override the current |
| # one with a faked one. |
| next = tb.tb_next |
| |
| # fake template exceptions |
| template = tb.tb_frame.f_globals.get('__jinja_template__') |
| if template is not None: |
| lineno = template.get_corresponding_lineno(tb.tb_lineno) |
| tb = fake_exc_info(exc_info[:2] + (tb,), template.filename, |
| lineno)[2] |
| |
| frames.append(make_frame_proxy(tb)) |
| tb = next |
| |
| # if we don't have any exceptions in the frames left, we have to |
| # reraise it unchanged. |
| # XXX: can we backup here? when could this happen? |
| if not frames: |
| reraise(exc_info[0], exc_info[1], exc_info[2]) |
| |
| return ProcessedTraceback(exc_info[0], exc_info[1], frames) |
| |
| |
| def fake_exc_info(exc_info, filename, lineno): |
| """Helper for `translate_exception`.""" |
| exc_type, exc_value, tb = exc_info |
| |
| # figure the real context out |
| if tb is not None: |
| real_locals = tb.tb_frame.f_locals.copy() |
| ctx = real_locals.get('context') |
| if ctx: |
| locals = ctx.get_all() |
| else: |
| locals = {} |
| for name, value in iteritems(real_locals): |
| if name.startswith('l_') and value is not missing: |
| locals[name[2:]] = value |
| |
| # if there is a local called __jinja_exception__, we get |
| # rid of it to not break the debug functionality. |
| locals.pop('__jinja_exception__', None) |
| else: |
| locals = {} |
| |
| # assamble fake globals we need |
| globals = { |
| '__name__': filename, |
| '__file__': filename, |
| '__jinja_exception__': exc_info[:2], |
| |
| # we don't want to keep the reference to the template around |
| # to not cause circular dependencies, but we mark it as Jinja |
| # frame for the ProcessedTraceback |
| '__jinja_template__': None |
| } |
| |
| # and fake the exception |
| code = compile('\n' * (lineno - 1) + raise_helper, filename, 'exec') |
| |
| # if it's possible, change the name of the code. This won't work |
| # on some python environments such as google appengine |
| try: |
| if tb is None: |
| location = 'template' |
| else: |
| function = tb.tb_frame.f_code.co_name |
| if function == 'root': |
| location = 'top-level template code' |
| elif function.startswith('block_'): |
| location = 'block "%s"' % function[6:] |
| else: |
| location = 'template' |
| code = code_type(0, code.co_nlocals, code.co_stacksize, |
| code.co_flags, code.co_code, code.co_consts, |
| code.co_names, code.co_varnames, filename, |
| location, code.co_firstlineno, |
| code.co_lnotab, (), ()) |
| except: |
| pass |
| |
| # execute the code and catch the new traceback |
| try: |
| exec(code, globals, locals) |
| except: |
| exc_info = sys.exc_info() |
| new_tb = exc_info[2].tb_next |
| |
| # return without this frame |
| return exc_info[:2] + (new_tb,) |
| |
| |
| def _init_ugly_crap(): |
| """This function implements a few ugly things so that we can patch the |
| traceback objects. The function returned allows resetting `tb_next` on |
| any python traceback object. Do not attempt to use this on non cpython |
| interpreters |
| """ |
| import ctypes |
| from types import TracebackType |
| |
| # figure out side of _Py_ssize_t |
| if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'): |
| _Py_ssize_t = ctypes.c_int64 |
| else: |
| _Py_ssize_t = ctypes.c_int |
| |
| # regular python |
| class _PyObject(ctypes.Structure): |
| pass |
| _PyObject._fields_ = [ |
| ('ob_refcnt', _Py_ssize_t), |
| ('ob_type', ctypes.POINTER(_PyObject)) |
| ] |
| |
| # python with trace |
| if hasattr(sys, 'getobjects'): |
| class _PyObject(ctypes.Structure): |
| pass |
| _PyObject._fields_ = [ |
| ('_ob_next', ctypes.POINTER(_PyObject)), |
| ('_ob_prev', ctypes.POINTER(_PyObject)), |
| ('ob_refcnt', _Py_ssize_t), |
| ('ob_type', ctypes.POINTER(_PyObject)) |
| ] |
| |
| class _Traceback(_PyObject): |
| pass |
| _Traceback._fields_ = [ |
| ('tb_next', ctypes.POINTER(_Traceback)), |
| ('tb_frame', ctypes.POINTER(_PyObject)), |
| ('tb_lasti', ctypes.c_int), |
| ('tb_lineno', ctypes.c_int) |
| ] |
| |
| def tb_set_next(tb, next): |
| """Set the tb_next attribute of a traceback object.""" |
| if not (isinstance(tb, TracebackType) and |
| (next is None or isinstance(next, TracebackType))): |
| raise TypeError('tb_set_next arguments must be traceback objects') |
| obj = _Traceback.from_address(id(tb)) |
| if tb.tb_next is not None: |
| old = _Traceback.from_address(id(tb.tb_next)) |
| old.ob_refcnt -= 1 |
| if next is None: |
| obj.tb_next = ctypes.POINTER(_Traceback)() |
| else: |
| next = _Traceback.from_address(id(next)) |
| next.ob_refcnt += 1 |
| obj.tb_next = ctypes.pointer(next) |
| |
| return tb_set_next |
| |
| |
| # try to get a tb_set_next implementation if we don't have transparent |
| # proxies. |
| tb_set_next = None |
| if tproxy is None: |
| try: |
| tb_set_next = _init_ugly_crap() |
| except: |
| pass |
| del _init_ugly_crap |