-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
debugger.py
213 lines (182 loc) · 7.98 KB
/
debugger.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# -*- coding: utf-8 -*-
"""
jinja.debugger
~~~~~~~~~~~~~~
This module implements helper function Jinja uses to give the users a
possibility to develop Jinja templates like they would debug python code.
It seamlessly integreates into the python traceback system, in fact it
just modifies the trackback stack so that the line numbers are correct
and the frame information are bound to the context and not the frame of
the template evaluation loop.
To achive this it raises the exception it cought before in an isolated
namespace at a given line. The locals namespace is set to the current
template context.
The traceback generated by raising that exception is then either returned
or linked with the former traceback if the `jinja._debugger` module is
available. Because it's not possible to modify traceback objects from the
python space this module is needed for this process.
If it's not available it just ignores the other frames. Because this can
lead to actually harder to debug code there is a setting on the jinja
environment to disable the debugging system.
The isolated namespace which is used to raise the exception also contains
a `__loader__` name that helds a reference to a PEP 302 compatible loader.
Because there are currently some traceback systems (such as the paste
evalexception debugger) that do not provide the frame globals when
retrieving the source from the linecache module, Jinja injects the source
to the linecache module itself and changes the filename to a URL style
"virtual filename" so that Jinja doesn't acidentally override other files
in the linecache.
:copyright: 2007 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
import sys
from random import randrange
# if we have extended debugger support we should really use it
try:
from jinja._debugger import *
has_extended_debugger = True
except ImportError:
has_extended_debugger = False
# we need the RUNTIME_EXCEPTION_OFFSET to skip the not used frames
from jinja.utils import reversed, RUNTIME_EXCEPTION_OFFSET
def fake_template_exception(exc_type, exc_value, tb, filename, lineno,
source, context_or_env, tb_back=None):
"""
Raise an exception "in a template". Return a traceback
object. This is used for runtime debugging, not compile time.
"""
# some traceback systems allow to skip frames
__traceback_hide__ = True
# create the namespace which will be the local namespace
# of the new frame then. Some debuggers show local variables
# so we better inject the context and not the evaluation loop context.
from jinja.datastructure import Context
if isinstance(context_or_env, Context):
env = context_or_env.environment
namespace = context_or_env.to_dict()
else:
env = context_or_env
namespace = {}
# no unicode for filenames
if isinstance(filename, unicode):
filename = filename.encode('utf-8')
# generate an jinja unique filename used so that linecache
# gets data that doesn't interfere with other modules
if filename is None:
vfilename = 'jinja://~%d' % randrange(0, 10000)
filename = '<string>'
else:
vfilename = 'jinja://%s' % filename
# now create the used loaded and update the linecache
loader = TracebackLoader(env, source, filename)
loader.update_linecache(vfilename)
globals = {
'__name__': vfilename,
'__file__': vfilename,
'__loader__': loader
}
# use the simple debugger to reraise the exception in the
# line where the error originally occoured
globals['__exception_to_raise__'] = (exc_type, exc_value)
offset = '\n' * (lineno - 1)
code = compile(offset + 'raise __exception_to_raise__[0], '
'__exception_to_raise__[1]',
vfilename or '<template>', 'exec')
try:
exec code in globals, namespace
except:
exc_info = sys.exc_info()
# if we have an extended debugger we set the tb_next flag so that
# we don't loose the higher stack items.
if has_extended_debugger:
if tb_back is not None:
tb_set_next(tb_back, exc_info[2])
if tb is not None:
tb_set_next(exc_info[2].tb_next, tb.tb_next)
# otherwise just return the exc_info from the simple debugger
return exc_info
def translate_exception(template, context, exc_type, exc_value, tb):
"""
Translate an exception and return the new traceback.
"""
# depending on the python version we have to skip some frames to
# step to get the frame of the current template. The frames before
# are the toolchain used to render that thing.
for x in xrange(RUNTIME_EXCEPTION_OFFSET):
tb = tb.tb_next
result_tb = prev_tb = None
initial_tb = tb
# translate all the jinja frames in this traceback
while tb is not None:
if tb.tb_frame.f_globals.get('__jinja_template__'):
debug_info = tb.tb_frame.f_globals['debug_info']
# the next thing we do is matching the current error line against the
# debugging table to get the correct source line. If we can't find the
# filename and line number we return the traceback object unaltered.
error_line = tb.tb_lineno
for code_line, tmpl_filename, tmpl_line in reversed(debug_info):
if code_line <= error_line:
source = tb.tb_frame.f_globals['template_source']
tb = fake_template_exception(exc_type, exc_value, tb,
tmpl_filename, tmpl_line,
source, context, prev_tb)[-1]
break
if result_tb is None:
result_tb = tb
prev_tb = tb
tb = tb.tb_next
# under some conditions we cannot translate any frame. in that
# situation just return the original traceback.
return (exc_type, exc_value, result_tb or intial_tb)
def raise_syntax_error(exception, env, source=None):
"""
This method raises an exception that includes more debugging
informations so that debugging works better. Unlike
`translate_exception` this method raises the exception with
the traceback.
"""
exc_info = fake_template_exception(exception, None, None,
exception.filename,
exception.lineno, source, env)
raise exc_info[0], exc_info[1], exc_info[2]
class TracebackLoader(object):
"""
Fake importer that just returns the source of a template. It's just used
by Jinja interally and you shouldn't use it on your own.
"""
def __init__(self, environment, source, filename):
self.loader = environment.loader
self.source = source
self.filename = filename
def update_linecache(self, virtual_filename):
"""
Hacky way to let traceback systems know about the
Jinja template sourcecode. Very hackish indeed.
"""
# check for linecache, not every implementation of python
# might have such an module (this check is pretty senseless
# because we depend on cpython anway)
try:
from linecache import cache
except ImportError:
return
data = self.get_source(None)
cache[virtual_filename] = (
len(data),
None,
data.splitlines(True),
virtual_filename
)
def get_source(self, impname):
"""Return the source as bytestring."""
source = ''
if self.source is not None:
source = self.source
elif self.loader is not None:
try:
source = self.loader.get_source(self.filename)
except TemplateNotFound:
pass
if isinstance(source, unicode):
source = source.encode('utf-8')
return source