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
6 changes: 6 additions & 0 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,12 @@ def _get_code_position_from_tb(tb):
def _get_code_position(code, instruction_index):
if instruction_index < 0:
return (None, None, None, None)
# Byte offset, the same value traceback.c passes to PyCode_Addr2Location.
# co_positions() builds a tuple per earlier instruction. Real code objects
# can look the one offset up directly. Duck-typed code objects only
# implement co_positions().
if isinstance(code, types.CodeType):
return code._co_position_at(instruction_index)
positions_gen = code.co_positions()
# The nth entry in code.co_positions() corresponds to instruction (2*n)th since Python 3.10+
return next(itertools.islice(positions_gen, instruction_index // 2, None))
Expand Down
33 changes: 33 additions & 0 deletions Lib/test/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,39 @@ def func():
self.assertIsNone(line)
self.assertEqual(end_line, new_code.co_firstlineno + 1)

def test_co_position_at_matches_co_positions(self):
# _co_position_at is the single-offset form of co_positions().
# The argument is a byte offset, and each instruction is 2 bytes.
import inspect
import traceback

def func(x):
return x + 1

samples = [
func.__code__,
(lambda y: y).__code__,
func.__code__.replace(co_linetable=b''),
]
for code in samples:
with self.subTest(code=code):
for index, expected in enumerate(code.co_positions()):
offset = index * 2
self.assertEqual(code._co_position_at(offset), expected)
self.assertEqual(
inspect._get_code_position(code, offset), expected)
self.assertEqual(
traceback._get_code_position(code, offset), expected)
self.assertEqual(
inspect._get_code_position(code, -1),
(None, None, None, None))
with self.assertRaises(ValueError):
code._co_position_at(-1)
with self.assertRaises(ValueError):
code._co_position_at(len(code.co_code))
with self.assertRaises(TypeError):
code._co_position_at(None)

def test_code_equality(self):
def f():
try:
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,19 @@ def test_trace(self):
(modfile, 18, 'eggs', [' q = y / 0\n'], 0))
self.assertEqual(frame3.positions, dis.Positions(18, 18, 8, 13))

def test_stack_context_zero_keeps_positions(self):
# context=0 drops source lines. Column positions stay filled, and
# the filename still comes from getsourcefile().
stack = inspect.stack(0)
top = stack[0]
self.assertIsNone(top.code_context)
self.assertIsNone(top.index)
self.assertIn(tuple(top.positions),
list(top.frame.f_code.co_positions()))
self.assertEqual(
top.filename,
inspect.getsourcefile(top.frame) or inspect.getfile(top.frame))

def test_frame(self):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr)
self.assertEqual(args, ['x', 'y'])
Expand Down
4 changes: 4 additions & 0 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,10 @@ def _walk_tb_with_full_positions(tb):
def _get_code_position(code, instruction_index):
if instruction_index < 0:
return (None, None, None, None)
# Byte offset. See inspect._get_code_position. Objects that only
# implement co_positions() keep the iterator path.
if isinstance(code, types.CodeType):
return code._co_position_at(instruction_index)
positions_gen = code.co_positions()
return next(itertools.islice(positions_gen, instruction_index // 2, None))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Speed up :func:`inspect.getframeinfo` and :mod:`traceback` position lookup
on large code objects.
37 changes: 37 additions & 0 deletions Objects/codeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,42 @@ code_positionsiterator(PyObject *self, PyObject* Py_UNUSED(args))
return (PyObject*)pi;
}

/* One position for inspect and traceback.

co_positions() yields one tuple per instruction, so islice() builds a
tuple for every instruction before the requested offset.
PyCode_Addr2Location walks the same table and only fills four ints.
The offset is a byte offset (f_lasti / tb_lasti), not an instruction index.
*/
static PyObject *
code_co_position_at(PyObject *self, PyObject *indexobj)
{
PyCodeObject *code = (PyCodeObject *)self;
int start_line, end_line, start_column, end_column;
long addrq;
Py_ssize_t nbytes;

addrq = PyLong_AsLong(indexobj);
if (addrq == -1 && PyErr_Occurred()) {
return NULL;
}
nbytes = _PyCode_NBYTES(code);
if (addrq < 0 || addrq >= nbytes || addrq > INT_MAX) {
PyErr_SetString(PyExc_ValueError, "bytecode offset out of range");
return NULL;
}
if (!PyCode_Addr2Location(code, (int)addrq, &start_line, &start_column,
&end_line, &end_column)) {
PyErr_SetString(PyExc_SystemError, "PyCode_Addr2Location failed");
return NULL;
}
return Py_BuildValue("(O&O&O&O&)",
_source_offset_converter, &start_line,
_source_offset_converter, &end_line,
_source_offset_converter, &start_column,
_source_offset_converter, &end_column);
}


/******************
* "extra" frame eval info (see PEP 523)
Expand Down Expand Up @@ -2873,6 +2909,7 @@ static struct PyMethodDef code_methods[] = {
{"co_lines", code_linesiterator, METH_NOARGS},
{"co_branches", code_branchesiterator, METH_NOARGS},
{"co_positions", code_positionsiterator, METH_NOARGS},
{"_co_position_at", code_co_position_at, METH_O},
CODE_REPLACE_METHODDEF
CODE__VARNAME_FROM_OPARG_METHODDEF
{"__replace__", _PyCFunction_CAST(code_replace), METH_FASTCALL|METH_KEYWORDS,
Expand Down
Loading