babel extraction can now properly extract newstyle gettext calls.
[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 jinja2.utils import CodeType, missing, internal_code
16 from jinja2.exceptions import TemplateSyntaxError
17
18
19 # how does the raise helper look like?
20 try:
21     exec "raise TypeError, 'foo'"
22 except SyntaxError:
23     raise_helper = 'raise __jinja_exception__[1]'
24 except TypeError:
25     raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]'
26
27
28 class 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
53 class 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
101 def 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
112 def 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
123 def 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
170 def 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
239 def _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
301 try:
302     from jinja2._speedups import tb_set_next
303 except ImportError:
304     try:
305         tb_set_next = _init_ugly_crap()
306     except:
307         tb_set_next = None
308 del _init_ugly_crap