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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
|
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""An exception formatter that shows traceback supplements and traceback info,
optionally in HTML.
"""
import sys
try:
from html import escape
except ImportError: # pragma: PY2
from cgi import escape
import linecache
import traceback
DEBUG_EXCEPTION_FORMATTER = 1
class TextExceptionFormatter(object):
line_sep = '\n'
def __init__(self, limit=None, with_filenames=False):
self.limit = limit
self.with_filenames = with_filenames
def escape(self, s):
return s
def getPrefix(self):
return 'Traceback (most recent call last):'
def getLimit(self):
limit = self.limit
if limit is None:
limit = getattr(sys, 'tracebacklimit', 200)
return limit
def formatSupplementLine(self, line):
result = ' - %s' % line
if not isinstance(result, str): # pragma: PY2
# Must be an Python 2, and must be a unicode `line`
# and we upconverted the result to a unicode
result = result.encode('utf-8')
return result
def formatSourceURL(self, url):
return [self.formatSupplementLine(url)]
def formatSupplement(self, supplement, tb):
result = []
fmtLine = self.formatSupplementLine
url = getattr(supplement, 'source_url', None)
if url is not None:
result.extend(self.formatSourceURL(url))
line = getattr(supplement, 'line', 0)
if line == -1:
line = tb.tb_lineno
col = getattr(supplement, 'column', -1)
if line:
if col is not None and col >= 0:
result.append(fmtLine('Line %s, Column %s' % (
line, col)))
else:
result.append(fmtLine('Line %s' % line))
elif col is not None and col >= 0:
result.append(fmtLine('Column %s' % col))
expr = getattr(supplement, 'expression', None)
if expr:
result.append(fmtLine('Expression: %s' % expr))
warnings = getattr(supplement, 'warnings', None)
if warnings:
for warning in warnings:
result.append(fmtLine('Warning: %s' % warning))
getInfo = getattr(supplement, 'getInfo', None)
if getInfo is not None:
try:
extra = getInfo()
if extra:
result.append(self.formatSupplementInfo(extra))
except Exception: # pragma: no cover
if DEBUG_EXCEPTION_FORMATTER:
traceback.print_exc()
# else just swallow the exception.
return result
def formatSupplementInfo(self, info):
return self.escape(info)
def formatTracebackInfo(self, tbi):
return self.formatSupplementLine('__traceback_info__: %s' % (tbi, ))
def formatLine(self, tb=None, f=None):
if tb and not f:
f = tb.tb_frame
lineno = tb.tb_lineno
elif not tb and f:
lineno = f.f_lineno
else:
raise ValueError("Pass exactly one of tb or f")
co = f.f_code
filename = co.co_filename
name = co.co_name
f_locals = f.f_locals
f_globals = f.f_globals
if self.with_filenames:
s = ' File "%s", line %d' % (filename, lineno)
else:
modname = f_globals.get('__name__', filename)
s = ' Module %s, line %d' % (modname, lineno)
s = s + ', in %s' % name
result = []
result.append(self.escape(s))
# Append the source line, if available
line = linecache.getline(filename, lineno)
if line:
result.append(" " + self.escape(line.strip()))
# Output a traceback supplement, if any.
if '__traceback_supplement__' in f_locals:
# Use the supplement defined in the function.
tbs = f_locals['__traceback_supplement__']
elif '__traceback_supplement__' in f_globals:
# Use the supplement defined in the module.
# This is used by Scripts (Python).
tbs = f_globals['__traceback_supplement__']
else:
tbs = None
if tbs is not None:
factory = tbs[0]
args = tbs[1:]
try:
supp = factory(*args)
result.extend(self.formatSupplement(supp, tb))
except Exception: # pragma: no cover
if DEBUG_EXCEPTION_FORMATTER:
traceback.print_exc()
# else just swallow the exception.
try:
tbi = f_locals.get('__traceback_info__', None)
if tbi is not None:
result.append(self.formatTracebackInfo(tbi))
except Exception: # pragma: no cover
if DEBUG_EXCEPTION_FORMATTER:
traceback.print_exc()
# else just swallow the exception.
return self.line_sep.join(result)
def formatExceptionOnly(self, etype, value):
# We don't want to get an error when we format an error, so we
# compensate in our code. For example, on Python 3.11.0 HTTPError
# gives an unhelpful KeyError in tempfile when Python formats it.
# See https://github.com/python/cpython/issues/90113
try:
result = ''.join(traceback.format_exception_only(etype, value))
except Exception: # pragma: no cover
# This code branch is only covered on Python 3.11.
result = str(value)
return result
def formatLastLine(self, exc_line):
return self.escape(exc_line)
def formatException(self, etype, value, tb):
# The next line provides a way to detect recursion. The 'noqa'
# comment disables a flake8 warning about the unused variable.
__exception_formatter__ = 1 # noqa
result = []
while tb is not None:
if tb.tb_frame.f_locals.get('__exception_formatter__'):
# Stop recursion.
result.append('(Recursive formatException() stopped, '
'trying traceback.format_tb)\n')
result.extend(traceback.format_tb(tb))
break
line = self.formatLine(tb=tb)
result.append(line + '\n')
tb = tb.tb_next
template = (
'...\n'
'{omitted} entries omitted, because limit is {limit}.\n'
'Set sys.tracebacklimit or {klass}.limit to a higher'
' value to see omitted entries\n'
'...')
self._obeyLimit(result, template)
result = [self.getPrefix() + '\n'] + result
exc_line = self.formatExceptionOnly(etype, value)
result.append(self.formatLastLine(exc_line))
return result
def extractStack(self, f=None):
if f is None:
try:
raise ZeroDivisionError
except ZeroDivisionError:
f = sys.exc_info()[2].tb_frame.f_back
# The next line provides a way to detect recursion. The 'noqa'
# comment disables a flake8 warning about the unused variable.
__exception_formatter__ = 1 # noqa
result = []
while f is not None:
if f.f_locals.get('__exception_formatter__'):
# Stop recursion.
result.append('(Recursive extractStack() stopped, '
'trying traceback.format_stack)\n')
res = traceback.format_stack(f)
res.reverse()
result.extend(res)
break
line = self.formatLine(f=f)
result.append(line + '\n')
f = f.f_back
self._obeyLimit(
result,
'...{omitted} entries omitted, because limit is {limit}...\n')
result.reverse()
return result
def _obeyLimit(self, result, template):
limit = self.getLimit()
if limit is not None and len(result) > limit:
# cut out the middle part of the TB
tocut = len(result) - limit
middle = len(result) // 2
lower = middle - tocut // 2
msg = template.format(
omitted=tocut, limit=limit, klass=self.__class__.__name__)
result[lower:lower + tocut] = [msg]
class HTMLExceptionFormatter(TextExceptionFormatter):
line_sep = '<br />\r\n'
def escape(self, s):
if not isinstance(s, str):
try: # pragma: PY2
s = str(s)
except UnicodeError: # pragma: PY2
if hasattr(s, 'encode'):
# We probably got a unicode string on Python 2.
s = s.encode('utf-8')
else: # pragma: no cover
raise
return escape(s, quote=False)
def getPrefix(self):
return '<p>Traceback (most recent call last):</p>\r\n<ul>'
def formatSupplementLine(self, line):
return '<b>%s</b>' % self.escape(line)
def formatSupplementInfo(self, info):
info = self.escape(info)
info = info.replace(" ", " ")
info = info.replace("\n", self.line_sep)
return info
def formatTracebackInfo(self, tbi):
s = self.escape(tbi)
s = s.replace('\n', self.line_sep)
return '__traceback_info__: %s' % (s, )
def formatLine(self, tb=None, f=None):
line = TextExceptionFormatter.formatLine(self, tb, f)
return '<li>%s</li>' % line
def formatLastLine(self, exc_line):
line = '</ul><p>%s</p>' % self.escape(exc_line)
return line.replace('\n', self.line_sep)
def format_exception(t, v, tb, limit=None, as_html=False,
with_filenames=False):
"""Format a stack trace and the exception information.
Similar to 'traceback.format_exception', but adds supplemental
information to the traceback and accepts two options, 'as_html'
and 'with_filenames'.
The result is a list of native strings; on Python 2 they are UTF-8
encoded if need be.
"""
if as_html:
fmt = HTMLExceptionFormatter(limit, with_filenames)
else:
fmt = TextExceptionFormatter(limit, with_filenames)
return fmt.formatException(t, v, tb)
def print_exception(t, v, tb, limit=None, file=None, as_html=False,
with_filenames=True):
"""Print exception up to 'limit' stack trace entries from 'tb' to 'file'.
Similar to 'traceback.print_exception', but adds supplemental
information to the traceback and accepts two options, 'as_html'
and 'with_filenames'.
"""
if file is None: # pragma: no cover
file = sys.stderr
lines = format_exception(t, v, tb, limit, as_html, with_filenames)
for line in lines:
file.write(line)
def extract_stack(f=None, limit=None, as_html=False,
with_filenames=True):
"""Format a stack trace and the exception information.
Similar to 'traceback.extract_stack', but adds supplemental
information to the traceback and accepts two options, 'as_html'
and 'with_filenames'.
"""
if as_html:
fmt = HTMLExceptionFormatter(limit, with_filenames)
else:
fmt = TextExceptionFormatter(limit, with_filenames)
return fmt.extractStack(f)
|