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