Show the traceback line numbers as well as the current line numbers if an exception is being debugged. Courtesy of pdb++ by Antonio Cuni. Also document -> and >> markers for "list".
diff --git a/Lib/pdb.py b/Lib/pdb.py
index 2531e19..83e1197 100755
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -70,8 +70,10 @@
import linecache
import cmd
import bdb
+import dis
import os
import re
+import code
import pprint
import traceback
import inspect
@@ -107,14 +109,22 @@
def getsourcelines(obj):
lines, lineno = inspect.findsource(obj)
- if inspect.isframe(obj) and lineno == 0 and \
- obj.f_globals is obj.f_locals:
+ if inspect.isframe(obj) and obj.f_globals is obj.f_locals:
# must be a module frame: do not try to cut a block out of it
- return lines, 0
+ return lines, 1
elif inspect.ismodule(obj):
- return lines, 0
+ return lines, 1
return inspect.getblock(lines[lineno:]), lineno+1
+def lasti2lineno(code, lasti):
+ linestarts = list(dis.findlinestarts(code))
+ linestarts.reverse()
+ for i, lineno in linestarts:
+ if lasti >= i:
+ return lineno
+ return 0
+
+
# Interaction prompt line will separate file and call info from code
# text using value of line_prefix string. A newline and arrow may
# be to your liking. You can set it once pdb is imported using the
@@ -133,6 +143,7 @@
self.aliases = {}
self.mainpyfile = ''
self._wait_for_mainpyfile = 0
+ self.tb_lineno = {}
# Try to load readline if it exists
try:
import readline
@@ -179,10 +190,18 @@
self.stack = []
self.curindex = 0
self.curframe = None
+ self.tb_lineno.clear()
- def setup(self, f, t):
+ def setup(self, f, tb):
self.forget()
- self.stack, self.curindex = self.get_stack(f, t)
+ self.stack, self.curindex = self.get_stack(f, tb)
+ while tb:
+ # when setting up post-mortem debugging with a traceback, save all
+ # the original line numbers to be displayed along the current line
+ # numbers (which can be different, e.g. due to finally clauses)
+ lineno = lasti2lineno(tb.tb_frame.f_code, tb.tb_lasti)
+ self.tb_lineno[tb.tb_frame] = lineno
+ tb = tb.tb_next
self.curframe = self.stack[self.curindex][0]
# The f_locals dictionary is updated from the actual frame
# locals whenever the .f_locals accessor is called, so we
@@ -1005,13 +1024,18 @@
def do_list(self, arg):
"""l(ist) [first [,last] | .]
- List source code for the current file.
- Without arguments, list 11 lines around the current line
- or continue the previous listing.
- With . as argument, list 11 lines around the current line.
- With one argument, list 11 lines starting at that line.
- With two arguments, list the given range;
- if the second argument is less than the first, it is a count.
+
+ List source code for the current file. Without arguments,
+ list 11 lines around the current line or continue the previous
+ listing. With . as argument, list 11 lines around the current
+ line. With one argument, list 11 lines starting at that line.
+ With two arguments, list the given range; if the second
+ argument is less than the first, it is a count.
+
+ The current line in the current frame is indicated by "->".
+ If an exception is being debugged, the line where the
+ exception was originally raised or propagated is indicated by
+ ">>", if it differs from the current line.
"""
self.lastcmd = 'list'
last = None
@@ -1039,10 +1063,9 @@
filename = self.curframe.f_code.co_filename
breaklist = self.get_file_breaks(filename)
try:
- # XXX add tb_lineno feature
lines = linecache.getlines(filename, self.curframe.f_globals)
self._print_lines(lines[first-1:last], first, breaklist,
- self.curframe.f_lineno, -1)
+ self.curframe)
self.lineno = min(last, len(lines))
if len(lines) < last:
self.message('[EOF]')
@@ -1061,7 +1084,7 @@
except IOError as err:
self.error(err)
return
- self._print_lines(lines, lineno, breaklist, self.curframe.f_lineno, -1)
+ self._print_lines(lines, lineno, breaklist, self.curframe)
do_ll = do_longlist
def do_source(self, arg):
@@ -1077,10 +1100,15 @@
except (IOError, TypeError) as err:
self.error(err)
return
- self._print_lines(lines, lineno, [], -1, -1)
+ self._print_lines(lines, lineno)
- def _print_lines(self, lines, start, breaks, current, special):
+ def _print_lines(self, lines, start, breaks=(), frame=None):
"""Print a range of lines."""
+ if frame:
+ current_lineno = frame.f_lineno
+ exc_lineno = self.tb_lineno.get(frame, -1)
+ else:
+ current_lineno = exc_lineno = -1
for lineno, line in enumerate(lines, start):
s = str(lineno).rjust(3)
if len(s) < 4:
@@ -1089,9 +1117,9 @@
s += 'B'
else:
s += ' '
- if lineno == current:
+ if lineno == current_lineno:
s += '->'
- elif lineno == special:
+ elif lineno == exc_lineno:
s += '>>'
self.message(s + '\t' + line.rstrip())
diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py
index 0861e1e..6010bf3 100644
--- a/Lib/test/test_pdb.py
+++ b/Lib/test/test_pdb.py
@@ -359,6 +359,68 @@
"""
+def test_post_mortem():
+ """Test post mortem traceback debugging.
+
+ >>> def test_function_2():
+ ... try:
+ ... 1/0
+ ... finally:
+ ... print('Exception!')
+
+ >>> def test_function():
+ ... import pdb; pdb.Pdb().set_trace()
+ ... test_function_2()
+ ... print('Not reached.')
+
+ >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ ... 'next', # step over exception-raising call
+ ... 'bt', # get a backtrace
+ ... 'list', # list code of test_function()
+ ... 'down', # step into test_function_2()
+ ... 'list', # list code of test_function_2()
+ ... 'continue',
+ ... ]):
+ ... try:
+ ... test_function()
+ ... except ZeroDivisionError:
+ ... print('Correctly reraised.')
+ > <doctest test.test_pdb.test_post_mortem[1]>(3)test_function()
+ -> test_function_2()
+ (Pdb) next
+ Exception!
+ ZeroDivisionError: division by zero
+ > <doctest test.test_pdb.test_post_mortem[1]>(3)test_function()
+ -> test_function_2()
+ (Pdb) bt
+ ...
+ <doctest test.test_pdb.test_post_mortem[2]>(10)<module>()
+ -> test_function()
+ > <doctest test.test_pdb.test_post_mortem[1]>(3)test_function()
+ -> test_function_2()
+ <doctest test.test_pdb.test_post_mortem[0]>(3)test_function_2()
+ -> 1/0
+ (Pdb) list
+ 1 def test_function():
+ 2 import pdb; pdb.Pdb().set_trace()
+ 3 -> test_function_2()
+ 4 print('Not reached.')
+ [EOF]
+ (Pdb) down
+ > <doctest test.test_pdb.test_post_mortem[0]>(3)test_function_2()
+ -> 1/0
+ (Pdb) list
+ 1 def test_function_2():
+ 2 try:
+ 3 >> 1/0
+ 4 finally:
+ 5 -> print('Exception!')
+ [EOF]
+ (Pdb) continue
+ Correctly reraised.
+ """
+
+
def test_pdb_skip_modules():
"""This illustrates the simple case of module skipping.