This is Richie Hindle's patch

[ 643835 ] Set Next Statement for Python debuggers

with a few tweaks by me: adding an unsigned or two, mentioning that
not all jumps are allowed in the doc for pdb, adding a NEWS item and
a note to whatsnew, and AuCTeX doing something cosmetic to libpdb.tex.
This commit is contained in:
Michael W. Hudson 2002-12-17 16:15:34 +00:00
parent f680cc460c
commit cfd3884882
7 changed files with 607 additions and 13 deletions

View File

@ -255,6 +255,16 @@ Continue execution until the current function returns.
Continue execution, only stop when a breakpoint is encountered.
\item[j(ump) \var{lineno}]
Set the next line that will be executed. Only available in the
bottom-most frame. This lets you jump back and execute code
again, or jump forward to skip code that you don't want to run.
It should be noted that not all jumps are allowed -- for instance it
it not possible to jump into the middle of a for loop or out of a
finally clause.
\item[l(ist) \optional{\var{first\optional{, last}}}]
List source code for the current file. Without arguments, list 11

View File

@ -812,8 +812,7 @@ frame; \member{f_locals} is the dictionary used to look up local
variables; \member{f_globals} is used for global variables;
\member{f_builtins} is used for built-in (intrinsic) names;
\member{f_restricted} is a flag indicating whether the function is
executing in restricted execution mode;
\member{f_lineno} gives the line number and \member{f_lasti} gives the
executing in restricted execution mode; \member{f_lasti} gives the
precise instruction (this is an index into the bytecode string of
the code object).
\withsubitem{(frame attribute)}{
@ -821,7 +820,6 @@ the code object).
\ttindex{f_code}
\ttindex{f_globals}
\ttindex{f_locals}
\ttindex{f_lineno}
\ttindex{f_lasti}
\ttindex{f_builtins}
\ttindex{f_restricted}}
@ -830,12 +828,16 @@ Special writable attributes: \member{f_trace}, if not \code{None}, is a
function called at the start of each source code line (this is used by
the debugger); \member{f_exc_type}, \member{f_exc_value},
\member{f_exc_traceback} represent the most recent exception caught in
this frame.
this frame; \member{f_lineno} is the current line number of the frame
--- writing to this from within a trace function jumps to the given line
(only for the bottom-most frame). A debugger can implement a Jump
command (aka Set Next Statement) by writing to f_lineno.
\withsubitem{(frame attribute)}{
\ttindex{f_trace}
\ttindex{f_exc_type}
\ttindex{f_exc_value}
\ttindex{f_exc_traceback}}
\ttindex{f_exc_traceback}
\ttindex{f_lineno}}
\item[Traceback objects] \label{traceback}
Traceback objects represent a stack trace of an exception. A

View File

