Catch down errors caused by tb_set_next. This fixes #22
[jinja2.git] / jinja2 / debug.py
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