diff options
-rw-r--r-- | Doc/lib/libpdb.tex | 12 | ||||
-rw-r--r-- | Doc/ref/ref3.tex | 12 | ||||
-rw-r--r-- | Doc/whatsnew/whatsnew23.tex | 10 | ||||
-rwxr-xr-x | Lib/pdb.py | 26 | ||||
-rw-r--r-- | Lib/test/test_trace.py | 289 | ||||
-rw-r--r-- | Misc/NEWS | 11 | ||||
-rw-r--r-- | Objects/frameobject.c | 260 |
7 files changed, 607 insertions, 13 deletions
diff --git a/Doc/lib/libpdb.tex b/Doc/lib/libpdb.tex index f8417b8..bf779fe 100644 --- a/Doc/lib/libpdb.tex +++ b/Doc/lib/libpdb.tex @@ -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 @@ -303,7 +313,7 @@ alias pi for k in %1.__dict__.keys(): print "%1.",k,"=",%1.__dict__[k] #Print instance variables in self alias ps pi self \end{verbatim} - + \item[unalias \var{name}] Deletes the specified alias. diff --git a/Doc/ref/ref3.tex b/Doc/ref/ref3.tex index eb1d658..605ed55 100644 --- a/Doc/ref/ref3.tex +++ b/Doc/ref/ref3.tex @@ -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 diff --git a/Doc/whatsnew/whatsnew23.tex b/Doc/whatsnew/whatsnew23.tex index 39b7c70..ee8d644 100644 --- a/Doc/whatsnew/whatsnew23.tex +++ b/Doc/whatsnew/whatsnew23.tex @@ -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 @@ -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() diff --git a/Lib/test/test_trace.py b/Lib/test/test_trace.py index faee713..f973a19 100644 --- a/Lib/test/test_trace.py +++ b/Lib/test/test_trace.py @@ -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() @@ -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. diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 3036ab6..b7b3021 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -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} }; |