diff --git a/Lib/test/test_sys_settrace.py b/Lib/test/test_sys_settrace.py --- a/Lib/test/test_sys_settrace.py +++ b/Lib/test/test_sys_settrace.py @@ -467,16 +467,20 @@ 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 + def __init__(self, function, event='line'): + self.code = function.__code__ self.jumpFrom = function.jump[0] self.jumpTo = function.jump[1] + self.event = event self.done = False def trace(self, frame, event, arg): - if not self.done and frame.f_code == self.function.__code__: + # Jumps may also be tested in a nested function. + if (not self.done and (frame.f_code == self.code or + (frame.f_back and frame.f_back.f_code == self.code))): firstLine = frame.f_code.co_firstlineno - if event == 'line' and frame.f_lineno == firstLine + self.jumpFrom: + if (event == self.event and + frame.f_lineno == firstLine + self.jumpFrom): # Cope with non-integer self.jumpTo (because of # no_jump_to_non_integers below). try: @@ -683,6 +687,26 @@ jump_across_with.jump = (1, 3) jump_across_with.output = [] +def no_jump_from_return_event(output): + lineno = 1 + return +no_jump_from_return_event.jump = (2, 1) +no_jump_from_return_event.output = [] + +def no_jump_from_yield(output): + def gen(): + lineno = 1 + yield 1 + next(gen()) +no_jump_from_yield.jump = (2, 1) +no_jump_from_yield.output = [] + +def no_jump_from_exception_event(output): + lineno = 1 + 1 / 0 +no_jump_from_exception_event.jump = (2, 1) +no_jump_from_exception_event.output = [] + # This verifies that you can't set f_lineno via _getframe or similar # trickery. def no_jump_without_trace_function(): @@ -710,8 +734,8 @@ "Expected: " + repr(expected) + "\n" + "Received: " + repr(received)) - def run_test(self, func): - tracer = JumpTracer(func) + def run_test(self, func, event='line'): + tracer = JumpTracer(func, event) sys.settrace(tracer.trace) output = [] func(output) @@ -761,6 +785,15 @@ def test_jump_across_with(self): self.addCleanup(support.unlink, support.TESTFN) self.run_test(jump_across_with) + def test_no_jump_from_return_event(self): + self.assertRaisesRegex(ValueError, 'line trace function', + self.run_test, no_jump_from_return_event, 'return') + def test_no_jump_from_yield(self): + self.assertRaisesRegex(ValueError, 'yield', + self.run_test, no_jump_from_yield, 'return') + def test_no_jump_from_exception_event(self): + self.assertRaisesRegex(ValueError, 'line trace function', + self.run_test, no_jump_from_exception_event, 'exception') def test_20_large_function(self): d = {} diff --git a/Objects/frameobject.c b/Objects/frameobject.c --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -60,6 +60,9 @@ * 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. + * + * You can only set f_lineno from within a line trace function (not from a + * call, exception or return trace function). */ static int frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno) @@ -95,11 +98,15 @@ return -1; } - /* You can only do this from within a trace function, not via - * _getframe or similar hackery. */ - if (!f->f_trace) + /* You can only do this from within a trace function, not via _getframe or + * similar hackery. It must be a line trace function (f_stacktop remains + * NULL except when within a line trace function and when yield suspends + * the frame, so the test on f_stacktop made here is not sufficient in + * itself to assert we are within a line trace function and the test for + * yield done below achieves the assertion). */ + if (!f->f_trace || !f->f_stacktop) { - PyErr_Format(PyExc_ValueError, + PyErr_SetString(PyExc_ValueError, "f_lineno can only be set by a" " line trace function"); return -1; @@ -196,6 +203,17 @@ for (addr = 0; addr < code_len; addr++) { unsigned char op = code[addr]; switch (op) { + case YIELD_VALUE: + case YIELD_FROM: + /* f_lineno can only be set from within a line trace function and + * we are within a return trace function. */ + if (addr == f->f_lasti) { + PyErr_SetString(PyExc_ValueError, + "can't jump from a yield statement"); + return -1; + } + break; + case SETUP_LOOP: case SETUP_EXCEPT: case SETUP_FINALLY: