Skip to content

Commit 0daa99f

Browse files
authored
gh-88116: Enhance the inspect frame APIs to use the extended position information (GH-91531)
1 parent a3f2cf3 commit 0daa99f

File tree

5 files changed

+193
-38
lines changed

5 files changed

+193
-38
lines changed

Doc/library/inspect.rst

Lines changed: 102 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,17 +1163,85 @@ Classes and functions
11631163
The interpreter stack
11641164
---------------------
11651165

1166-
When the following functions return "frame records," each record is a
1167-
:term:`named tuple`
1168-
``FrameInfo(frame, filename, lineno, function, code_context, index)``.
1169-
The tuple contains the frame object, the filename, the line number of the
1170-
current line,
1171-
the function name, a list of lines of context from the source code, and the
1172-
index of the current line within that list.
1166+
Some of the following functions return
1167+
:class:`FrameInfo` objects. For backwards compatibility these objects allow
1168+
tuple-like operations on all attributes except ``positions``. This behavior
1169+
is considered deprecated and may be removed in the future.
1170+
1171+
.. class:: FrameInfo
1172+
1173+
.. attribute:: frame
1174+
1175+
The :ref:`frame object <frame-objects>` that the record corresponds to.
1176+
1177+
.. attribute:: filename
1178+
1179+
The file name associated with the code being executed by the frame this record
1180+
corresponds to.
1181+
1182+
.. attribute:: lineno
1183+
1184+
The line number of the current line associated with the code being
1185+
executed by the frame this record corresponds to.
1186+
1187+
.. attribute:: function
1188+
1189+
The function name that is being executed by the frame this record corresponds to.
1190+
1191+
.. attribute:: code_context
1192+
1193+
A list of lines of context from the source code that's being executed by the frame
1194+
this record corresponds to.
1195+
1196+
.. attribute:: index
1197+
1198+
The index of the current line being executed in the :attr:`code_context` list.
1199+
1200+
.. attribute:: positions
1201+
1202+
A :class:`dis.Positions` object containing the start line number, end line
1203+
number, start column offset, and end column offset associated with the
1204+
instruction being executed by the frame this record corresponds to.
11731205

11741206
.. versionchanged:: 3.5
11751207
Return a named tuple instead of a tuple.
11761208

1209+
.. versionchanged:: 3.11
1210+
Changed the return object from a named tuple to a regular object (that is
1211+
backwards compatible with the previous named tuple).
1212+
1213+
.. class:: Traceback
1214+
1215+
.. attribute:: filename
1216+
1217+
The file name associated with the code being executed by the frame this traceback
1218+
corresponds to.
1219+
1220+
.. attribute:: lineno
1221+
1222+
The line number of the current line associated with the code being
1223+
executed by the frame this traceback corresponds to.
1224+
1225+
.. attribute:: function
1226+
1227+
The function name that is being executed by the frame this traceback corresponds to.
1228+
1229+
.. attribute:: code_context
1230+
1231+
A list of lines of context from the source code that's being executed by the frame
1232+
this traceback corresponds to.
1233+
1234+
.. attribute:: index
1235+
1236+
The index of the current line being executed in the :attr:`code_context` list.
1237+
1238+
.. attribute:: positions
1239+
1240+
A :class:`dis.Positions` object containing the start line number, end
1241+
line number, start column offset, and end column offset associated with
1242+
the instruction being executed by the frame this traceback corresponds
1243+
to.
1244+
11771245
.. note::
11781246

11791247
Keeping references to frame objects, as found in the first element of the frame
@@ -1207,35 +1275,41 @@ line.
12071275

12081276
.. function:: getframeinfo(frame, context=1)
12091277

1210-
Get information about a frame or traceback object. A :term:`named tuple`
1211-
``Traceback(filename, lineno, function, code_context, index)`` is returned.
1278+
Get information about a frame or traceback object. A :class:`Traceback` object
1279+
is returned.
12121280

1281+
.. versionchanged:: 3.11
1282+
A :class:`Traceback` object is returned instead of a named tuple.
12131283

12141284
.. function:: getouterframes(frame, context=1)
12151285

