inspector.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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. try:
  11. from cStringIO import StringIO
  12. except ImportError:
  13. from StringIO import StringIO
  14. log = logging.getLogger(__name__)
  15. def inspect_traceback(tb):
  16. """Inspect a traceback and its frame, returning source for the expression
  17. where the exception was raised, with simple variable replacement performed
  18. and the line on which the exception was raised marked with '>>'
  19. """
  20. log.debug('inspect traceback %s', tb)
  21. # we only want the innermost frame, where the exception was raised
  22. while tb.tb_next:
  23. tb = tb.tb_next
  24. frame = tb.tb_frame
  25. lines, exc_line = tbsource(tb)
  26. # figure out the set of lines to grab.
  27. inspect_lines, mark_line = find_inspectable_lines(lines, exc_line)
  28. src = StringIO(textwrap.dedent(''.join(inspect_lines)))
  29. exp = Expander(frame.f_locals, frame.f_globals)
  30. while inspect_lines:
  31. try:
  32. for tok in tokenize.generate_tokens(src.readline):
  33. exp(*tok)
  34. except tokenize.TokenError, e:
  35. # this can happen if our inspectable region happens to butt up
  36. # against the end of a construct like a docstring with the closing
  37. # """ on separate line
  38. log.debug("Tokenizer error: %s", e)
  39. inspect_lines.pop(0)
  40. mark_line -= 1
  41. src = StringIO(textwrap.dedent(''.join(inspect_lines)))
  42. exp = Expander(frame.f_locals, frame.f_globals)
  43. continue
  44. break
  45. padded = []
  46. if exp.expanded_source:
  47. exp_lines = exp.expanded_source.split('\n')
  48. ep = 0
  49. for line in exp_lines:
  50. if ep == mark_line:
  51. padded.append('>> ' + line)
  52. else:
  53. padded.append(' ' + line)
  54. ep += 1
  55. return '\n'.join(padded)
  56. def tbsource(tb, context=6):
  57. """Get source from a traceback object.
  58. A tuple of two things is returned: a list of lines of context from
  59. the source code, and the index of the current line within that list.
  60. The optional second argument specifies the number of lines of context
  61. to return, which are centered around the current line.
  62. .. Note ::
  63. This is adapted from inspect.py in the python 2.4 standard library,
  64. since a bug in the 2.3 version of inspect prevents it from correctly
  65. locating source lines in a traceback frame.
  66. """
  67. lineno = tb.tb_lineno
  68. frame = tb.tb_frame
  69. if context > 0:
  70. start = lineno - 1 - context//2
  71. log.debug("lineno: %s start: %s", lineno, start)
  72. try:
  73. lines, dummy = inspect.findsource(frame)
  74. except IOError:
  75. lines, index = [''], 0
  76. else:
  77. all_lines = lines
  78. start = max(start, 1)
  79. start = max(0, min(start, len(lines) - context))
  80. lines = lines[start:start+context]
  81. index = lineno - 1 - start
  82. # python 2.5 compat: if previous line ends in a continuation,
  83. # decrement start by 1 to match 2.4 behavior
  84. if sys.version_info >= (2, 5) and index > 0:
  85. while lines[index-1].strip().endswith('\\'):
  86. start -= 1
  87. lines = all_lines[start:start+context]
  88. else:
  89. lines, index = [''], 0
  90. log.debug("tbsource lines '''%s''' around index %s", lines, index)
  91. return (lines, index)
  92. def find_inspectable_lines(lines, pos):
  93. """Find lines in home that are inspectable.
  94. Walk back from the err line up to 3 lines, but don't walk back over
  95. changes in indent level.
  96. Walk forward up to 3 lines, counting \ separated lines as 1. Don't walk
  97. over changes in indent level (unless part of an extended line)
  98. """
  99. cnt = re.compile(r'\\[\s\n]*$')
  100. df = re.compile(r':[\s\n]*$')
  101. ind = re.compile(r'^(\s*)')
  102. toinspect = []
  103. home = lines[pos]
  104. home_indent = ind.match(home).groups()[0]
  105. before = lines[max(pos-3, 0):pos]
  106. before.reverse()
  107. after = lines[pos+1:min(pos+4, len(lines))]
  108. for line in before:
  109. if ind.match(line).groups()[0] == home_indent:
  110. toinspect.append(line)
  111. else:
  112. break
  113. toinspect.reverse()
  114. toinspect.append(home)
  115. home_pos = len(toinspect)-1
  116. continued = cnt.search(home)
  117. for line in after:
  118. if ((continued or ind.match(line).groups()[0] == home_indent)
  119. and not df.search(line)):
  120. toinspect.append(line)
  121. continued = cnt.search(line)
  122. else:
  123. break
  124. log.debug("Inspecting lines '''%s''' around %s", toinspect, home_pos)
  125. return toinspect, home_pos
  126. class Expander:
  127. """Simple expression expander. Uses tokenize to find the names and
  128. expands any that can be looked up in the frame.
  129. """
  130. def __init__(self, locals, globals):
  131. self.locals = locals
  132. self.globals = globals
  133. self.lpos = None
  134. self.expanded_source = ''
  135. def __call__(self, ttype, tok, start, end, line):
  136. # TODO
  137. # deal with unicode properly
  138. # TODO
  139. # Dealing with instance members
  140. # always keep the last thing seen
  141. # if the current token is a dot,
  142. # get ready to getattr(lastthing, this thing) on the
  143. # next call.
  144. if self.lpos is not None:
  145. if start[1] >= self.lpos:
  146. self.expanded_source += ' ' * (start[1]-self.lpos)
  147. elif start[1] < self.lpos:
  148. # newline, indent correctly
  149. self.expanded_source += ' ' * start[1]
  150. self.lpos = end[1]
  151. if ttype == tokenize.INDENT:
  152. pass
  153. elif ttype == tokenize.NAME:
  154. # Clean this junk up
  155. try:
  156. val = self.locals[tok]
  157. if callable(val):
  158. val = tok
  159. else:
  160. val = repr(val)
  161. except KeyError:
  162. try:
  163. val = self.globals[tok]
  164. if callable(val):
  165. val = tok
  166. else:
  167. val = repr(val)
  168. except KeyError:
  169. val = tok
  170. # FIXME... not sure how to handle things like funcs, classes
  171. # FIXME this is broken for some unicode strings
  172. self.expanded_source += val
  173. else:
  174. self.expanded_source += tok
  175. # if this is the end of the line and the line ends with
  176. # \, then tack a \ and newline onto the output
  177. # print line[end[1]:]
  178. if re.match(r'\s+\\\n', line[end[1]:]):
  179. self.expanded_source += ' \\\n'