You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					373 lines
				
				12 KiB
			
		
		
			
		
	
	
					373 lines
				
				12 KiB
			| 
											8 years ago
										 | # -*- 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) 2017 by the Jinja Team.
 | ||
|  |     :license: BSD, see LICENSE for more details.
 | ||
|  | """
 | ||
|  | import sys
 | ||
|  | import traceback
 | ||
|  | from types import TracebackType, CodeType
 | ||
|  | from jinja2.utils import missing, internal_code
 | ||
|  | from jinja2.exceptions import TemplateSyntaxError
 | ||
|  | from jinja2._compat import iteritems, reraise, PY2
 | ||
|  | 
 | ||
|  | # 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 get_jinja_locals(real_locals):
 | ||
|  |     ctx = real_locals.get('context')
 | ||
|  |     if ctx:
 | ||
|  |         locals = ctx.get_all()
 | ||
|  |     else:
 | ||
|  |         locals = {}
 | ||
|  | 
 | ||
|  |     local_overrides = {}
 | ||
|  | 
 | ||
|  |     for name, value in iteritems(real_locals):
 | ||
|  |         if not name.startswith('l_') or value is missing:
 | ||
|  |             continue
 | ||
|  |         try:
 | ||
|  |             _, depth, name = name.split('_', 2)
 | ||
|  |             depth = int(depth)
 | ||
|  |         except ValueError:
 | ||
|  |             continue
 | ||
|  |         cur_depth = local_overrides.get(name, (-1,))[0]
 | ||
|  |         if cur_depth < depth:
 | ||
|  |             local_overrides[name] = (depth, value)
 | ||
|  | 
 | ||
|  |     for name, (_, value) in iteritems(local_overrides):
 | ||
|  |         if value is missing:
 | ||
|  |             locals.pop(name, None)
 | ||
|  |         else:
 | ||
|  |             locals[name] = value
 | ||
|  | 
 | ||
|  |     return locals
 | ||
|  | 
 | ||
|  | 
 | ||
|  | 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:
 | ||
|  |         locals = get_jinja_locals(tb.tb_frame.f_locals)
 | ||
|  | 
 | ||
|  |         # 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'
 | ||
|  | 
 | ||
|  |         if PY2:
 | ||
|  |             code = CodeType(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, (), ())
 | ||
|  |         else:
 | ||
|  |             code = CodeType(0, code.co_kwonlyargcount,
 | ||
|  |                             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 Exception as e:
 | ||
|  |         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
 | ||
|  | 
 | ||
|  |     if PY2:
 | ||
|  |         # figure out size of _Py_ssize_t for Python 2:
 | ||
|  |         if hasattr(ctypes.pythonapi, 'Py_InitModule4_64'):
 | ||
|  |             _Py_ssize_t = ctypes.c_int64
 | ||
|  |         else:
 | ||
|  |             _Py_ssize_t = ctypes.c_int
 | ||
|  |     else:
 | ||
|  |         # platform ssize_t on Python 3
 | ||
|  |         _Py_ssize_t = ctypes.c_ssize_t
 | ||
|  | 
 | ||
|  |     # 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
 |