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