Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions Include/frameobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ typedef struct _frame {

int f_lasti; /* Last instruction if called */
/* Call PyFrame_GetLineNumber() instead of reading this field
directly. As of 2.3 f_lineno is only valid when tracing is
active (i.e. when f_trace is set). At other times we use
PyCode_Addr2Line to calculate the line from the current
bytecode index. */
directly. As of 2.3 f_lineno is valid when tracing is active. At
other times we use PyCode_Addr2Line to calculate the line from the
current bytecode index and f->f_lineno is -1. */
int f_lineno; /* Current line number */
int f_iblock; /* index in f_blockstack */
char f_executing; /* whether the frame is still executing */
Expand Down
4 changes: 0 additions & 4 deletions Lib/bdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,6 @@ def set_continue(self):
if not self.breaks:
# no breakpoints; run without debugger overhead
sys.settrace(None)
frame = sys._getframe().f_back
while frame and frame is not self.botframe:
del frame.f_trace
frame = frame.f_back

def set_quit(self):
"""Set quitting attribute to True.
Expand Down
35 changes: 35 additions & 0 deletions Lib/test/test_bdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,41 @@ def main():
with TracerRun(self) as tracer:
tracer.runcall(tfunc_import)

def test_botframe_lineno(self):
# Issue #17697.
# This test failed when the condition f->f_trace != NULL was used to
# mean that tracing is active and therefore f->f_lineno are valid. The
# implementation of Bdb.set_continue() did not delete the f_trace of
# self.botframe and in that case all the f.f_lineno in the func()
# frame after returning from subf() have the same value, i.e. the line
# number of the call to subf().
def subf():
lno = 2

def func(tracer):
tracer.set_trace()
f = sys._getframe()
tracer.botframe = f # assume func() is a module
subf()
if f.f_lineno - f.f_code.co_firstlineno + 1 == 6:
tracer.set_trace()
lno = 8

self.expect_set = [
('line', 3, 'func'), ('step',),
('line', 4, 'func'), ('step',),
('line', 5, 'func'), ('step',),
('call', 1, 'subf'), ('step',),
('line', 2, 'subf'), ('continue',),
('line', 8, 'func'), ('step',),
('return', 8, 'func'), ('quit',),
]
with TracerRun(self) as tracer:
try:
func(tracer)
except _bdb.BdbQuit:
pass

def test_main():
test.support.run_unittest(
StateTestCase,
Expand Down
101 changes: 101 additions & 0 deletions Lib/test/test_sys_settrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
import asyncio


def get_frame_lineno(caller_level=0):
f = sys._getframe().f_back
while caller_level and f:
caller_level -= 1
f = f.f_back
return (f.f_lineno - f.f_code.co_firstlineno) if f else None

class tracecontext:
"""Context manager that traces its enter and exit."""
def __init__(self, output, value):
Expand Down Expand Up @@ -447,6 +454,100 @@ def func():
[(0, 'call'),
(1, 'line')])

def test_18_generator(self):
# Issue 17277: the f_lineno attribute of a generator frame is correct
# after the tracer has been removed while the generator was suspended.
def func():
def gen():
yield 1
self.assertEqual(get_frame_lineno(), 2)
yield 3

it = gen()
next(it)
sys.settrace(None)
next(it)

func.events = [
(0, 'call'),
(1, 'line'),
(6, 'line'),
(7, 'line'),
(1, 'call'),
(2, 'line'),
(2, 'return'),
(8, 'line'),
]
self.run_test(func)

def test_19_stack_frame(self):
# Issues 16482 and 17277: the f_lineno of a frame on the call stack is
# correct after the tracer has been removed.
def func():
def remove_tracer():
sys.settrace(None)

def f():
remove_tracer()
self.assertEqual(get_frame_lineno(), 2)

f()

func.events = [
(0, 'call'),
(1, 'line'),
(4, 'line'),
(8, 'line'),
(4, 'call'),
(5, 'line'),
(1, 'call'),
(2, 'line'),
]
self.run_test(func)

def test_20_return_in_caller(self):
# Issue 24565: upon returning in the caller and when the next opcode
# in the caller is RETURN_VALUE, f_lineno is the line of the previous
# line trace event, which is not necessarily the line obtained from
# co_lnotab and f_lasti. See the discussion at the end of
# Objects/lnotab_notes.txt.
def func(tracer):
def set_trace():
sys._getframe().f_back.f_trace = tracer
sys.settrace(tracer)

def f(a):
while a:
lineno = 7
set_trace(); break
else:
lineno = 10

f(True)

func.events = [(8, 'return')]
self.run_test2(func)

def test_21_tracer_removed_after_call(self):
# Issue 7238: remove the tracer after a call event.
def func():
def f():
sys.settrace(None)
self.assertEqual(get_frame_lineno(), 2)
self.assertEqual(get_frame_lineno(), 3)

f()
f()

func.events = [
(0, 'call'),
(1, 'line'),
(6, 'line'),
(1, 'call'),
(2, 'line'),
]
self.run_test(func)


class SkipLineEventsTraceTestCase(TraceTestCase):
"""Repeat the trace tests, but with per-line events skipped"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
When tracing is not set, ``f->f_lineno`` is now ``-1`` (its value was
undefined until this change). When tracing is set, ``f->f_lineno`` is valid
for all the frames on the stack.
12 changes: 3 additions & 9 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ frame_getlocals(PyFrameObject *f, void *closure)
int
PyFrame_GetLineNumber(PyFrameObject *f)
{
if (f->f_trace)
if (f->f_lineno != -1)
return f->f_lineno;
else
return PyCode_Addr2Line(f->f_code, f->f_lasti);
Expand Down Expand Up @@ -116,10 +116,7 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno, void *Py_UNUSED(ignore
return -1;
}

/* Upon the 'call' trace event of a new frame, f->f_lasti is -1 and
* f->f_trace is NULL, check first on the first condition.
* Forbidding jumps from the 'call' event of a new frame is a side effect
* of allowing to set f_lineno only from trace functions. */
/* Upon the 'call' trace event of a new frame, f->f_lasti is -1 */
if (f->f_lasti == -1) {
PyErr_Format(PyExc_ValueError,
"can't jump from the 'call' trace event of a new frame");
Expand Down Expand Up @@ -338,9 +335,6 @@ frame_gettrace(PyFrameObject *f, void *closure)
static int
frame_settrace(PyFrameObject *f, PyObject* v, void *closure)
{
/* We rely on f_lineno being accurate when f_trace is set. */
f->f_lineno = PyFrame_GetLineNumber(f);

if (v == Py_None)
v = NULL;
Py_XINCREF(v);
Expand Down Expand Up @@ -715,7 +709,7 @@ _PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
}

f->f_lasti = -1;
f->f_lineno = code->co_firstlineno;
f->f_lineno = -1;
f->f_iblock = 0;
f->f_executing = 0;
f->f_gen = NULL;
Expand Down
17 changes: 15 additions & 2 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -3644,6 +3644,7 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
tstate, f, PyTrace_RETURN, retval)) {
Py_CLEAR(retval);
}
f->f_lineno = -1;
}
if (tstate->c_profilefunc) {
if (call_trace_protected(tstate->c_profilefunc, tstate->c_profileobj,
Expand Down Expand Up @@ -4466,6 +4467,7 @@ call_trace_protected(Py_tracefunc func, PyObject *obj,
{
PyObject *type, *value, *traceback;
int err;
frame->f_lineno = PyFrame_GetLineNumber(frame);
PyErr_Fetch(&type, &value, &traceback);
err = call_trace(func, obj, tstate, frame, what, arg);
if (err == 0)
Expand Down Expand Up @@ -4572,6 +4574,7 @@ PyEval_SetProfile(Py_tracefunc func, PyObject *arg)
void
PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
{
PyFrameObject *f;
_PyRuntimeState *runtime = &_PyRuntime;
PyThreadState *tstate = _PyRuntimeState_GetThreadState(runtime);
PyObject *temp = tstate->c_traceobj;
Expand All @@ -4587,6 +4590,14 @@ PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
/* Flag that tracing or profiling is turned on */
tstate->use_tracing = ((func != NULL)
|| (tstate->c_profilefunc != NULL));

/* When tracing is not set, ensure that f_lineno is -1 for all the frames
* on the stack.
* When tracing is set, we rely on f_lineno being valid for all the frames
* on the stack for the case where the next opcode would be RETURN_VALUE,
* see the last paragraph of Objects/lnotab_notes.txt. */
for (f = tstate->frame; f != NULL; f = f->f_back)
f->f_lineno = func ? PyCode_Addr2Line(f->f_code, f->f_lasti) : -1;
}

void
Expand Down Expand Up @@ -5473,7 +5484,7 @@ static void
maybe_dtrace_line(PyFrameObject *frame,
int *instr_lb, int *instr_ub, int *instr_prev)
{
int line = frame->f_lineno;
int line = -1;
const char *co_filename, *co_name;

/* If the last instruction executed isn't in the current
Expand All @@ -5490,7 +5501,9 @@ maybe_dtrace_line(PyFrameObject *frame,
it represents a jump backwards, update the frame's line
number and call the trace function. */
if (frame->f_lasti == *instr_lb || frame->f_lasti < *instr_prev) {
frame->f_lineno = line;
if (line == -1) {
line = PyFrame_GetLineNumber(frame);
}
co_filename = PyUnicode_AsUTF8(frame->f_code->co_filename);
if (!co_filename)
co_filename = "?";
Expand Down