1216-
Get a list of frame records for a frame and all outer frames. These frames
1217-
represent the calls that lead to the creation of *frame*. The first entry in the
1218-
returned list represents *frame*; the last entry represents the outermost call
1219-
on *frame*'s stack.
1286+
Get a list of :class:`FrameInfo` objects for a frame and all outer frames.
1287+
These frames represent the calls that lead to the creation of *frame*. The
1288+
first entry in the returned list represents *frame*; the last entry
1289+
represents the outermost call on *frame*'s stack.
12201290

12211291
.. versionchanged:: 3.5
12221292
A list of :term:`named tuples <named tuple>`
12231293
``FrameInfo(frame, filename, lineno, function, code_context, index)``
12241294
is returned.
12251295

1296+
.. versionchanged:: 3.11
1297+
A list of :class:`FrameInfo` objects is returned.
12261298

12271299
.. function:: getinnerframes(traceback, context=1)
12281300

1229-
Get a list of frame records for a traceback's frame and all inner frames. These
1230-
frames represent calls made as a consequence of *frame*. The first entry in the
1231-
list represents *traceback*; the last entry represents where the exception was
1232-
raised.
1301+
Get a list of :class:`FrameInfo` objects for a traceback's frame and all
1302+
inner frames. These frames represent calls made as a consequence of *frame*.
1303+
The first entry in the list represents *traceback*; the last entry represents
1304+
where the exception was raised.
12331305

12341306
.. versionchanged:: 3.5
12351307
A list of :term:`named tuples <named tuple>`
12361308
``FrameInfo(frame, filename, lineno, function, code_context, index)``
12371309
is returned.
12381310

1311+
.. versionchanged:: 3.11
1312+
A list of :class:`FrameInfo` objects is returned.
12391313

12401314
.. function:: currentframe()
12411315

@@ -1251,28 +1325,32 @@ line.
12511325

12521326
.. function:: stack(context=1)
12531327

1254-
Return a list of frame records for the caller's stack. The first entry in the
1255-
returned list represents the caller; the last entry represents the outermost
1256-
call on the stack.
1328+
Return a list of :class:`FrameInfo` objects for the caller's stack. The
1329+
first entry in the returned list represents the caller; the last entry
1330+
represents the outermost call on the stack.
12571331

12581332
.. versionchanged:: 3.5
12591333
A list of :term:`named tuples <named tuple>`
12601334
``FrameInfo(frame, filename, lineno, function, code_context, index)``
12611335
is returned.
12621336

1337+
.. versionchanged:: 3.11
1338+
A list of :class:`FrameInfo` objects is returned.
12631339

12641340
.. function:: trace(context=1)
12651341

1266-
Return a list of frame records for the stack between the current frame and the
1267-
frame in which an exception currently being handled was raised in. The first
1268-
entry in the list represents the caller; the last entry represents where the
1269-
exception was raised.
1342+
Return a list of :class:`FrameInfo` objects for the stack between the current
1343+
frame and the frame in which an exception currently being handled was raised
1344+
in. The first entry in the list represents the caller; the last entry
1345+
represents where the exception was raised.
12701346

12711347
.. versionchanged:: 3.5
12721348
A list of :term:`named tuples <named tuple>`
12731349
``FrameInfo(frame, filename, lineno, function, code_context, index)``
12741350
is returned.
12751351

1352+
.. versionchanged:: 3.11
1353+
A list of :class:`FrameInfo` objects is returned.
12761354

12771355
Fetching attributes statically
12781356
------------------------------

Doc/whatsnew/3.11.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ inspect
326326
* Add :func:`inspect.ismethodwrapper` for checking if the type of an object is a
327327
:class:`~types.MethodWrapperType`. (Contributed by Hakan Çelik in :issue:`29418`.)
328328

329+
* Change the frame-related functions in the :mod:`inspect` module to return a
330+
regular object (that is backwards compatible with the old tuple-like
331+
interface) that include the extended :pep:`657` position information (end
332+
line number, column and end column). The affected functions are:
333+
:func:`inspect.getframeinfo`, :func:`inspect.getouterframes`, :func:`inspect.getinnerframes`,
334+
:func:`inspect.stack` and :func:`inspect.trace`. (Contributed by Pablo Galindo in
335+
:issue:`88116`)
336+
329337
locale
330338
------
331339

Lib/inspect.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,7 +1638,30 @@ def getclosurevars(func):
16381638

16391639
# -------------------------------------------------- stack frame extraction
16401640

