blob: 4f81271be3ca7864a51bb0ace8f1ee2899b243f4 [file] [log] [blame]
Olivier Deprezf4ef2d02021-04-20 13:36:24 +02001"""More comprehensive traceback formatting for Python scripts.
2
3To enable this module, do:
4
5 import cgitb; cgitb.enable()
6
7at the top of your script. The optional arguments to enable() are:
8
9 display - if true, tracebacks are displayed in the web browser
10 logdir - if set, tracebacks are written to files in this directory
11 context - number of lines of source code to show for each stack frame
12 format - 'text' or 'html' controls the output format
13
14By default, tracebacks are displayed but not saved, the context is 5 lines
15and the output format is 'html' (for backwards compatibility with the
16original use of this module)
17
18Alternatively, if you have caught an exception and want cgitb to display it
19for you, call cgitb.handler(). The optional argument to handler() is a
203-item tuple (etype, evalue, etb) just like the value of sys.exc_info().
21The default handler displays output as HTML.
22
23"""
24import inspect
25import keyword
26import linecache
27import os
28import pydoc
29import sys
30import tempfile
31import time
32import tokenize
33import traceback
34
35def reset():
36 """Return a string that resets the CGI and browser to a known state."""
37 return '''<!--: spam
38Content-Type: text/html
39
40<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> -->
41<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> --> -->
42</font> </font> </font> </script> </object> </blockquote> </pre>
43</table> </table> </table> </table> </table> </font> </font> </font>'''
44
45__UNDEF__ = [] # a special sentinel object
46def small(text):
47 if text:
48 return '<small>' + text + '</small>'
49 else:
50 return ''
51
52def strong(text):
53 if text:
54 return '<strong>' + text + '</strong>'
55 else:
56 return ''
57
58def grey(text):
59 if text:
60 return '<font color="#909090">' + text + '</font>'
61 else:
62 return ''
63
64def lookup(name, frame, locals):
65 """Find the value for a given name in the given environment."""
66 if name in locals:
67 return 'local', locals[name]
68 if name in frame.f_globals:
69 return 'global', frame.f_globals[name]
70 if '__builtins__' in frame.f_globals:
71 builtins = frame.f_globals['__builtins__']
72 if type(builtins) is type({}):
73 if name in builtins:
74 return 'builtin', builtins[name]
75 else:
76 if hasattr(builtins, name):
77 return 'builtin', getattr(builtins, name)
78 return None, __UNDEF__
79
80def scanvars(reader, frame, locals):
81 """Scan one logical line of Python and look up values of variables used."""
82 vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
83 for ttype, token, start, end, line in tokenize.generate_tokens(reader):
84 if ttype == tokenize.NEWLINE: break
85 if ttype == tokenize.NAME and token not in keyword.kwlist:
86 if lasttoken == '.':
87 if parent is not __UNDEF__:
88 value = getattr(parent, token, __UNDEF__)
89 vars.append((prefix + token, prefix, value))
90 else:
91 where, value = lookup(token, frame, locals)
92 vars.append((token, where, value))
93 elif token == '.':
94 prefix += lasttoken + '.'
95 parent = value
96 else:
97 parent, prefix = None, ''
98 lasttoken = token
99 return vars
100
101def html(einfo, context=5):
102 """Return a nice HTML document describing a given traceback."""
103 etype, evalue, etb = einfo
104 if isinstance(etype, type):
105 etype = etype.__name__
106 pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
107 date = time.ctime(time.time())
108 head = '<body bgcolor="#f0f0f8">' + pydoc.html.heading(
109 '<big><big>%s</big></big>' %
110 strong(pydoc.html.escape(str(etype))),
111 '#ffffff', '#6622aa', pyver + '<br>' + date) + '''
112<p>A problem occurred in a Python script. Here is the sequence of
113function calls leading up to the error, in the order they occurred.</p>'''
114
115 indent = '<tt>' + small('&nbsp;' * 5) + '&nbsp;</tt>'
116 frames = []
117 records = inspect.getinnerframes(etb, context)
118 for frame, file, lnum, func, lines, index in records:
119 if file:
120 file = os.path.abspath(file)
121 link = '<a href="file://%s">%s</a>' % (file, pydoc.html.escape(file))
122 else:
123 file = link = '?'
124 args, varargs, varkw, locals = inspect.getargvalues(frame)
125 call = ''
126 if func != '?':
127 call = 'in ' + strong(pydoc.html.escape(func))
128 if func != "<module>":
129 call += inspect.formatargvalues(args, varargs, varkw, locals,
130 formatvalue=lambda value: '=' + pydoc.html.repr(value))
131
132 highlight = {}
133 def reader(lnum=[lnum]):
134 highlight[lnum[0]] = 1
135 try: return linecache.getline(file, lnum[0])
136 finally: lnum[0] += 1
137 vars = scanvars(reader, frame, locals)
138
139 rows = ['<tr><td bgcolor="#d8bbff">%s%s %s</td></tr>' %
140 ('<big>&nbsp;</big>', link, call)]
141 if index is not None:
142 i = lnum - index
143 for line in lines:
144 num = small('&nbsp;' * (5-len(str(i))) + str(i)) + '&nbsp;'
145 if i in highlight:
146 line = '<tt>=&gt;%s%s</tt>' % (num, pydoc.html.preformat(line))
147 rows.append('<tr><td bgcolor="#ffccee">%s</td></tr>' % line)
148 else:
149 line = '<tt>&nbsp;&nbsp;%s%s</tt>' % (num, pydoc.html.preformat(line))
150 rows.append('<tr><td>%s</td></tr>' % grey(line))
151 i += 1
152
153 done, dump = {}, []
154 for name, where, value in vars:
155 if name in done: continue
156 done[name] = 1
157 if value is not __UNDEF__:
158 if where in ('global', 'builtin'):
159 name = ('<em>%s</em> ' % where) + strong(name)
160 elif where == 'local':
161 name = strong(name)
162 else:
163 name = where + strong(name.split('.')[-1])
164 dump.append('%s&nbsp;= %s' % (name, pydoc.html.repr(value)))
165 else:
166 dump.append(name + ' <em>undefined</em>')
167
168 rows.append('<tr><td>%s</td></tr>' % small(grey(', '.join(dump))))
169 frames.append('''
170<table width="100%%" cellspacing=0 cellpadding=0 border=0>
171%s</table>''' % '\n'.join(rows))
172
173 exception = ['<p>%s: %s' % (strong(pydoc.html.escape(str(etype))),
174 pydoc.html.escape(str(evalue)))]
175 for name in dir(evalue):
176 if name[:1] == '_': continue
177 value = pydoc.html.repr(getattr(evalue, name))
178 exception.append('\n<br>%s%s&nbsp;=\n%s' % (indent, name, value))
179
180 return head + ''.join(frames) + ''.join(exception) + '''
181
182
183<!-- The above is a description of an error in a Python program, formatted
184 for a Web browser because the 'cgitb' module was enabled. In case you
185 are not reading this in a Web browser, here is the original traceback:
186
187%s
188-->
189''' % pydoc.html.escape(
190 ''.join(traceback.format_exception(etype, evalue, etb)))
191
192def text(einfo, context=5):
193 """Return a plain text document describing a given traceback."""
194 etype, evalue, etb = einfo
195 if isinstance(etype, type):
196 etype = etype.__name__
197 pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
198 date = time.ctime(time.time())
199 head = "%s\n%s\n%s\n" % (str(etype), pyver, date) + '''
200A problem occurred in a Python script. Here is the sequence of
201function calls leading up to the error, in the order they occurred.
202'''
203
204 frames = []
205 records = inspect.getinnerframes(etb, context)
206 for frame, file, lnum, func, lines, index in records:
207 file = file and os.path.abspath(file) or '?'
208 args, varargs, varkw, locals = inspect.getargvalues(frame)
209 call = ''
210 if func != '?':
211 call = 'in ' + func
212 if func != "<module>":
213 call += inspect.formatargvalues(args, varargs, varkw, locals,
214 formatvalue=lambda value: '=' + pydoc.text.repr(value))
215
216 highlight = {}
217 def reader(lnum=[lnum]):
218 highlight[lnum[0]] = 1
219 try: return linecache.getline(file, lnum[0])
220 finally: lnum[0] += 1
221 vars = scanvars(reader, frame, locals)
222
223 rows = [' %s %s' % (file, call)]
224 if index is not None:
225 i = lnum - index
226 for line in lines:
227 num = '%5d ' % i
228 rows.append(num+line.rstrip())
229 i += 1
230
231 done, dump = {}, []
232 for name, where, value in vars:
233 if name in done: continue
234 done[name] = 1
235 if value is not __UNDEF__:
236 if where == 'global': name = 'global ' + name
237 elif where != 'local': name = where + name.split('.')[-1]
238 dump.append('%s = %s' % (name, pydoc.text.repr(value)))
239 else:
240 dump.append(name + ' undefined')
241
242 rows.append('\n'.join(dump))
243 frames.append('\n%s\n' % '\n'.join(rows))
244
245 exception = ['%s: %s' % (str(etype), str(evalue))]
246 for name in dir(evalue):
247 value = pydoc.text.repr(getattr(evalue, name))
248 exception.append('\n%s%s = %s' % (" "*4, name, value))
249
250 return head + ''.join(frames) + ''.join(exception) + '''
251
252The above is a description of an error in a Python program. Here is
253the original traceback:
254
255%s
256''' % ''.join(traceback.format_exception(etype, evalue, etb))
257
258class Hook:
259 """A hook to replace sys.excepthook that shows tracebacks in HTML."""
260
261 def __init__(self, display=1, logdir=None, context=5, file=None,
262 format="html"):
263 self.display = display # send tracebacks to browser if true
264 self.logdir = logdir # log tracebacks to files if not None
265 self.context = context # number of source code lines per frame
266 self.file = file or sys.stdout # place to send the output
267 self.format = format
268
269 def __call__(self, etype, evalue, etb):
270 self.handle((etype, evalue, etb))
271
272 def handle(self, info=None):
273 info = info or sys.exc_info()
274 if self.format == "html":
275 self.file.write(reset())
276
277 formatter = (self.format=="html") and html or text
278 plain = False
279 try:
280 doc = formatter(info, self.context)
281 except: # just in case something goes wrong
282 doc = ''.join(traceback.format_exception(*info))
283 plain = True
284
285 if self.display:
286 if plain:
287 doc = pydoc.html.escape(doc)
288 self.file.write('<pre>' + doc + '</pre>\n')
289 else:
290 self.file.write(doc + '\n')
291 else:
292 self.file.write('<p>A problem occurred in a Python script.\n')
293
294 if self.logdir is not None:
295 suffix = ['.txt', '.html'][self.format=="html"]
296 (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir)
297
298 try:
299 with os.fdopen(fd, 'w') as file:
300 file.write(doc)
301 msg = '%s contains the description of this error.' % path
302 except:
303 msg = 'Tried to save traceback to %s, but failed.' % path
304
305 if self.format == 'html':
306 self.file.write('<p>%s</p>\n' % msg)
307 else:
308 self.file.write(msg + '\n')
309 try:
310 self.file.flush()
311 except: pass
312
313handler = Hook().handle
314def enable(display=1, logdir=None, context=5, format="html"):
315 """Install an exception handler that formats tracebacks as HTML.
316
317 The optional argument 'display' can be set to 0 to suppress sending the
318 traceback to the browser, and 'logdir' can be set to a directory to cause
319 tracebacks to be written to files there."""
320 sys.excepthook = Hook(display=display, logdir=logdir,
321 context=context, format=format)