Home | History | Annotate | Download | only in jinja2
      1 # -*- coding: utf-8 -*-
      2 """
      3     jinja2.debug
      4     ~~~~~~~~~~~~
      5 
      6     Implements the debug interface for Jinja.  This module does some pretty
      7     ugly stuff with the Python traceback system in order to achieve tracebacks
      8     with correct line numbers, locals and contents.
      9 
     10     :copyright: (c) 2010 by the Jinja Team.
     11     :license: BSD, see LICENSE for more details.
     12 """
     13 import sys
     14 import traceback
     15 from types import TracebackType
     16 from jinja2.utils import missing, internal_code
     17 from jinja2.exceptions import TemplateSyntaxError
     18 from jinja2._compat import iteritems, reraise, code_type
     19 
     20 # on pypy we can take advantage of transparent proxies
     21 try:
     22     from __pypy__ import tproxy
     23 except ImportError:
     24     tproxy = None
     25 
     26 
     27 # how does the raise helper look like?
     28 try:
     29     exec("raise TypeError, 'foo'")
     30 except SyntaxError:
     31     raise_helper = 'raise __jinja_exception__[1]'
     32 except TypeError:
     33     raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]'
     34 
     35 
     36 class TracebackFrameProxy(object):
     37     """Proxies a traceback frame."""
     38 
     39     def __init__(self, tb):
     40         self.tb = tb
     41         self._tb_next = None
     42 
     43     @property
     44     def tb_next(self):
     45         return self._tb_next
     46 
     47     def set_next(self, next):
     48         if tb_set_next is not None:
     49             try:
     50                 tb_set_next(self.tb, next and next.tb or None)
     51             except Exception:
     52                 # this function can fail due to all the hackery it does
     53                 # on various python implementations.  We just catch errors
     54                 # down and ignore them if necessary.
     55                 pass
     56         self._tb_next = next
     57 
     58     @property
     59     def is_jinja_frame(self):
     60         return '__jinja_template__' in self.tb.tb_frame.f_globals
     61 
     62     def __getattr__(self, name):
     63         return getattr(self.tb, name)
     64 
     65 
     66 def make_frame_proxy(frame):
     67     proxy = TracebackFrameProxy(frame)
     68     if tproxy is None:
     69         return proxy
     70     def operation_handler(operation, *args, **kwargs):
     71         if operation in ('__getattribute__', '__getattr__'):
     72             return getattr(proxy, args[0])
     73         elif operation == '__setattr__':
     74             proxy.__setattr__(*args, **kwargs)
     75         else:
     76             return getattr(proxy, operation)(*args, **kwargs)
     77     return tproxy(TracebackType, operation_handler)
     78 
     79 
     80 class ProcessedTraceback(object):
     81     """Holds a Jinja preprocessed traceback for printing or reraising."""
     82 
     83     def __init__(self, exc_type, exc_value, frames):
     84         assert frames, 'no frames for this traceback?'
     85         self.exc_type = exc_type
     86         self.exc_value = exc_value
     87         self.frames = frames
     88 
     89         # newly concatenate the frames (which are proxies)
     90         prev_tb = None
     91         for tb in self.frames:
     92             if prev_tb is not None:
     93                 prev_tb.set_next(tb)
     94             prev_tb = tb
     95         prev_tb.set_next(None)
     96 
     97     def render_as_text(self, limit=None):
     98         """Return a string with the traceback."""
     99         lines = traceback.format_exception(self.exc_type, self.exc_value,
    100                                            self.frames[0], limit=limit)
    101         return ''.join(lines).rstrip()
    102 
    103     def render_as_html(self, full=False):
    104         """Return a unicode string with the traceback as rendered HTML."""
    105         from jinja2.debugrenderer import render_traceback
    106         return u'%s\n\n<!--\n%s\n-->' % (
    107             render_traceback(self, full=full),
    108             self.render_as_text().decode('utf-8', 'replace')
    109         )
    110 
    111     @property
    112     def is_template_syntax_error(self):
    113         """`True` if this is a template syntax error."""
    114         return isinstance(self.exc_value, TemplateSyntaxError)
    115 
    116     @property
    117     def exc_info(self):
    118         """Exception info tuple with a proxy around the frame objects."""
    119         return self.exc_type, self.exc_value, self.frames[0]
    120 
    121     @property
    122     def standard_exc_info(self):
    123         """Standard python exc_info for re-raising"""
    124         tb = self.frames[0]
    125         # the frame will be an actual traceback (or transparent proxy) if
    126         # we are on pypy or a python implementation with support for tproxy
    127         if type(tb) is not TracebackType:
    128             tb = tb.tb
    129         return self.exc_type, self.exc_value, tb
    130 
    131 
    132 def make_traceback(exc_info, source_hint=None):
    133     """Creates a processed traceback object from the exc_info."""
    134     exc_type, exc_value, tb = exc_info
    135     if isinstance(exc_value, TemplateSyntaxError):
    136         exc_info = translate_syntax_error(exc_value, source_hint)
    137         initial_skip = 0
    138     else:
    139         initial_skip = 1
    140     return translate_exception(exc_info, initial_skip)
    141 
    142 
    143 def translate_syntax_error(error, source=None):
    144     """Rewrites a syntax error to please traceback systems."""
    145     error.source = source
    146     error.translated = True
    147     exc_info = (error.__class__, error, None)
    148     filename = error.filename
    149     if filename is None:
    150         filename = '<unknown>'
    151     return fake_exc_info(exc_info, filename, error.lineno)
    152 
    153 
    154 def translate_exception(exc_info, initial_skip=0):
    155     """If passed an exc_info it will automatically rewrite the exceptions
    156     all the way down to the correct line numbers and frames.
    157     """
    158     tb = exc_info[2]
    159     frames = []
    160 
    161     # skip some internal frames if wanted
    162     for x in range(initial_skip):
    163         if tb is not None:
    164             tb = tb.tb_next
    165     initial_tb = tb
    166 
    167     while tb is not None:
    168         # skip frames decorated with @internalcode.  These are internal
    169         # calls we can't avoid and that are useless in template debugging
    170         # output.
    171         if tb.tb_frame.f_code in internal_code:
    172             tb = tb.tb_next
    173             continue
    174 
    175         # save a reference to the next frame if we override the current
    176         # one with a faked one.
    177         next = tb.tb_next
    178 
    179         # fake template exceptions
    180         template = tb.tb_frame.f_globals.get('__jinja_template__')
    181         if template is not None:
    182             lineno = template.get_corresponding_lineno(tb.tb_lineno)
    183             tb = fake_exc_info(exc_info[:2] + (tb,), template.filename,
    184                                lineno)[2]
    185 
    186         frames.append(make_frame_proxy(tb))
    187         tb = next
    188 
    189     # if we don't have any exceptions in the frames left, we have to
    190     # reraise it unchanged.
    191     # XXX: can we backup here?  when could this happen?
    192     if not frames:
    193         reraise(exc_info[0], exc_info[1], exc_info[2])
    194 
    195     return ProcessedTraceback(exc_info[0], exc_info[1], frames)
    196 
    197 
    198 def fake_exc_info(exc_info, filename, lineno):
    199     """Helper for `translate_exception`."""
    200     exc_type, exc_value, tb = exc_info
    201 
    202     # figure the real context out
    203     if tb is not None:
    204         real_locals = tb.tb_frame.f_locals.copy()
    205         ctx = real_locals.get('context')
    206         if ctx:
    207             locals = ctx.get_all()
    208         else:
    209             locals = {}
    210         for name, value in iteritems(real_locals):
    211             if name.startswith('l_') and value is not missing:
    212                 locals[name[2:]] = value
    213 
    214         # if there is a local called __jinja_exception__, we get
    215         # rid of it to not break the debug functionality.
    216         locals.pop('__jinja_exception__', None)
    217     else:
    218         locals = {}
    219 
    220     # assamble fake globals we need
    221     globals = {
    222         '__name__':             filename,
    223         '__file__':             filename,
    224         '__jinja_exception__':  exc_info[:2],
    225 
    226         # we don't want to keep the reference to the template around
    227         # to not cause circular dependencies, but we mark it as Jinja
    228         # frame for the ProcessedTraceback
    229         '__jinja_template__':   None
    230     }
    231 
    232     # and fake the exception
    233     code = compile('\n' * (lineno - 1) + raise_helper, filename, 'exec')
    234 
    235     # if it's possible, change the name of the code.  This won't work
    236     # on some python environments such as google appengine
    237     try:
    238         if tb is None:
    239             location = 'template'
    240         else:
    241             function = tb.tb_frame.f_code.co_name
    242             if function == 'root':
    243                 location = 'top-level template code'
    244             elif function.startswith('block_'):
    245                 location = 'block "%s"' % function[6:]
    246             else:
    247                 location = 'template'
    248         code = code_type(0, code.co_nlocals, code.co_stacksize,
    249                          code.co_flags, code.co_code, code.co_consts,
    250                          code.co_names, code.co_varnames, filename,
    251                          location, code.co_firstlineno,
    252                          code.co_lnotab, (), ())
    253     except:
    254         pass
    255 
    256     # execute the code and catch the new traceback
    257     try:
    258         exec(code, globals, locals)
    259     except:
    260         exc_info = sys.exc_info()
    261         new_tb = exc_info[2].tb_next
    262 
    263     # return without this frame
    264     return exc_info[:2] + (new_tb,)
    265 
    266 
    267 def _init_ugly_crap():
    268     """This function implements a few ugly things so that we can patch the
    269     traceback objects.  The function returned allows resetting `tb_next` on
    270     any python traceback object.  Do not attempt to use this on non cpython
    271     interpreters
    272     """
    273     import ctypes
    274     from types import TracebackType
    275 
    276     # figure out side of _Py_ssize_t
    277     if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'):
    278         _Py_ssize_t = ctypes.c_int64
    279     else:
    280         _Py_ssize_t = ctypes.c_int
    281 
    282     # regular python
    283     class _PyObject(ctypes.Structure):
    284         pass
    285     _PyObject._fields_ = [
    286         ('ob_refcnt', _Py_ssize_t),
    287         ('ob_type', ctypes.POINTER(_PyObject))
    288     ]
    289 
    290     # python with trace
    291     if hasattr(sys, 'getobjects'):
    292         class _PyObject(ctypes.Structure):
    293             pass
    294         _PyObject._fields_ = [
    295             ('_ob_next', ctypes.POINTER(_PyObject)),
    296             ('_ob_prev', ctypes.POINTER(_PyObject)),
    297             ('ob_refcnt', _Py_ssize_t),
    298             ('ob_type', ctypes.POINTER(_PyObject))
    299         ]
    300 
    301     class _Traceback(_PyObject):
    302         pass
    303     _Traceback._fields_ = [
    304         ('tb_next', ctypes.POINTER(_Traceback)),
    305         ('tb_frame', ctypes.POINTER(_PyObject)),
    306         ('tb_lasti', ctypes.c_int),
    307         ('tb_lineno', ctypes.c_int)
    308     ]
    309 
    310     def tb_set_next(tb, next):
    311         """Set the tb_next attribute of a traceback object."""
    312         if not (isinstance(tb, TracebackType) and
    313                 (next is None or isinstance(next, TracebackType))):
    314             raise TypeError('tb_set_next arguments must be traceback objects')
    315         obj = _Traceback.from_address(id(tb))
    316         if tb.tb_next is not None:
    317             old = _Traceback.from_address(id(tb.tb_next))
    318             old.ob_refcnt -= 1
    319         if next is None:
    320             obj.tb_next = ctypes.POINTER(_Traceback)()
    321         else:
    322             next = _Traceback.from_address(id(next))
    323             next.ob_refcnt += 1
    324             obj.tb_next = ctypes.pointer(next)
    325 
    326     return tb_set_next
    327 
    328 
    329 # try to get a tb_set_next implementation if we don't have transparent
    330 # proxies.
    331 tb_set_next = None
    332 if tproxy is None:
    333     try:
    334         tb_set_next = _init_ugly_crap()
    335     except:
    336         pass
    337     del _init_ugly_crap
    338