@ -12,6 +12,8 @@
% MacOS framework-related changes (section of its own, probably)
% the new set-next-statement functionality of pdb (SF #643835)
%\section{Introduction \label{intro}}
{\large This article is a draft, and is currently up to date for some
@ -1201,13 +1203,13 @@ For example:
\begin{verbatim}
>>> days = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'St', 'Sn']
>>> random.sample(days, 3) # Choose 3 elements
>>> random.sample(days, 3) # Choose 3 elements
['St', 'Sn', 'Th']
>>> random.sample(days, 7) # Choose 7 elements
>>> random.sample(days, 7) # Choose 7 elements
['Tu', 'Th', 'Mo', 'We', 'St', 'Fr', 'Sn']
>>> random.sample(days, 7) # Choose 7 again
>>> random.sample(days, 7) # Choose 7 again
['We', 'Mo', 'Sn', 'Fr', 'Tu', 'St', 'Th']
>>> random.sample(days, 8) # Can't choose eight
>>> random.sample(days, 8) # Can't choose eight
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "random.py", line 414, in sample

View File

@ -506,6 +506,25 @@ class Pdb(bdb.Bdb, cmd.Cmd):
return 1
do_c = do_cont = do_continue
def do_jump(self, arg):
if self.curindex + 1 != len(self.stack):
print "*** You can only jump within the bottom frame"
return
try:
arg = int(arg)
except ValueError:
print "*** The 'jump' command requires a line number."
else:
try:
# Do the jump, fix up our copy of the stack, and display the
# new position
self.curframe.f_lineno = arg
self.stack[self.curindex] = self.stack[self.curindex][0], arg
self.print_stack_entry(self.stack[self.curindex])
except ValueError, e:
print '*** Jump failed:', e
do_j = do_jump
def do_quit(self, arg):
self.set_quit()
return 1
@ -805,6 +824,13 @@ Continue execution until the current function returns."""
print """c(ont(inue))
Continue execution, only stop when a breakpoint is encountered."""
def help_jump(self):
self.help_j()
def help_j(self):
print """j(ump) lineno
Set the next line that will be executed."""
def help_list(self):
self.help_l()

View File

@ -221,9 +221,298 @@ class RaisingTraceFuncTestCase(unittest.TestCase):
def test_exception(self):
self.run_test_for_event('exception')
# 'Jump' tests: assigning to frame.f_lineno within a trace function
# moves the execution position - it's how debuggers implement a Jump
# command (aka. "Set next statement").
class JumpTracer:
"""Defines a trace function that jumps from one place to another,
with the source and destination lines of the jump being defined by
the 'jump' property of the function under test."""
def __init__(self, function):
self.function = function
self.jumpFrom = function.jump[0]
self.jumpTo = function.jump[1]
self.done = False
def trace(self, frame, event, arg):
if not self.done and frame.f_code == self.function.func_code:
firstLine = frame.f_code.co_firstlineno
if frame.f_lineno == firstLine + self.jumpFrom:
# Cope with non-integer self.jumpTo (because of
# no_jump_to_non_integers below).
try:
frame.f_lineno = firstLine + self.jumpTo
except TypeError:
frame.f_lineno = self.jumpTo
self.done = True
return self.trace
# The first set of 'jump' tests are for things that are allowed:
def jump_simple_forwards(output):
output.append(1)
output.append(2)
output.append(3)
jump_simple_forwards.jump = (1, 3)
jump_simple_forwards.output = [3]
def jump_simple_backwards(output):
output.append(1)
output.append(2)
jump_simple_backwards.jump = (2, 1)
jump_simple_backwards.output = [1, 1, 2]
def jump_out_of_block_forwards(output):
for i in 1, 2:
output.append(2)
for j in [3]: # Also tests jumping over a block
output.append(4)
output.append(5)
jump_out_of_block_forwards.jump = (3, 5)
jump_out_of_block_forwards.output = [2, 5]
def jump_out_of_block_backwards(output):
output.append(1)
for i in [1]:
output.append(3)
for j in [2]: # Also tests jumping over a block
output.append(5)
output.append(6)
output.append(7)
jump_out_of_block_backwards.jump = (6, 1)
jump_out_of_block_backwards.output = [1, 3, 5, 1, 3, 5, 6, 7]
def jump_to_codeless_line(output):
output.append(1)
# Jumping to this line should skip to the next one.
output.append(3)
jump_to_codeless_line.jump = (1, 2)
jump_to_codeless_line.output = [3]
def jump_to_same_line(output):
output.append(1)
output.append(2)
output.append(3)
jump_to_same_line.jump = (2, 2)
jump_to_same_line.output = [1, 2, 3]
# Tests jumping within a finally block, and over one.
def jump_in_nested_finally(output):
try:
output.append(2)
finally:
output.append(4)
try:
output.append(6)
finally:
output.append(8)
output.append(9)
jump_in_nested_finally.jump = (4, 9)
jump_in_nested_finally.output = [2, 9]
# The second set of 'jump' tests are for things that are not allowed:
def no_jump_too_far_forwards(output):
try:
output.append(2)
output.append(3)
except ValueError, e:
output.append('after' in str(e))
no_jump_too_far_forwards.jump = (3, 6)
no_jump_too_far_forwards.output = [2, True]
def no_jump_too_far_backwards(output):
try:
output.append(2)
output.append(3)
except ValueError, e:
output.append('before' in str(e))
no_jump_too_far_backwards.jump = (3, -1)
no_jump_too_far_backwards.output = [2, True]
# Test each kind of 'except' line.
def no_jump_to_except_1(output):
try:
output.append(2)
except:
e = sys.exc_info()[1]
output.append('except' in str(e))
no_jump_to_except_1.jump = (2, 3)
no_jump_to_except_1.output = [True]
def no_jump_to_except_2(output):
try:
output.append(2)
except ValueError:
e = sys.exc_info()[1]
output.append('except' in str(e))
no_jump_to_except_2.jump = (2, 3)
no_jump_to_except_2.output = [True]
def no_jump_to_except_3(output):
try:
output.append(2)
except ValueError, e:
output.append('except' in str(e))
no_jump_to_except_3.jump = (2, 3)
no_jump_to_except_3.output = [True]
def no_jump_to_except_4(output):
try:
output.append(2)
except (ValueError, RuntimeError), e:
output.append('except' in str(e))
no_jump_to_except_4.jump = (2, 3)
no_jump_to_except_4.output = [True]
def no_jump_forwards_into_block(output):
try:
output.append(2)
for i in 1, 2:
output.append(4)
except ValueError, e:
output.append('into' in str(e))
no_jump_forwards_into_block.jump = (2, 4)
no_jump_forwards_into_block.output = [True]
def no_jump_backwards_into_block(output):
try:
for i in 1, 2:
output.append(3)
output.append(4)
except ValueError, e:
output.append('into' in str(e))
no_jump_backwards_into_block.jump = (4, 3)
no_jump_backwards_into_block.output = [3, 3, True]
def no_jump_into_finally_block(output):
try:
try:
output.append(3)
x = 1
finally:
output.append(6)
except ValueError, e:
output.append('finally' in str(e))
no_jump_into_finally_block.jump = (4, 6)
no_jump_into_finally_block.output = [3, 6, True] # The 'finally' still runs
def no_jump_out_of_finally_block(output):
try:
try:
output.append(3)
finally:
output.append(5)
output.append(6)
except ValueError, e:
output.append('finally' in str(e))
no_jump_out_of_finally_block.jump = (5, 1)
no_jump_out_of_finally_block.output = [3, True]
# This verifies the line-numbers-must-be-integers rule.
def no_jump_to_non_integers(output):
try:
output.append(2)
except ValueError, e:
output.append('integer' in str(e))
no_jump_to_non_integers.jump = (2, "Spam")
no_jump_to_non_integers.output = [True]
# This verifies that you can't set f_lineno via _getframe or similar
# trickery.
def no_jump_without_trace_function():
try:
previous_frame = sys._getframe().f_back
previous_frame.f_lineno = previous_frame.f_lineno
except ValueError, e:
# This is the exception we wanted; make sure the error message
# talks about trace functions.
if 'trace' not in str(e):
raise
else:
# Something's wrong - the expected exception wasn't raised.
raise RuntimeError, "Trace-function-less jump failed to fail"
class JumpTestCase(unittest.TestCase):
def compare_jump_output(self, expected, received):
if received != expected:
self.fail( "Outputs don't match:\n" +
"Expected: " + repr(expected) + "\n" +
"Received: " + repr(received))
def run_test(self, func):
tracer = JumpTracer(func)
sys.settrace(tracer.trace)
output = []
func(output)
sys.settrace(None)
self.compare_jump_output(func.output, output)
def test_01_jump_simple_forwards(self):
self.run_test(jump_simple_forwards)
def test_02_jump_simple_backwards(self):
self.run_test(jump_simple_backwards)
def test_03_jump_out_of_block_forwards(self):
self.run_test(jump_out_of_block_forwards)
def test_04_jump_out_of_block_backwards(self):
self.run_test(jump_out_of_block_backwards)
def test_05_jump_to_codeless_line(self):
self.run_test(jump_to_codeless_line)
def test_06_jump_to_same_line(self):
self.run_test(jump_to_same_line)
def test_07_jump_in_nested_finally(self):
self.run_test(jump_in_nested_finally)
def test_08_no_jump_too_far_forwards(self):
self.run_test(no_jump_too_far_forwards)
def test_09_no_jump_too_far_backwards(self):
self.run_test(no_jump_too_far_backwards)
def test_10_no_jump_to_except_1(self):
self.run_test(no_jump_to_except_1)
def test_11_no_jump_to_except_2(self):
self.run_test(no_jump_to_except_2)
def test_12_no_jump_to_except_3(self):
self.run_test(no_jump_to_except_3)
def test_13_no_jump_to_except_4(self):
self.run_test(no_jump_to_except_4)
def test_14_no_jump_forwards_into_block(self):
self.run_test(no_jump_forwards_into_block)
def test_15_no_jump_backwards_into_block(self):
self.run_test(no_jump_backwards_into_block)
def test_16_no_jump_into_finally_block(self):
self.run_test(no_jump_into_finally_block)
def test_17_no_jump_out_of_finally_block(self):
self.run_test(no_jump_out_of_finally_block)
def test_18_no_jump_to_non_integers(self):
self.run_test(no_jump_to_non_integers)
def test_19_no_jump_without_trace_function(self):
no_jump_without_trace_function()
def test_main():
test_support.run_unittest(TraceTestCase)
test_support.run_unittest(RaisingTraceFuncTestCase)
test_support.run_unittest(JumpTestCase)
if __name__ == "__main__":
test_main()

View File

@ -84,6 +84,10 @@ Type/class unification and new-style classes
Core and builtins
-----------------
- A frame object's f_lineno attribute can now be written to from a
trace function to change which line will execute next. A command to
exploit this from pdb has been added. [SF patch #643835]
- The _codecs support module for codecs.py was turned into a builtin
module to assure that at least the builtin codecs are available
to the Python parser for source code decoding according to PEP 263.
@ -118,8 +122,8 @@ Core and builtins
- SET_LINENO is gone. co_lnotab is now consulted to determine when to
call the trace function. C code that accessed f_lineno should call
PyCode_Addr2Line instead (f_lineno is still there, but not kept up
to date).
PyCode_Addr2Line instead (f_lineno is still there, but only kept up
to date when there is a trace function set).
- There's a new warning category, FutureWarning. This is used to warn
about a number of situations where the value or sign of an integer
@ -439,6 +443,9 @@ Extension modules
Library
-------
- pdb has a new 'j(ump)' command to select the next line to be
executed.
- The distutils created windows installers now can run a
postinstallation script.

View File

@ -8,6 +8,9 @@
#include "opcode.h"
#include "structmember.h"
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define OFF(x) offsetof(PyFrameObject, x)
static PyMemberDef frame_memberlist[] = {
@ -44,6 +47,260 @@ frame_getlineno(PyFrameObject *f, void *closure)
return PyInt_FromLong(lineno);
}
/* Setter for f_lineno - you can set f_lineno from within a trace function in
* order to jump to a given line of code, subject to some restrictions. Most
* lines are OK to jump to because they don't make any assumptions about the
* state of the stack (obvious because you could remove the line and the code
* would still work without any stack errors), but there are some constructs
* that limit jumping:
*
* o Lines with an 'except' statement on them can't be jumped to, because
* they expect an exception to be on the top of the stack.
* o Lines that live in a 'finally' block can't be jumped from or to, since
* the END_FINALLY expects to clean up the stack after the 'try' block.
* o 'try'/'for'/'while' blocks can't be jumped into because the blockstack
* needs to be set up before their code runs, and for 'for' loops the
* iterator needs to be on the stack.
*/
static int
frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
{
int new_lineno = 0; /* The new value of f_lineno */
int new_lasti = 0; /* The new value of f_lasti */
int new_iblock = 0; /* The new value of f_iblock */
char *code = NULL; /* The bytecode for the frame... */
int code_len = 0; /* ...and its length */
char *lnotab = NULL; /* Iterating over co_lnotab */
int lnotab_len = 0; /* (ditto) */
int offset = 0; /* (ditto) */
int line = 0; /* (ditto) */
int addr = 0; /* (ditto) */
int min_addr = 0; /* Scanning the SETUPs and POPs */
int max_addr = 0; /* (ditto) */
int delta_iblock = 0; /* (ditto) */
int min_delta_iblock = 0; /* (ditto) */
int min_iblock = 0; /* (ditto) */
int f_lasti_setup_addr = 0; /* Policing no-jump-into-finally */
int new_lasti_setup_addr = 0; /* (ditto) */
int blockstack[CO_MAXBLOCKS]; /* Walking the 'finally' blocks */
int in_finally[CO_MAXBLOCKS]; /* (ditto) */
int blockstack_top = 0; /* (ditto) */
int setup_op = 0; /* (ditto) */
/* f_lineno must be an integer. */
if (!PyInt_Check(p_new_lineno)) {
PyErr_SetString(PyExc_ValueError,
"lineno must be an integer");
return -1;
}
/* You can only do this from within a trace function, not via
* _getframe or similar hackery. */
if (!f->f_trace)
{
PyErr_Format(PyExc_ValueError,
"f_lineno can only be set by a trace function");
return -1;
}
/* Fail if the line comes before the start of the code block. */
new_lineno = (int) PyInt_AsLong(p_new_lineno);
if (new_lineno < f->f_code->co_firstlineno) {
PyErr_Format(PyExc_ValueError,
"line %d comes before the current code block",
new_lineno);
return -1;
}
/* Find the bytecode offset for the start of the given line, or the
* first code-owning line after it. */
PyString_AsStringAndSize(f->f_code->co_lnotab, &lnotab, &lnotab_len);
addr = 0;
line = f->f_code->co_firstlineno;
new_lasti = -1;
for (offset = 0; offset < lnotab_len; offset += 2) {
addr += lnotab[offset];
line += lnotab[offset+1];
if (line >= new_lineno) {
new_lasti = addr;
new_lineno = line;
break;
}
}
/* If we didn't reach the requested line, return an error. */
if (new_lasti == -1) {
PyErr_Format(PyExc_ValueError,
"line %d comes after the current code block",
new_lineno);
return -1;
}
/* We're now ready to look at the bytecode. */
PyString_AsStringAndSize(f->f_code->co_code, &code, &code_len);
min_addr = MIN(new_lasti, f->f_lasti);
max_addr = MAX(new_lasti, f->f_lasti);
/* You can't jump onto a line with an 'except' statement on it -
* they expect to have an exception on the top of the stack, which
* won't be true if you jump to them. They always start with code
* that either pops the exception using POP_TOP (plain 'except:'
* lines do this) or duplicates the exception on the stack using
* DUP_TOP (if there's an exception type specified). See compile.c,
* 'com_try_except' for the full details. There aren't any other
* cases (AFAIK) where a line's code can start with DUP_TOP or
* POP_TOP, but if any ever appear, they'll be subject to the same
* restriction (but with a different error message). */
if (code[new_lasti] == DUP_TOP || code[new_lasti] == POP_TOP) {
PyErr_SetString(PyExc_ValueError,
"can't jump to 'except' line as there's no exception");
return -1;
}
/* You can't jump into or out of a 'finally' block because the 'try'
* block leaves something on the stack for the END_FINALLY to clean
* up. So we walk the bytecode, maintaining a simulated blockstack.
* When we reach the old or new address and it's in a 'finally' block
* we note the address of the corresponding SETUP_FINALLY. The jump
* is only legal if neither address is in a 'finally' block or
* they're both in the same one. 'blockstack' is a stack of the
* bytecode addresses of the SETUP_X opcodes, and 'in_finally' tracks
* whether we're in a 'finally' block at each blockstack level. */
f_lasti_setup_addr = -1;
new_lasti_setup_addr = -1;
memset(blockstack, '\0', sizeof(blockstack));
memset(in_finally, '\0', sizeof(in_finally));
blockstack_top = 0;
for (addr = 0; addr < code_len; addr++) {
unsigned char op = code[addr];
switch (op) {
case SETUP_LOOP:
case SETUP_EXCEPT:
case SETUP_FINALLY:
blockstack[blockstack_top++] = addr;
in_finally[blockstack_top-1] = 0;
break;
case POP_BLOCK:
setup_op = code[blockstack[blockstack_top-1]];
if (setup_op == SETUP_FINALLY) {
in_finally[blockstack_top-1] = 1;
}
else {
blockstack_top--;
}
break;
case END_FINALLY:
/* Ignore END_FINALLYs for SETUP_EXCEPTs - they exist
* in the bytecode but don't correspond to an actual
* 'finally' block. */
setup_op = code[blockstack[blockstack_top-1]];
if (setup_op == SETUP_FINALLY) {
blockstack_top--;
}
break;
}
/* For the addresses we're interested in, see whether they're
* within a 'finally' block and if so, remember the address
* of the SETUP_FINALLY. */
if (addr == new_lasti || addr == f->f_lasti) {
int i = 0;
int setup_addr = -1;
for (i = blockstack_top-1; i >= 0; i--) {
if (in_finally[i]) {
setup_addr = blockstack[i];
break;
}
}
if (setup_addr != -1) {
if (addr == new_lasti) {
new_lasti_setup_addr = setup_addr;
}
if (addr == f->f_lasti) {
f_lasti_setup_addr = setup_addr;
}
}
}
if (op >= HAVE_ARGUMENT) {
addr += 2;
}
}
if (new_lasti_setup_addr != f_lasti_setup_addr) {
PyErr_SetString(PyExc_ValueError,
"can't jump into or out of a 'finally' block");
return -1;
}
/* Police block-jumping (you can't jump into the middle of a block)
* and ensure that the blockstack finishes up in a sensible state (by
* popping any blocks we're jumping out of). We look at all the
* blockstack operations between the current position and the new
* one, and keep track of how many blocks we drop out of on the way.
* By also keeping track of the lowest blockstack position we see, we
* can tell whether the jump goes into any blocks without coming out
* again - in that case we raise an exception below. */
delta_iblock = 0;
for (addr = min_addr; addr < max_addr; addr++) {
unsigned char op = code[addr];
switch (op) {
case SETUP_LOOP:
case SETUP_EXCEPT:
case SETUP_FINALLY:
delta_iblock++;
break;
case POP_BLOCK:
delta_iblock--;
break;
}
min_delta_iblock = MIN(min_delta_iblock, delta_iblock);
if (op >= HAVE_ARGUMENT) {
addr += 2;
}
}
/* Derive the absolute iblock values from the deltas. */
min_iblock = f->f_iblock + min_delta_iblock;
if (new_lasti > f->f_lasti) {
/* Forwards jump. */
new_iblock = f->f_iblock + delta_iblock;
}
else {
/* Backwards jump. */
new_iblock = f->f_iblock - delta_iblock;
}
/* Are we jumping into a block? */
if (new_iblock > min_iblock) {
PyErr_SetString(PyExc_ValueError,
"can't jump into the middle of a block");
return -1;
}
/* Pop any blocks that we're jumping out of. */
while (f->f_iblock > new_iblock) {
PyTryBlock *b = &f->f_blockstack[--f->f_iblock];
while ((f->f_stacktop - f->f_valuestack) > b->b_level) {
PyObject *v = (*--f->f_stacktop);
Py_DECREF(v);
}
}
/* Finally set the new f_lineno and f_lasti and return OK. */
f->f_lineno = new_lineno;
f->f_lasti = new_lasti;
return 0;
}
static PyObject *
frame_gettrace(PyFrameObject *f, void *closure)
{
@ -77,7 +334,8 @@ frame_settrace(PyFrameObject *f, PyObject* v, void *closure)
static PyGetSetDef frame_getsetlist[] = {
{"f_locals", (getter)frame_getlocals, NULL, NULL},
{"f_lineno", (getter)frame_getlineno, NULL, NULL},
{"f_lineno", (getter)frame_getlineno,
(setter)frame_setlineno, NULL},
{"f_trace", (getter)frame_gettrace, (setter)frame_settrace, NULL},
{0}
};