123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- """Simple traceback introspection. Used to add additional information to
- AssertionErrors in tests, so that failure messages may be more informative.
- """
- import inspect
- import logging
- import re
- import sys
- import textwrap
- import tokenize
- try:
- from cStringIO import StringIO
- except ImportError:
- from StringIO import StringIO
- log = logging.getLogger(__name__)
- def inspect_traceback(tb):
- """Inspect a traceback and its frame, returning source for the expression
- where the exception was raised, with simple variable replacement performed
- and the line on which the exception was raised marked with '>>'
- """
- log.debug('inspect traceback %s', tb)
- # we only want the innermost frame, where the exception was raised
- while tb.tb_next:
- tb = tb.tb_next
-
- frame = tb.tb_frame
- lines, exc_line = tbsource(tb)
-
- # figure out the set of lines to grab.
- inspect_lines, mark_line = find_inspectable_lines(lines, exc_line)
- src = StringIO(textwrap.dedent(''.join(inspect_lines)))
- exp = Expander(frame.f_locals, frame.f_globals)
- while inspect_lines:
- try:
- for tok in tokenize.generate_tokens(src.readline):
- exp(*tok)
- except tokenize.TokenError, e:
- # this can happen if our inspectable region happens to butt up
- # against the end of a construct like a docstring with the closing
- # """ on separate line
- log.debug("Tokenizer error: %s", e)
- inspect_lines.pop(0)
- mark_line -= 1
- src = StringIO(textwrap.dedent(''.join(inspect_lines)))
- exp = Expander(frame.f_locals, frame.f_globals)
- continue
- break
- padded = []
- if exp.expanded_source:
- exp_lines = exp.expanded_source.split('\n')
- ep = 0
- for line in exp_lines:
- if ep == mark_line:
- padded.append('>> ' + line)
- else:
- padded.append(' ' + line)
- ep += 1
- return '\n'.join(padded)
- def tbsource(tb, context=6):
- """Get source from a traceback object.
- A tuple of two things is returned: a list of lines of context from
- the source code, and the index of the current line within that list.
- The optional second argument specifies the number of lines of context
- to return, which are centered around the current line.
- .. Note ::
- This is adapted from inspect.py in the python 2.4 standard library,
- since a bug in the 2.3 version of inspect prevents it from correctly
- locating source lines in a traceback frame.
- """
-
- lineno = tb.tb_lineno
- frame = tb.tb_frame
- if context > 0:
- start = lineno - 1 - context//2
- log.debug("lineno: %s start: %s", lineno, start)
-
- try:
- lines, dummy = inspect.findsource(frame)
- except IOError:
- lines, index = [''], 0
- else:
- all_lines = lines
- start = max(start, 1)
- start = max(0, min(start, len(lines) - context))
- lines = lines[start:start+context]
- index = lineno - 1 - start
-
- # python 2.5 compat: if previous line ends in a continuation,
- # decrement start by 1 to match 2.4 behavior
- if sys.version_info >= (2, 5) and index > 0:
- while lines[index-1].strip().endswith('\\'):
- start -= 1
- lines = all_lines[start:start+context]
- else:
- lines, index = [''], 0
- log.debug("tbsource lines '''%s''' around index %s", lines, index)
- return (lines, index)
-
- def find_inspectable_lines(lines, pos):
- """Find lines in home that are inspectable.
-
- Walk back from the err line up to 3 lines, but don't walk back over
- changes in indent level.
- Walk forward up to 3 lines, counting \ separated lines as 1. Don't walk
- over changes in indent level (unless part of an extended line)
- """
- cnt = re.compile(r'\\[\s\n]*$')
- df = re.compile(r':[\s\n]*$')
- ind = re.compile(r'^(\s*)')
- toinspect = []
- home = lines[pos]
- home_indent = ind.match(home).groups()[0]
-
- before = lines[max(pos-3, 0):pos]
- before.reverse()
- after = lines[pos+1:min(pos+4, len(lines))]
- for line in before:
- if ind.match(line).groups()[0] == home_indent:
- toinspect.append(line)
- else:
- break
- toinspect.reverse()
- toinspect.append(home)
- home_pos = len(toinspect)-1
- continued = cnt.search(home)
- for line in after:
- if ((continued or ind.match(line).groups()[0] == home_indent)
- and not df.search(line)):
- toinspect.append(line)
- continued = cnt.search(line)
- else:
- break
- log.debug("Inspecting lines '''%s''' around %s", toinspect, home_pos)
- return toinspect, home_pos
- class Expander:
- """Simple expression expander. Uses tokenize to find the names and
- expands any that can be looked up in the frame.
- """
- def __init__(self, locals, globals):
- self.locals = locals
- self.globals = globals
- self.lpos = None
- self.expanded_source = ''
-
- def __call__(self, ttype, tok, start, end, line):
- # TODO
- # deal with unicode properly
-
- # TODO
- # Dealing with instance members
- # always keep the last thing seen
- # if the current token is a dot,
- # get ready to getattr(lastthing, this thing) on the
- # next call.
-
- if self.lpos is not None:
- if start[1] >= self.lpos:
- self.expanded_source += ' ' * (start[1]-self.lpos)
- elif start[1] < self.lpos:
- # newline, indent correctly
- self.expanded_source += ' ' * start[1]
- self.lpos = end[1]
-
- if ttype == tokenize.INDENT:
- pass
- elif ttype == tokenize.NAME:
- # Clean this junk up
- try:
- val = self.locals[tok]
- if callable(val):
- val = tok
- else:
- val = repr(val)
- except KeyError:
- try:
- val = self.globals[tok]
- if callable(val):
- val = tok
- else:
- val = repr(val)
- except KeyError:
- val = tok
- # FIXME... not sure how to handle things like funcs, classes
- # FIXME this is broken for some unicode strings
- self.expanded_source += val
- else:
- self.expanded_source += tok
- # if this is the end of the line and the line ends with
- # \, then tack a \ and newline onto the output
- # print line[end[1]:]
- if re.match(r'\s+\\\n', line[end[1]:]):
- self.expanded_source += ' \\\n'
|