| 1 | """Simple traceback introspection. Used to add additional information to |
|---|
| 2 | AssertionErrors in tests, so that failure messages may be more informative. |
|---|
| 3 | """ |
|---|
| 4 | import inspect |
|---|
| 5 | import logging |
|---|
| 6 | import re |
|---|
| 7 | import sys |
|---|
| 8 | import textwrap |
|---|
| 9 | import tokenize |
|---|
| 10 | |
|---|
| 11 | try: |
|---|
| 12 | from cStringIO import StringIO |
|---|
| 13 | except ImportError: |
|---|
| 14 | from StringIO import StringIO |
|---|
| 15 | |
|---|
| 16 | log = logging.getLogger(__name__) |
|---|
| 17 | |
|---|
| 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 '>>' |
|---|
| 22 | """ |
|---|
| 23 | log.debug('inspect traceback %s', tb) |
|---|
| 24 | |
|---|
| 25 | # we only want the innermost frame, where the exception was raised |
|---|
| 26 | while tb.tb_next: |
|---|
| 27 | tb = tb.tb_next |
|---|
| 28 | |
|---|
| 29 | frame = tb.tb_frame |
|---|
| 30 | lines, exc_line = tbsource(tb) |
|---|
| 31 | |
|---|
| 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) |
|---|
| 36 | |
|---|
| 37 | while inspect_lines: |
|---|
| 38 | try: |
|---|
| 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) |
|---|
| 45 | inspect_lines.pop(0) |
|---|
| 46 | mark_line -= 1 |
|---|
| 47 | src = StringIO(textwrap.dedent(''.join(inspect_lines))) |
|---|
| 48 | exp = Expander(frame.f_locals, frame.f_globals) |
|---|
| 49 | continue |
|---|
| 50 | break |
|---|
| 51 | padded = [] |
|---|
| 52 | if exp.expanded_source: |
|---|
| 53 | exp_lines = exp.expanded_source.split('\n') |
|---|
| 54 | ep = 0 |
|---|
| 55 | for line in exp_lines: |
|---|
| 56 | if ep == mark_line: |
|---|
| 57 | padded.append('>> ' + line) |
|---|
| 58 | else: |
|---|
| 59 | padded.append(' ' + line) |
|---|
| 60 | ep += 1 |
|---|
| 61 | return '\n'.join(padded) |
|---|
| 62 | |
|---|
| 63 | |
|---|
| 64 | def tbsource(tb, context=6): |
|---|
| 65 | """Get source from a traceback object. |
|---|
| 66 | |
|---|
| 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. |
|---|
| 71 | |
|---|
| 72 | .. Note :: |
|---|
| 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. |
|---|
| 76 | """ |
|---|
| 77 | |
|---|
| 78 | lineno = tb.tb_lineno |
|---|
| 79 | frame = tb.tb_frame |
|---|
| 80 | |
|---|
| 81 | if context > 0: |
|---|
| 82 | start = lineno - 1 - context//2 |
|---|
| 83 | log.debug("lineno: %s start: %s", lineno, start) |
|---|
| 84 | |
|---|
| 85 | try: |
|---|
| 86 | lines, dummy = inspect.findsource(frame) |
|---|
| 87 | except IOError: |
|---|
| 88 | lines, index = [''], 0 |
|---|
| 89 | else: |
|---|
| 90 | all_lines = lines |
|---|
| 91 | start = max(start, 1) |
|---|
| 92 | start = max(0, min(start, len(lines) - context)) |
|---|
| 93 | lines = lines[start:start+context] |
|---|
| 94 | index = lineno - 1 - start |
|---|
| 95 | |
|---|
| 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('\\'): |
|---|
| 100 | start -= 1 |
|---|
| 101 | lines = all_lines[start:start+context] |
|---|
| 102 | else: |
|---|
| 103 | lines, index = [''], 0 |
|---|
| 104 | log.debug("tbsource lines '''%s''' around index %s", lines, index) |
|---|
| 105 | return (lines, index) |
|---|
| 106 | |
|---|
| 107 | |
|---|
| 108 | def find_inspectable_lines(lines, pos): |
|---|
| 109 | """Find lines in home that are inspectable. |
|---|
| 110 | |
|---|
| 111 | Walk back from the err line up to 3 lines, but don't walk back over |
|---|
| 112 | changes in indent level. |
|---|
| 113 | |
|---|
| 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) |
|---|
| 116 | """ |
|---|
| 117 | cnt = re.compile(r'\\[\s\n]*$') |
|---|
| 118 | df = re.compile(r':[\s\n]*$') |
|---|
| 119 | ind = re.compile(r'^(\s*)') |
|---|
| 120 | toinspect = [] |
|---|
| 121 | home = lines[pos] |
|---|
| 122 | home_indent = ind.match(home).groups()[0] |
|---|
| 123 | |
|---|
| 124 | before = lines[max(pos-3, 0):pos] |
|---|
| 125 | before.reverse() |
|---|
| 126 | after = lines[pos+1:min(pos+4, len(lines))] |
|---|
| 127 | |
|---|
| 128 | for line in before: |
|---|
| 129 | if ind.match(line).groups()[0] == home_indent: |
|---|
| 130 | toinspect.append(line) |
|---|
| 131 | else: |
|---|
| 132 | break |
|---|
| 133 | toinspect.reverse() |
|---|
| 134 | toinspect.append(home) |
|---|
| 135 | home_pos = len(toinspect)-1 |
|---|
| 136 | continued = cnt.search(home) |
|---|
| 137 | for line in after: |
|---|
| 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) |
|---|
| 142 | else: |
|---|
| 143 | break |
|---|
| 144 | log.debug("Inspecting lines '''%s''' around %s", toinspect, home_pos) |
|---|
| 145 | return toinspect, home_pos |
|---|
| 146 | |
|---|
| 147 | |
|---|
| 148 | class Expander: |
|---|
| 149 | """Simple expression expander. Uses tokenize to find the names and |
|---|
| 150 | expands any that can be looked up in the frame. |
|---|
| 151 | """ |
|---|
| 152 | def __init__(self, locals, globals): |
|---|
| 153 | self.locals = locals |
|---|
| 154 | self.globals = globals |
|---|
| 155 | self.lpos = None |
|---|
| 156 | self.expanded_source = '' |
|---|
| 157 | |
|---|
| 158 | def __call__(self, ttype, tok, start, end, line): |
|---|
| 159 | # TODO |
|---|
| 160 | # deal with unicode properly |
|---|
| 161 | |
|---|
| 162 | # TODO |
|---|
| 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 |
|---|
| 167 | # next call. |
|---|
| 168 | |
|---|
| 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] |
|---|
| 174 | self.lpos = end[1] |
|---|
| 175 | |
|---|
| 176 | if ttype == tokenize.INDENT: |
|---|
| 177 | pass |
|---|
| 178 | elif ttype == tokenize.NAME: |
|---|
| 179 | # Clean this junk up |
|---|
| 180 | try: |
|---|
| 181 | val = self.locals[tok] |
|---|
| 182 | if callable(val): |
|---|
| 183 | val = tok |
|---|
| 184 | else: |
|---|
| 185 | val = repr(val) |
|---|
| 186 | except KeyError: |
|---|
| 187 | try: |
|---|
| 188 | val = self.globals[tok] |
|---|
| 189 | if callable(val): |
|---|
| 190 | val = tok |
|---|
| 191 | else: |
|---|
| 192 | val = repr(val) |
|---|
| 193 | |
|---|
| 194 | except KeyError: |
|---|
| 195 | val = 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 |
|---|
| 199 | else: |
|---|
| 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' |
|---|