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