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