1641-
Traceback = namedtuple('Traceback', 'filename lineno function code_context index')
1641+
_Traceback = namedtuple('_Traceback', 'filename lineno function code_context index')
1642+
1643+
class Traceback(_Traceback):
1644+
def __new__(cls, filename, lineno, function, code_context, index, *, positions=None):
1645+
instance = super().__new__(cls, filename, lineno, function, code_context, index)
1646+
instance.positions = positions
1647+
return instance
1648+
1649+
def __repr__(self):
1650+
return ('Traceback(filename={!r}, lineno={!r}, function={!r}, '
1651+
'code_context={!r}, index={!r}, positions={!r})'.format(
1652+
self.filename, self.lineno, self.function, self.code_context,
1653+
self.index, self.positions))
1654+
1655+
def _get_code_position_from_tb(tb):
1656+
code, instruction_index = tb.tb_frame.f_code, tb.tb_lasti
1657+
return _get_code_position(code, instruction_index)
1658+
1659+
def _get_code_position(code, instruction_index):
1660+
if instruction_index < 0:
1661+
return (None, None, None, None)
1662+
positions_gen = code.co_positions()
1663+
# The nth entry in code.co_positions() corresponds to instruction (2*n)th since Python 3.10+
1664+
return next(itertools.islice(positions_gen, instruction_index // 2, None))
16421665

16431666
def getframeinfo(frame, context=1):
16441667
"""Get information about a frame or traceback object.
@@ -1649,10 +1672,20 @@ def getframeinfo(frame, context=1):
16491672
The optional second argument specifies the number of lines of context
16501673
to return, which are centered around the current line."""
16511674
if istraceback(frame):
1675+
positions = _get_code_position_from_tb(frame)
16521676
lineno = frame.tb_lineno
16531677
frame = frame.tb_frame
16541678
else:
16551679
lineno = frame.f_lineno
1680+
positions = _get_code_position(frame.f_code, frame.f_lasti)
1681+
1682+
if positions[0] is None:
1683+
frame, *positions = (frame, lineno, *positions[1:])
1684+
else:
1685+
frame, *positions = (frame, *positions)
1686+
1687+
lineno = positions[0]
1688+
16561689
if not isframe(frame):
16571690
raise TypeError('{!r} is not a frame or traceback object'.format(frame))
16581691

@@ -1670,14 +1703,26 @@ def getframeinfo(frame, context=1):
16701703
else:
16711704
lines = index = None
16721705

1673-
return Traceback(filename, lineno, frame.f_code.co_name, lines, index)
1706+
return Traceback(filename, lineno, frame.f_code.co_name, lines,
1707+
index, positions=dis.Positions(*positions))
16741708

16751709
def getlineno(frame):
16761710
"""Get the line number from a frame object, allowing for optimization."""
16771711
# FrameType.f_lineno is now a descriptor that grovels co_lnotab
16781712
return frame.f_lineno
16791713

1680-
FrameInfo = namedtuple('FrameInfo', ('frame',) + Traceback._fields)
1714+
_FrameInfo = namedtuple('_FrameInfo', ('frame',) + Traceback._fields)
1715+
class FrameInfo(_FrameInfo):
1716+
def __new__(cls, frame, filename, lineno, function, code_context, index, *, positions=None):
1717+
instance = super().__new__(cls, frame, filename, lineno, function, code_context, index)
1718+
instance.positions = positions
1719+
return instance
1720+
1721+
def __repr__(self):
1722+
return ('FrameInfo(frame={!r}, filename={!r}, lineno={!r}, function={!r}, '
1723+
'code_context={!r}, index={!r}, positions={!r})'.format(
1724+
self.frame, self.filename, self.lineno, self.function,
1725+
self.code_context, self.index, self.positions))
16811726

16821727
def getouterframes(frame, context=1):
16831728
"""Get a list of records for a frame and all higher (calling) frames.
@@ -1686,8 +1731,9 @@ def getouterframes(frame, context=1):
16861731
name, a list of lines of context, and index within the context."""
16871732
framelist = []
16881733
while frame:
1689-
frameinfo = (frame,) + getframeinfo(frame, context)
1690-
framelist.append(FrameInfo(*frameinfo))
1734+
traceback_info = getframeinfo(frame, context)
1735+
frameinfo = (frame,) + traceback_info
1736+
framelist.append(FrameInfo(*frameinfo, positions=traceback_info.positions))
16911737
frame = frame.f_back
16921738
return framelist
16931739

@@ -1698,8 +1744,9 @@ def getinnerframes(tb, context=1):
16981744
name, a list of lines of context, and index within the context."""
16991745
framelist = []
17001746
while tb:
1701-
frameinfo = (tb.tb_frame,) + getframeinfo(tb, context)
1702-
framelist.append(FrameInfo(*frameinfo))
1747+
traceback_info = getframeinfo(tb, context)
1748+
frameinfo = (tb.tb_frame,) + traceback_info
1749+
framelist.append(FrameInfo(*frameinfo, positions=traceback_info.positions))
17031750
tb = tb.tb_next
17041751
return framelist
17051752

Lib/test/test_inspect.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io
88
import linecache
99
import os
10+
import dis
1011
from os.path import normcase
1112
import _pickle
1213
import pickle
@@ -361,14 +362,23 @@ def test_abuse_done(self):
361362

362363
def test_stack(self):
363364
self.assertTrue(len(mod.st) >= 5)
364-
self.assertEqual(revise(*mod.st[0][1:]),
365+
frame1, frame2, frame3, frame4, *_ = mod.st
366+
frameinfo = revise(*frame1[1:])
367+
self.assertEqual(frameinfo,
365368
(modfile, 16, 'eggs', [' st = inspect.stack()\n'], 0))
366-
self.assertEqual(revise(*mod.st[1][1:]),
369+
self.assertEqual(frame1.positions, dis.Positions(16, 16, 9, 24))
370+
frameinfo = revise(*frame2[1:])
371+
self.assertEqual(frameinfo,
367372
(modfile, 9, 'spam', [' eggs(b + d, c + f)\n'], 0))
368-
self.assertEqual(revise(*mod.st[2][1:]),
373+
self.assertEqual(frame2.positions, dis.Positions(9, 9, 4, 22))
374+
frameinfo = revise(*frame3[1:])
375+
self.assertEqual(frameinfo,
369376
(modfile, 43, 'argue', [' spam(a, b, c)\n'], 0))
370-
self.assertEqual(revise(*mod.st[3][1:]),
377+
self.assertEqual(frame3.positions, dis.Positions(43, 43, 12, 25))
378+
frameinfo = revise(*frame4[1:])
379+
self.assertEqual(frameinfo,
371380
(modfile, 39, 'abuse', [' self.argue(a, b, c)\n'], 0))
381+
self.assertEqual(frame4.positions, dis.Positions(39, 39, 8, 27))
372382
# Test named tuple fields
373383
record = mod.st[0]
374384
self.assertIs(record.frame, mod.fr)
@@ -380,12 +390,16 @@ def test_stack(self):
380390

381391
def test_trace(self):
382392
self.assertEqual(len(git.tr), 3)
383-
self.assertEqual(revise(*git.tr[0][1:]),
393+
frame1, frame2, frame3, = git.tr
394+
self.assertEqual(revise(*frame1[1:]),
384395
(modfile, 43, 'argue', [' spam(a, b, c)\n'], 0))
385-
self.assertEqual(revise(*git.tr[1][1:]),
396+
self.assertEqual(frame1.positions, dis.Positions(43, 43, 12, 25))
397+
self.assertEqual(revise(*frame2[1:]),
386398
(modfile, 9, 'spam', [' eggs(b + d, c + f)\n'], 0))
387-
self.assertEqual(revise(*git.tr[2][1:]),
399+
self.assertEqual(frame2.positions, dis.Positions(9, 9, 4, 22))
400+
self.assertEqual(revise(*frame3[1:]),
388401
(modfile, 18, 'eggs', [' q = y / 0\n'], 0))
402+
self.assertEqual(frame3.positions, dis.Positions(18, 18, 8, 13))
389403

390404
def test_frame(self):
391405
args, varargs, varkw, locals = inspect.getargvalues(mod.fr)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Change the frame-related functions in the :mod:`inspect` module to return a
2+
regular object (that is backwards compatible with the old tuple-like interface)
3+
that include the extended :pep:`657` position information (end line number,
4+
column and end column). The affected functions are: :func:`inspect.getframeinfo`,
5+
:func:`inspect.getouterframes`, :func:`inspect.getinnerframes`, :func:`inspect.stack` and
6+
:func:`inspect.trace`. Patch by Pablo Galindo.
7+
8+

0 commit comments

Comments
 (0)