Merge lp:~alecu/openobject-server/django-tracebacks into lp:openobject-server
- django-tracebacks
- Merge into trunk
Status: | Rejected |
---|---|
Rejected by: | Antony Lesuisse (OpenERP) |
Proposed branch: | lp:~alecu/openobject-server/django-tracebacks |
Merge into: | lp:openobject-server |
Diff against target: |
494 lines (+470/-2) 2 files modified
bin/htmltraceback.py (+468/-0) bin/netsvc.py (+2/-2) |
To merge this branch: | bzr merge lp:~alecu/openobject-server/django-tracebacks |
Related bugs: | |
Related blueprints: |
Django-like detailed tracebacks for OpenERP
(Undefined)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Antony Lesuisse (OpenERP) | Disapprove | ||
Fabien (Open ERP) | Pending | ||
Review via email: mp+14655@code.launchpad.net |
Commit message
Description of the change
Alejandro J. Cura (alecu) wrote : | # |
Christophe CHAUVET (christophe-chauvet) wrote : | # |
Hi
why i see this (os.environ[
Does we installed django dependancies ?
Regards,
Alejandro J. Cura (alecu) wrote : | # |
The prototype version of the code used to depend on Django's templating module, so that why that "os.environ" line was there. I've now removed it from the code, and cleaned up a few imports. Thanks Christophe!
- 1872. By Alejandro J. Cura <email address hidden>
-
clean up imports
Christophe CHAUVET (christophe-chauvet) wrote : | # |
I think there is a license issue, the new license to the trunk branch are AGPLv3
Which license use by Dajngo project ?
Alejandro J. Cura (alecu) wrote : | # |
Django's license [1] is the modified BSD license [2] (with no advertising clause)
From what I've researched, you can use BSD code in AGPL projects, but not the other way around: http://
So it's ok to use this module from OpenObject.
--
[1] http://
[2] http://
Antony Lesuisse (OpenERP) (al-openerp) wrote : | # |
I prefer that we stick to plain text default python traceback so this will never be merged.
Preview Diff
1 | === added file 'bin/htmltraceback.py' |
2 | --- bin/htmltraceback.py 1970-01-01 00:00:00 +0000 |
3 | +++ bin/htmltraceback.py 2009-11-10 14:04:09 +0000 |
4 | @@ -0,0 +1,468 @@ |
5 | +# |
6 | +# by Alejandro J. Cura - alecu@gcoop.coop |
7 | +# based on Django's debug.py, copyright notice follows |
8 | +# |
9 | +# Copyright (c) Django Software Foundation and individual contributors. |
10 | +# All rights reserved. |
11 | +# |
12 | +# Redistribution and use in source and binary forms, with or without modification, |
13 | +# are permitted provided that the following conditions are met: |
14 | +# |
15 | +# 1. Redistributions of source code must retain the above copyright notice, |
16 | +# this list of conditions and the following disclaimer. |
17 | +# |
18 | +# 2. Redistributions in binary form must reproduce the above copyright |
19 | +# notice, this list of conditions and the following disclaimer in the |
20 | +# documentation and/or other materials provided with the distribution. |
21 | +# |
22 | +# 3. Neither the name of Django nor the names of its contributors may be used |
23 | +# to endorse or promote products derived from this software without |
24 | +# specific prior written permission. |
25 | +# |
26 | +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
27 | +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
28 | +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
29 | +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR |
30 | +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
31 | +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
32 | +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
33 | +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
34 | +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
35 | +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
36 | + |
37 | +import re |
38 | +import cgi |
39 | +import traceback |
40 | +import string |
41 | +import pprint |
42 | +import datetime |
43 | + |
44 | +UNICODE_HINT_TEMPLATE = """ |
45 | +<div id="unicode-hint"> |
46 | + <h2>Unicode error hint</h2> |
47 | + <p>The string that could not be encoded/decoded was: <strong>${hint}</strong></p> |
48 | +</div> |
49 | +""" |
50 | + |
51 | +FRAME_LINE_TEMPLATE = """ |
52 | + <li onclick="toggle('pre${id}', 'post${id}')">${line}</li>""" |
53 | + |
54 | +FRAME_PRE_HTML = """ |
55 | + <ol start="${pre_context_lineno}" class="pre-context" id="pre${id}">${pre_lines_html} |
56 | + </ol> |
57 | +""" |
58 | + |
59 | +FRAME_POST_HTML = """ |
60 | + <ol start="${post_context_lineno}" class="post-context" id="post${id}">${post_lines_html} |
61 | + </ol> |
62 | +""" |
63 | + |
64 | +FRAME_CONTEXT_TEMPLATE = """ |
65 | + <div class="context" id="c${id}"> |
66 | + ${pre_context_html} |
67 | + <ol start="${lineno}" class="context-line"> |
68 | + <li onclick="toggle('pre${id}', 'post${id}')">${context_line} <span>...</span></li> |
69 | + </ol> |
70 | + ${post_context_html} |
71 | + </div> |
72 | +""" |
73 | + |
74 | +FRAME_VARS_ROW = """ |
75 | + <tr> |
76 | + <td>${var_name}</td> |
77 | + <td class="code"><div>${var_value}</div></td> |
78 | + </tr> |
79 | +""" |
80 | + |
81 | +FRAME_VARS_TEMPLATE = """ |
82 | + <div class="commands"> |
83 | + <a href="#" onclick="return varToggle(this, '${id}')"><span>▶</span> Local vars</a> |
84 | + </div> |
85 | + <table class="vars" id="v${id}"> |
86 | + <thead> |
87 | + <tr> |
88 | + <th>Variable</th> |
89 | + <th>Value</th> |
90 | + </tr> |
91 | + </thead> |
92 | + <tbody> |
93 | + ${frame_vars_rows} |
94 | + </tbody> |
95 | + </table> |
96 | +""" |
97 | + |
98 | +FRAME_TEMPLATE = """ |
99 | + <li class="frame"> |
100 | + <code>${filename}</code> in <code>${function}</code> |
101 | + ${frame_context} |
102 | + ${frame_vars} |
103 | + </li> |
104 | +""" |
105 | + |
106 | +HTML_TRACEBACK_TEMPLATE = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> |
107 | +<html lang="en"> |
108 | +<head> |
109 | + <meta http-equiv="content-type" content="text/html; charset=utf-8"/> |
110 | + <meta name="robots" content="NONE,NOARCHIVE"/> |
111 | + <title>${exception_type}</title> |
112 | + <style type="text/css"> |
113 | + html * { padding:0; margin:0; } |
114 | + body * { padding:10px 20px; } |
115 | + body * * { padding:0; } |
116 | + body { font:small sans-serif; } |
117 | + body>div { border-bottom:1px solid #ddd; } |
118 | + h1 { font-weight:normal; } |
119 | + h2 { margin-bottom:.8em; } |
120 | + h2 span { font-size:80%; color:#666; font-weight:normal; } |
121 | + h3 { margin:1em 0 .5em 0; } |
122 | + h4 { margin:0 0 .5em 0; font-weight: normal; } |
123 | + table { border:1px solid #ccc; border-collapse: collapse; width:100%; background:white; } |
124 | + tbody td, tbody th { vertical-align:top; padding:2px 3px; } |
125 | + thead th { padding:1px 6px 1px 3px; background:#fefefe; text-align:left; font-weight:normal; font-size:11px; border:1px solid #ddd; } |
126 | + tbody th { width:12em; text-align:right; color:#666; padding-right:.5em; } |
127 | + table.vars { margin:5px 0 2px 40px; } |
128 | + table.vars td, table.req td { font-family:monospace; } |
129 | + table td.code { width:100%; } |
130 | + table td.code div { overflow:hidden; } |
131 | + table.source th { color:#666; } |
132 | + table.source td { font-family:monospace; white-space:pre; border-bottom:1px solid #eee; } |
133 | + ul.traceback { list-style-type:none; } |
134 | + ul.traceback li.frame { margin-bottom:1em; } |
135 | + div.context { margin: 10px 0; } |
136 | + div.context ol { padding-left:30px; margin:0 10px; list-style-position: inside; } |
137 | + div.context ol li { font-family:monospace; white-space:pre; color:#666; cursor:pointer; } |
138 | + div.context ol.context-line li { color:black; background-color:#ccc; } |
139 | + div.context ol.context-line li span { float: right; } |
140 | + div.commands { margin-left: 40px; } |
141 | + div.commands a { color:black; text-decoration:none; } |
142 | + #summary { background: #ffc; } |
143 | + #summary h2 { font-weight: normal; color: #666; } |
144 | + #template, #template-not-exist { background:#f6f6f6; } |
145 | + #unicode-hint { background:#eee; } |
146 | + #traceback { background:#eee; } |
147 | + #summary table { border:none; background:transparent; } |
148 | + .error { background: #ffc; } |
149 | + .specific { color:#cc3300; font-weight:bold; } |
150 | + h2 span.commands { font-size:.7em;} |
151 | + span.commands a:link {color:#5E5694;} |
152 | + pre.exception_value { font-family: sans-serif; color: #666; font-size: 1.5em; margin: 10px 0 10px 0; } |
153 | + pre.textdetails {display:none} |
154 | + </style> |
155 | + <script type="text/javascript"> |
156 | + //<!-- |
157 | + function getElementsByClassName(oElm, strTagName, strClassName){ |
158 | + // Written by Jonathan Snook, http://www.snook.ca/jon; Add-ons by Robert Nyman, http://www.robertnyman.com |
159 | + var arrElements = (strTagName == "*" && document.all)? document.all : |
160 | + oElm.getElementsByTagName(strTagName); |
161 | + var arrReturnElements = new Array(); |
162 | + strClassName = strClassName.replace(/\-/g, "\\-"); |
163 | + var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)"); |
164 | + var oElement; |
165 | + for(var i=0; i<arrElements.length; i++){ |
166 | + oElement = arrElements[i]; |
167 | + if(oRegExp.test(oElement.className)){ |
168 | + arrReturnElements.push(oElement); |
169 | + } |
170 | + } |
171 | + return (arrReturnElements) |
172 | + } |
173 | + function hideAll(elems) { |
174 | + for (var e = 0; e < elems.length; e++) { |
175 | + elems[e].style.display = 'none'; |
176 | + } |
177 | + } |
178 | + window.onload = function() { |
179 | + hideAll(getElementsByClassName(document, 'table', 'vars')); |
180 | + hideAll(getElementsByClassName(document, 'ol', 'pre-context')); |
181 | + hideAll(getElementsByClassName(document, 'ol', 'post-context')); |
182 | + } |
183 | + function toggle() { |
184 | + for (var i = 0; i < arguments.length; i++) { |
185 | + var e = document.getElementById(arguments[i]); |
186 | + if (e) { |
187 | + e.style.display = e.style.display == 'none' ? 'block' : 'none'; |
188 | + } |
189 | + } |
190 | + return false; |
191 | + } |
192 | + function varToggle(link, id) { |
193 | + toggle('v' + id); |
194 | + var s = link.getElementsByTagName('span')[0]; |
195 | + var uarr = String.fromCharCode(0x25b6); |
196 | + var darr = String.fromCharCode(0x25bc); |
197 | + s.innerHTML = s.innerHTML == uarr ? darr : uarr; |
198 | + return false; |
199 | + } |
200 | + //--> |
201 | + </script> |
202 | +</head> |
203 | +<body> |
204 | +<pre class="textdetails"> |
205 | +${text_details} |
206 | +</pre> |
207 | +<div id="summary"> |
208 | + <h1>${exception_type}</h1> |
209 | + <pre class="exception_value">${exception_value}</pre> |
210 | +</div> |
211 | +${unicode_hint} |
212 | +<div id="traceback"> |
213 | + <h2>Traceback</h2> |
214 | + <div id="browserTraceback"> |
215 | + <ul class="traceback"> |
216 | + ${frames} |
217 | + </ul> |
218 | + </div> |
219 | +</div> |
220 | + |
221 | +</body> |
222 | +</html> |
223 | +""" |
224 | + |
225 | +class DjangoUnicodeDecodeError(UnicodeDecodeError): |
226 | + def __init__(self, obj, *args): |
227 | + self.obj = obj |
228 | + UnicodeDecodeError.__init__(self, *args) |
229 | + |
230 | + def __str__(self): |
231 | + original = UnicodeDecodeError.__str__(self) |
232 | + return '%s. You passed in %r (%s)' % (original, self.obj, |
233 | + type(self.obj)) |
234 | + |
235 | +def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): |
236 | + if strings_only and isinstance(s, (types.NoneType, int, long, datetime.datetime, datetime.date, datetime.time, float)): |
237 | + return s |
238 | + try: |
239 | + if not isinstance(s, basestring,): |
240 | + if hasattr(s, '__unicode__'): |
241 | + s = unicode(s) |
242 | + else: |
243 | + try: |
244 | + s = unicode(str(s), encoding, errors) |
245 | + except UnicodeEncodeError: |
246 | + if not isinstance(s, Exception): |
247 | + raise |
248 | + # If we get to here, the caller has passed in an Exception |
249 | + # subclass populated with non-ASCII data without special |
250 | + # handling to display as a string. We need to handle this |
251 | + # without raising a further exception. We do an |
252 | + # approximation to what the Exception's standard str() |
253 | + # output should be. |
254 | + s = ' '.join([force_unicode(arg, encoding, strings_only, |
255 | + errors) for arg in s]) |
256 | + elif not isinstance(s, unicode): |
257 | + # Note: We use .decode() here, instead of unicode(s, encoding, |
258 | + # errors), so that if s is a SafeString, it ends up being a |
259 | + # SafeUnicode at the end. |
260 | + s = s.decode(encoding, errors) |
261 | + except UnicodeDecodeError, e: |
262 | + raise DjangoUnicodeDecodeError(s, *e.args) |
263 | + return s |
264 | + |
265 | +def render_template(t, params): |
266 | + st = string.Template(t) |
267 | + return st.substitute(params) |
268 | + |
269 | +class ExceptionReporter: |
270 | + def __init__(self, exc_type, exc_value, tb): |
271 | + self.exc_type = exc_type |
272 | + self.exc_value = exc_value |
273 | + self.tb = tb |
274 | + |
275 | + # Handle deprecated string exceptions |
276 | + if isinstance(self.exc_type, basestring): |
277 | + self.exc_value = Exception('Deprecated String Exception: %r' % self.exc_type) |
278 | + self.exc_type = type(self.exc_value) |
279 | + |
280 | + def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, module_name=None): |
281 | + """ |
282 | + Returns context_lines before and after lineno from file. |
283 | + Returns (pre_context_lineno, pre_context, context_line, post_context). |
284 | + """ |
285 | + source = None |
286 | + if loader is not None and hasattr(loader, "get_source"): |
287 | + source = loader.get_source(module_name) |
288 | + if source is not None: |
289 | + source = source.splitlines() |
290 | + if source is None: |
291 | + try: |
292 | + f = open(filename) |
293 | + try: |
294 | + source = f.readlines() |
295 | + finally: |
296 | + f.close() |
297 | + except (OSError, IOError): |
298 | + pass |
299 | + if source is None: |
300 | + return None, [], None, [] |
301 | + |
302 | + encoding = 'ascii' |
303 | + for line in source[:2]: |
304 | + # File coding may be specified. Match pattern from PEP-263 |
305 | + # (http://www.python.org/dev/peps/pep-0263/) |
306 | + match = re.search(r'coding[:=]\s*([-\w.]+)', line) |
307 | + if match: |
308 | + encoding = match.group(1) |
309 | + break |
310 | + source = [unicode(sline, encoding, 'replace') for sline in source] |
311 | + |
312 | + lower_bound = max(0, lineno - context_lines) |
313 | + upper_bound = lineno + context_lines |
314 | + |
315 | + pre_context = [line.strip('\n') for line in source[lower_bound:lineno]] |
316 | + context_line = source[lineno].strip('\n') |
317 | + post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]] |
318 | + |
319 | + return lower_bound, pre_context, context_line, post_context |
320 | + |
321 | + def get_traceback_frames(self): |
322 | + frames = [] |
323 | + tb = self.tb |
324 | + while tb is not None: |
325 | + # support for __traceback_hide__ which is used by a few libraries |
326 | + # to hide internal frames. |
327 | + if tb.tb_frame.f_locals.get('__traceback_hide__'): |
328 | + tb = tb.tb_next |
329 | + continue |
330 | + filename = tb.tb_frame.f_code.co_filename |
331 | + function = tb.tb_frame.f_code.co_name |
332 | + lineno = tb.tb_lineno - 1 |
333 | + loader = tb.tb_frame.f_globals.get('__loader__') |
334 | + module_name = tb.tb_frame.f_globals.get('__name__') |
335 | + pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(filename, lineno, 7, loader, module_name) |
336 | + if pre_context_lineno is not None: |
337 | + frames.append({ |
338 | + 'tb': tb, |
339 | + 'filename': cgi.escape(filename), |
340 | + 'function': cgi.escape(function), |
341 | + 'lineno': lineno + 1, |
342 | + 'vars': tb.tb_frame.f_locals.items(), |
343 | + 'id': id(tb), |
344 | + 'pre_context': pre_context, |
345 | + 'context_line': context_line, |
346 | + 'post_context': post_context, |
347 | + 'pre_context_lineno': pre_context_lineno + 1, |
348 | + }) |
349 | + tb = tb.tb_next |
350 | + |
351 | + if not frames: |
352 | + frames = [{ |
353 | + 'filename': '<unknown>', |
354 | + 'function': '?', |
355 | + 'lineno': '?', |
356 | + 'context_line': '???', |
357 | + }] |
358 | + |
359 | + return frames |
360 | + |
361 | + def get_traceback_html(self): |
362 | + frames = self.get_traceback_frames() |
363 | + unicode_hint = '' |
364 | + if issubclass(self.exc_type, UnicodeError): |
365 | + start = getattr(self.exc_value, 'start', None) |
366 | + end = getattr(self.exc_value, 'end', None) |
367 | + if start is not None and end is not None: |
368 | + unicode_str = self.exc_value.args[1] |
369 | + unicode_hint = force_unicode(unicode_str[max(start-5, 0):min(end+5, len(unicode_str))], 'ascii', errors='replace') |
370 | + |
371 | + unicode_hint_html = render_template(UNICODE_HINT_TEMPLATE, {'hint':cgi.escape(unicode_hint)}) if unicode_hint else "" |
372 | + |
373 | + frames_html = [] |
374 | + for frame in frames: |
375 | + frame_vars_rows = [] |
376 | + |
377 | + for var_name, var_value in sorted(frame['vars']): |
378 | + c = { |
379 | + 'var_name': cgi.escape(var_name), |
380 | + 'var_value': cgi.escape(pprint.pformat(var_value)), |
381 | + } |
382 | + frame_vars_rows.append( render_template(FRAME_VARS_ROW, c) ) |
383 | + |
384 | + c = { |
385 | + 'id': frame['id'], |
386 | + 'frame_vars_rows': "".join(frame_vars_rows), |
387 | + } |
388 | + frame_vars_html = render_template(FRAME_VARS_TEMPLATE, c) |
389 | + |
390 | + pre_lines = [] |
391 | + for line in frame['pre_context']: |
392 | + c = { |
393 | + 'id': frame['id'], |
394 | + 'line': cgi.escape(line), |
395 | + } |
396 | + pre_lines.append(render_template(FRAME_LINE_TEMPLATE, c)) |
397 | + pre_lines_html = "".join(pre_lines) |
398 | + |
399 | + c = { |
400 | + 'id': frame['id'], |
401 | + 'pre_context_lineno': frame['pre_context_lineno'], |
402 | + 'pre_lines_html': pre_lines_html, |
403 | + } |
404 | + pre_context_html = render_template(FRAME_PRE_HTML, c) |
405 | + |
406 | + post_lines = [] |
407 | + for line in frame['post_context']: |
408 | + c = { |
409 | + 'id': frame['id'], |
410 | + 'line': cgi.escape(line), |
411 | + } |
412 | + post_lines.append(render_template(FRAME_LINE_TEMPLATE, c)) |
413 | + post_lines_html = "".join(pre_lines) |
414 | + |
415 | + c = { |
416 | + 'id': frame['id'], |
417 | + 'post_context_lineno': int(frame['lineno'])+1, |
418 | + 'post_lines_html': post_lines_html, |
419 | + } |
420 | + post_context_html = render_template(FRAME_POST_HTML, c) |
421 | + |
422 | + c = { |
423 | + 'id': frame['id'], |
424 | + 'pre_context_html': pre_context_html if frame['pre_context'] else "", |
425 | + 'lineno': frame['lineno'], |
426 | + 'context_line': cgi.escape(frame['context_line']), |
427 | + 'post_context_html': post_context_html if frame['post_context'] else "", |
428 | + } |
429 | + frame_context_html = render_template(FRAME_CONTEXT_TEMPLATE, c) |
430 | + |
431 | + c = { |
432 | + 'filename': frame['filename'], |
433 | + 'function': frame['function'], |
434 | + 'frame_context': frame_context_html if frame['context_line'] else "", |
435 | + 'frame_vars': frame_vars_html if frame['vars'] else "", |
436 | + } |
437 | + frames_html.append( render_template(FRAME_TEMPLATE, c) ) |
438 | + |
439 | + frames_html = "".join(frames_html) |
440 | + |
441 | + c = { |
442 | + 'exception_type': self.exc_type.__name__, |
443 | + 'exception_value': cgi.escape(force_unicode(self.exc_value, errors='replace')), |
444 | + 'unicode_hint': unicode_hint_html, |
445 | + 'frames': frames_html, |
446 | + 'lastframe': frames[-1], |
447 | + 'text_details': force_unicode("".join(traceback.format_exception(self.exc_type, self.exc_value, self.tb))), |
448 | + } |
449 | + return render_template(HTML_TRACEBACK_TEMPLATE,c) |
450 | + |
451 | + |
452 | +def format_exception(*a): |
453 | + return ExceptionReporter(*a).get_traceback_html() |
454 | + |
455 | + |
456 | +def test(): |
457 | + def inner_function(s): |
458 | + a = u"\u26a7".encode("ascii") |
459 | + assert False |
460 | + |
461 | + def outer_function(d): |
462 | + inner_function(d) |
463 | + |
464 | + try: |
465 | + outer_function("some string") |
466 | + except: |
467 | + import sys |
468 | + t, v, tb = sys.exc_info() |
469 | + print format_exception(t, v, tb) |
470 | + |
471 | +if __name__ == "__main__": |
472 | + test() |
473 | |
474 | === modified file 'bin/netsvc.py' |
475 | --- bin/netsvc.py 2009-11-06 10:06:23 +0000 |
476 | +++ bin/netsvc.py 2009-11-10 14:04:09 +0000 |
477 | @@ -216,7 +216,7 @@ |
478 | def quit(cls): |
479 | cls.cancel(None) |
480 | |
481 | -import traceback |
482 | +import htmltraceback |
483 | |
484 | class xmlrpc(object): |
485 | class RpcGateway(object): |
486 | @@ -249,7 +249,7 @@ |
487 | tb = e.traceback |
488 | else: |
489 | tb = sys.exc_info() |
490 | - tb_s = "".join(traceback.format_exception(*tb)) |
491 | + tb_s = "".join(htmltraceback.format_exception(*tb)) |
492 | if tools.config['debug_mode']: |
493 | import pdb |
494 | pdb.post_mortem(tb[2]) |
The code is inspired in Django's traceback.