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