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) 2017 by the Jinja Team. 11 :license: BSD, see LICENSE for more details. 12""" 13import sys 14import traceback 15from types import TracebackType, CodeType 16from jinja2.utils import missing, internal_code 17from jinja2.exceptions import TemplateSyntaxError 18from jinja2._compat import iteritems, reraise, PY2 19 20# on pypy we can take advantage of transparent proxies 21try: 22 from __pypy__ import tproxy 23except ImportError: 24 tproxy = None 25 26 27# how does the raise helper look like? 28try: 29 exec("raise TypeError, 'foo'") 30except SyntaxError: 31 raise_helper = 'raise __jinja_exception__[1]' 32except TypeError: 33 raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]' 34 35 36class 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 66def 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 80class 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 132def 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 143def 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 154def 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 198def get_jinja_locals(real_locals): 199 ctx = real_locals.get('context') 200 if ctx: 201 locals = ctx.get_all().copy() 202 else: 203 locals = {} 204 205 local_overrides = {} 206 207 for name, value in iteritems(real_locals): 208 if not name.startswith('l_') or value is missing: 209 continue 210 try: 211 _, depth, name = name.split('_', 2) 212 depth = int(depth) 213 except ValueError: 214 continue 215 cur_depth = local_overrides.get(name, (-1,))[0] 216 if cur_depth < depth: 217 local_overrides[name] = (depth, value) 218 219 for name, (_, value) in iteritems(local_overrides): 220 if value is missing: 221 locals.pop(name, None) 222 else: 223 locals[name] = value 224 225 return locals 226 227 228def fake_exc_info(exc_info, filename, lineno): 229 """Helper for `translate_exception`.""" 230 exc_type, exc_value, tb = exc_info 231 232 # figure the real context out 233 if tb is not None: 234 locals = get_jinja_locals(tb.tb_frame.f_locals) 235 236 # if there is a local called __jinja_exception__, we get 237 # rid of it to not break the debug functionality. 238 locals.pop('__jinja_exception__', None) 239 else: 240 locals = {} 241 242 # assamble fake globals we need 243 globals = { 244 '__name__': filename, 245 '__file__': filename, 246 '__jinja_exception__': exc_info[:2], 247 248 # we don't want to keep the reference to the template around 249 # to not cause circular dependencies, but we mark it as Jinja 250 # frame for the ProcessedTraceback 251 '__jinja_template__': None 252 } 253 254 # and fake the exception 255 code = compile('\n' * (lineno - 1) + raise_helper, filename, 'exec') 256 257 # if it's possible, change the name of the code. This won't work 258 # on some python environments such as google appengine 259 try: 260 if tb is None: 261 location = 'template' 262 else: 263 function = tb.tb_frame.f_code.co_name 264 if function == 'root': 265 location = 'top-level template code' 266 elif function.startswith('block_'): 267 location = 'block "%s"' % function[6:] 268 else: 269 location = 'template' 270 271 if PY2: 272 code = CodeType(0, code.co_nlocals, code.co_stacksize, 273 code.co_flags, code.co_code, code.co_consts, 274 code.co_names, code.co_varnames, filename, 275 location, code.co_firstlineno, 276 code.co_lnotab, (), ()) 277 else: 278 code = CodeType(0, code.co_kwonlyargcount, 279 code.co_nlocals, code.co_stacksize, 280 code.co_flags, code.co_code, code.co_consts, 281 code.co_names, code.co_varnames, filename, 282 location, code.co_firstlineno, 283 code.co_lnotab, (), ()) 284 except Exception as e: 285 pass 286 287 # execute the code and catch the new traceback 288 try: 289 exec(code, globals, locals) 290 except: 291 exc_info = sys.exc_info() 292 new_tb = exc_info[2].tb_next 293 294 # return without this frame 295 return exc_info[:2] + (new_tb,) 296 297 298def _init_ugly_crap(): 299 """This function implements a few ugly things so that we can patch the 300 traceback objects. The function returned allows resetting `tb_next` on 301 any python traceback object. Do not attempt to use this on non cpython 302 interpreters 303 """ 304 import ctypes 305 from types import TracebackType 306 307 if PY2: 308 # figure out size of _Py_ssize_t for Python 2: 309 if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'): 310 _Py_ssize_t = ctypes.c_int64 311 else: 312 _Py_ssize_t = ctypes.c_int 313 else: 314 # platform ssize_t on Python 3 315 _Py_ssize_t = ctypes.c_ssize_t 316 317 # regular python 318 class _PyObject(ctypes.Structure): 319 pass 320 _PyObject._fields_ = [ 321 ('ob_refcnt', _Py_ssize_t), 322 ('ob_type', ctypes.POINTER(_PyObject)) 323 ] 324 325 # python with trace 326 if hasattr(sys, 'getobjects'): 327 class _PyObject(ctypes.Structure): 328 pass 329 _PyObject._fields_ = [ 330 ('_ob_next', ctypes.POINTER(_PyObject)), 331 ('_ob_prev', ctypes.POINTER(_PyObject)), 332 ('ob_refcnt', _Py_ssize_t), 333 ('ob_type', ctypes.POINTER(_PyObject)) 334 ] 335 336 class _Traceback(_PyObject): 337 pass 338 _Traceback._fields_ = [ 339 ('tb_next', ctypes.POINTER(_Traceback)), 340 ('tb_frame', ctypes.POINTER(_PyObject)), 341 ('tb_lasti', ctypes.c_int), 342 ('tb_lineno', ctypes.c_int) 343 ] 344 345 def tb_set_next(tb, next): 346 """Set the tb_next attribute of a traceback object.""" 347 if not (isinstance(tb, TracebackType) and 348 (next is None or isinstance(next, TracebackType))): 349 raise TypeError('tb_set_next arguments must be traceback objects') 350 obj = _Traceback.from_address(id(tb)) 351 if tb.tb_next is not None: 352 old = _Traceback.from_address(id(tb.tb_next)) 353 old.ob_refcnt -= 1 354 if next is None: 355 obj.tb_next = ctypes.POINTER(_Traceback)() 356 else: 357 next = _Traceback.from_address(id(next)) 358 next.ob_refcnt += 1 359 obj.tb_next = ctypes.pointer(next) 360 361 return tb_set_next 362 363 364# try to get a tb_set_next implementation if we don't have transparent 365# proxies. 366tb_set_next = None 367if tproxy is None: 368 try: 369 tb_set_next = _init_ugly_crap() 370 except: 371 pass 372 del _init_ugly_crap 373