1
"""Simple traceback introspection. Used to add additional information to
2
AssertionErrors in tests, so that failure messages may be more informative.
12
from cStringIO import StringIO
14
from StringIO import StringIO
16
log = logging.getLogger(__name__)
18
def inspect_traceback(tb):
19
"""Inspect a traceback and its frame, returning source for the expression
20
where the exception was raised, with simple variable replacement performed
21
and the line on which the exception was raised marked with '>>'
23
log.debug('inspect traceback %s', tb)
25
# we only want the innermost frame, where the exception was raised
30
lines, exc_line = tbsource(tb)
32
# figure out the set of lines to grab.
33
inspect_lines, mark_line = find_inspectable_lines(lines, exc_line)
34
src = StringIO(textwrap.dedent(''.join(inspect_lines)))
35
exp = Expander(frame.f_locals, frame.f_globals)
39
tokenize.tokenize(src.readline, exp)
40
except tokenize.TokenError, e:
41
# this can happen if our inspectable region happens to butt up
42
# against the end of a construct like a docstring with the closing
43
# """ on separate line
44
log.debug("Tokenizer error: %s", e)
47
src = StringIO(textwrap.dedent(''.join(inspect_lines)))
48
exp = Expander(frame.f_locals, frame.f_globals)
52
if exp.expanded_source:
53
exp_lines = exp.expanded_source.split('\n')
55
for line in exp_lines:
57
padded.append('>> ' + line)
59
padded.append(' ' + line)
61
return '\n'.join(padded)
64
def tbsource(tb, context=6):
65
"""Get source from a traceback object.
67
A tuple of two things is returned: a list of lines of context from
68
the source code, and the index of the current line within that list.
69
The optional second argument specifies the number of lines of context
70
to return, which are centered around the current line.
73
This is adapted from inspect.py in the python 2.4 standard library,
74
since a bug in the 2.3 version of inspect prevents it from correctly
75
locating source lines in a traceback frame.
82
start = lineno - 1 - context//2
83
log.debug("lineno: %s start: %s", lineno, start)
86
lines, dummy = inspect.findsource(frame)
88
lines, index = [''], 0
92
start = max(0, min(start, len(lines) - context))
93
lines = lines[start:start+context]
94
index = lineno - 1 - start
96
# python 2.5 compat: if previous line ends in a continuation,
97
# decrement start by 1 to match 2.4 behavior
98
if sys.version_info >= (2, 5) and index > 0:
99
while lines[index-1].strip().endswith('\\'):
101
lines = all_lines[start:start+context]
103
lines, index = [''], 0
104
log.debug("tbsource lines '''%s''' around index %s", lines, index)
105
return (lines, index)
108
def find_inspectable_lines(lines, pos):
109
"""Find lines in home that are inspectable.
111
Walk back from the err line up to 3 lines, but don't walk back over
112
changes in indent level.
114
Walk forward up to 3 lines, counting \ separated lines as 1. Don't walk
115
over changes in indent level (unless part of an extended line)
117
cnt = re.compile(r'\\[\s\n]*$')
118
df = re.compile(r':[\s\n]*$')
119
ind = re.compile(r'^(\s*)')
122
home_indent = ind.match(home).groups()[0]
124
before = lines[max(pos-3, 0):pos]
126
after = lines[pos+1:min(pos+4, len(lines))]
129
if ind.match(line).groups()[0] == home_indent:
130
toinspect.append(line)
134
toinspect.append(home)
135
home_pos = len(toinspect)-1
136
continued = cnt.search(home)
138
if ((continued or ind.match(line).groups()[0] == home_indent)
139
and not df.search(line)):
140
toinspect.append(line)
141
continued = cnt.search(line)
144
log.debug("Inspecting lines '''%s''' around %s", toinspect, home_pos)
145
return toinspect, home_pos
149
"""Simple expression expander. Uses tokenize to find the names and
150
expands any that can be looked up in the frame.
152
def __init__(self, locals, globals):
154
self.globals = globals
156
self.expanded_source = ''
158
def __call__(self, ttype, tok, start, end, line):
160
# deal with unicode properly
163
# Dealing with instance members
164
# always keep the last thing seen
165
# if the current token is a dot,
166
# get ready to getattr(lastthing, this thing) on the
169
if self.lpos is not None and start[1] >= self.lpos:
170
self.expanded_source += ' ' * (start[1]-self.lpos)
171
elif start[1] < self.lpos:
172
# newline, indent correctly
173
self.expanded_source += ' ' * start[1]
176
if ttype == tokenize.INDENT:
178
elif ttype == tokenize.NAME:
181
val = self.locals[tok]
188
val = self.globals[tok]
196
# FIXME... not sure how to handle things like funcs, classes
197
# FIXME this is broken for some unicode strings
198
self.expanded_source += val
200
self.expanded_source += tok
201
# if this is the end of the line and the line ends with
202
# \, then tack a \ and newline onto the output
203
# print line[end[1]:]
204
if re.match(r'\s+\\\n', line[end[1]:]):
205
self.expanded_source += ' \\\n'