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"""
13import sys
14import traceback
15from types import TracebackType
16from jinja2.utils import missing, internal_code
17from jinja2.exceptions import TemplateSyntaxError
18from jinja2._compat import iteritems, reraise, code_type
19
20# on pypy we can take advantage of transparent proxies
21try:
22    from __pypy__ import tproxy
23except ImportError:
24    tproxy = None
25
26
27# how does the raise helper look like?
28try:
29    exec("raise TypeError, 'foo'")
30except SyntaxError:
31    raise_helper = 'raise __jinja_exception__[1]'
32except TypeError:
33    raise_helper = 'raise __jinja_exception__[0], __jinja_exception__[1]'
34
35
36class 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
66def 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
80class 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
132def 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
143def 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
154def 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
198def 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
267def _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.
331tb_set_next = None
332if tproxy is None:
333    try:
334        tb_set_next = _init_ugly_crap()
335    except:
336        pass
337    del _init_ugly_crap
338