gh-88116: Enhance the inspect frame APIs to use the extended position information (GH-91531)

This commit is contained in:
Pablo Galindo Salgado 2022-04-23 03:16:48 +01:00 committed by GitHub
parent a3f2cf3ced
commit 0daa99f68b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 193 additions and 38 deletions

View File

@ -1163,17 +1163,85 @@ Classes and functions
The interpreter stack
---------------------
When the following functions return "frame records," each record is a
:term:`named tuple`
``FrameInfo(frame, filename, lineno, function, code_context, index)``.
The tuple contains the frame object, the filename, the line number of the
current line,
the function name, a list of lines of context from the source code, and the
index of the current line within that list.
Some of the following functions return
:class:`FrameInfo` objects. For backwards compatibility these objects allow
tuple-like operations on all attributes except ``positions``. This behavior
is considered deprecated and may be removed in the future.
.. class:: FrameInfo
.. attribute:: frame
The :ref:`frame object <frame-objects>` that the record corresponds to.
.. attribute:: filename
The file name associated with the code being executed by the frame this record
corresponds to.
.. attribute:: lineno
The line number of the current line associated with the code being
executed by the frame this record corresponds to.
.. attribute:: function
The function name that is being executed by the frame this record corresponds to.
.. attribute:: code_context
A list of lines of context from the source code that's being executed by the frame
this record corresponds to.
.. attribute:: index
The index of the current line being executed in the :attr:`code_context` list.
.. attribute:: positions
A :class:`dis.Positions` object containing the start line number, end line
number, start column offset, and end column offset associated with the
instruction being executed by the frame this record corresponds to.
.. versionchanged:: 3.5
Return a named tuple instead of a tuple.
.. versionchanged:: 3.11
Changed the return object from a named tuple to a regular object (that is
backwards compatible with the previous named tuple).
.. class:: Traceback
.. attribute:: filename
The file name associated with the code being executed by the frame this traceback
corresponds to.
.. attribute:: lineno
The line number of the current line associated with the code being
executed by the frame this traceback corresponds to.
.. attribute:: function
The function name that is being executed by the frame this traceback corresponds to.
.. attribute:: code_context
A list of lines of context from the source code that's being executed by the frame
this traceback corresponds to.
.. attribute:: index
The index of the current line being executed in the :attr:`code_context` list.
.. attribute:: positions
A :class:`dis.Positions` object containing the start line number, end
line number, start column offset, and end column offset associated with
the instruction being executed by the frame this traceback corresponds
to.
.. note::
Keeping references to frame objects, as found in the first element of the frame
@ -1207,35 +1275,41 @@ line.
.. function:: getframeinfo(frame, context=1)
Get information about a frame or traceback object. A :term:`named tuple`
``Traceback(filename, lineno, function, code_context, index)`` is returned.
Get information about a frame or traceback object. A :class:`Traceback` object
is returned.
.. versionchanged:: 3.11
A :class:`Traceback` object is returned instead of a named tuple.
.. function:: getouterframes(frame, context=1)
Get a list of frame records for a frame and all outer frames. These frames
represent the calls that lead to the creation of *frame*. The first entry in the
returned list represents *frame*; the last entry represents the outermost call
on *frame*'s stack.
Get a list of :class:`FrameInfo` objects for a frame and all outer frames.
These frames represent the calls that lead to the creation of *frame*. The
first entry in the returned list represents *frame*; the last entry
represents the outermost call on *frame*'s stack.
.. versionchanged:: 3.5
A list of :term:`named tuples <named tuple>`
``FrameInfo(frame, filename, lineno, function, code_context, index)``
is returned.
.. versionchanged:: 3.11
A list of :class:`FrameInfo` objects is returned.
.. function:: getinnerframes(traceback, context=1)
Get a list of frame records for a traceback's frame and all inner frames. These
frames represent calls made as a consequence of *frame*. The first entry in the
list represents *traceback*; the last entry represents where the exception was
raised.
Get a list of :class:`FrameInfo` objects for a traceback's frame and all
inner frames. These frames represent calls made as a consequence of *frame*.
The first entry in the list represents *traceback*; the last entry represents
where the exception was raised.
.. versionchanged:: 3.5
A list of :term:`named tuples <named tuple>`
``FrameInfo(frame, filename, lineno, function, code_context, index)``
is returned.
.. versionchanged:: 3.11
A list of :class:`FrameInfo` objects is returned.
.. function:: currentframe()
@ -1251,28 +1325,32 @@ line.
.. function:: stack(context=1)
Return a list of frame records for the caller's stack. The first entry in the
returned list represents the caller; the last entry represents the outermost
call on the stack.
Return a list of :class:`FrameInfo` objects for the caller's stack. The
first entry in the returned list represents the caller; the last entry
represents the outermost call on the stack.
.. versionchanged:: 3.5
A list of :term:`named tuples <named tuple>`
``FrameInfo(frame, filename, lineno, function, code_context, index)``
is returned.
.. versionchanged:: 3.11
A list of :class:`FrameInfo` objects is returned.
.. function:: trace(context=1)
Return a list of frame records for the stack between the current frame and the
frame in which an exception currently being handled was raised in. The first
entry in the list represents the caller; the last entry represents where the
exception was raised.
Return a list of :class:`FrameInfo` objects for the stack between the current
frame and the frame in which an exception currently being handled was raised
in. The first entry in the list represents the caller; the last entry
represents where the exception was raised.
.. versionchanged:: 3.5
A list of :term:`named tuples <named tuple>`
``FrameInfo(frame, filename, lineno, function, code_context, index)``
is returned.
.. versionchanged:: 3.11
A list of :class:`FrameInfo` objects is returned.
Fetching attributes statically
------------------------------

View File

@ -326,6 +326,14 @@ inspect
* Add :func:`inspect.ismethodwrapper` for checking if the type of an object is a
:class:`~types.MethodWrapperType`. (Contributed by Hakan Çelik in :issue:`29418`.)
* Change the frame-related functions in the :mod:`inspect` module to return a
regular object (that is backwards compatible with the old tuple-like
interface) that include the extended :pep:`657` position information (end
line number, column and end column). The affected functions are:
:func:`inspect.getframeinfo`, :func:`inspect.getouterframes`, :func:`inspect.getinnerframes`,
:func:`inspect.stack` and :func:`inspect.trace`. (Contributed by Pablo Galindo in
:issue:`88116`)
locale
------

View File

@ -1638,7 +1638,30 @@ def getclosurevars(func):
# -------------------------------------------------- stack frame extraction
Traceback = namedtuple('Traceback', 'filename lineno function code_context index')
_Traceback = namedtuple('_Traceback', 'filename lineno function code_context index')
class Traceback(_Traceback):
def __new__(cls, filename, lineno, function, code_context, index, *, positions=None):
instance = super().__new__(cls, filename, lineno, function, code_context, index)
instance.positions = positions
return instance
def __repr__(self):
return ('Traceback(filename={!r}, lineno={!r}, function={!r}, '
'code_context={!r}, index={!r}, positions={!r})'.format(
self.filename, self.lineno, self.function, self.code_context,
self.index, self.positions))
def _get_code_position_from_tb(tb):
code, instruction_index = tb.tb_frame.f_code, tb.tb_lasti
return _get_code_position(code, instruction_index)
def _get_code_position(code, instruction_index):
if instruction_index < 0:
return (None, None, None, None)
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))
def getframeinfo(frame, context=1):
"""Get information about a frame or traceback object.
@ -1649,10 +1672,20 @@ def getframeinfo(frame, context=1):
The optional second argument specifies the number of lines of context
to return, which are centered around the current line."""
if istraceback(frame):
positions = _get_code_position_from_tb(frame)
lineno = frame.tb_lineno
frame = frame.tb_frame
else:
lineno = frame.f_lineno
positions = _get_code_position(frame.f_code, frame.f_lasti)
if positions[0] is None:
frame, *positions = (frame, lineno, *positions[1:])
else:
frame, *positions = (frame, *positions)
lineno = positions[0]
if not isframe(frame):
raise TypeError('{!r} is not a frame or traceback object'.format(frame))
@ -1670,14 +1703,26 @@ def getframeinfo(frame, context=1):
else:
lines = index = None
return Traceback(filename, lineno, frame.f_code.co_name, lines, index)
return Traceback(filename, lineno, frame.f_code.co_name, lines,
index, positions=dis.Positions(*positions))
def getlineno(frame):
"""Get the line number from a frame object, allowing for optimization."""
# FrameType.f_lineno is now a descriptor that grovels co_lnotab
return frame.f_lineno
FrameInfo = namedtuple('FrameInfo', ('frame',) + Traceback._fields)
_FrameInfo = namedtuple('_FrameInfo', ('frame',) + Traceback._fields)
class FrameInfo(_FrameInfo):
def __new__(cls, frame, filename, lineno, function, code_context, index, *, positions=None):
instance = super().__new__(cls, frame, filename, lineno, function, code_context, index)
instance.positions = positions
return instance
def __repr__(self):
return ('FrameInfo(frame={!r}, filename={!r}, lineno={!r}, function={!r}, '
'code_context={!r}, index={!r}, positions={!r})'.format(
self.frame, self.filename, self.lineno, self.function,
self.code_context, self.index, self.positions))
def getouterframes(frame, context=1):
"""Get a list of records for a frame and all higher (calling) frames.
@ -1686,8 +1731,9 @@ def getouterframes(frame, context=1):
name, a list of lines of context, and index within the context."""
framelist = []
while frame:
frameinfo = (frame,) + getframeinfo(frame, context)
framelist.append(FrameInfo(*frameinfo))
traceback_info = getframeinfo(frame, context)
frameinfo = (frame,) + traceback_info
framelist.append(FrameInfo(*frameinfo, positions=traceback_info.positions))
frame = frame.f_back
return framelist
@ -1698,8 +1744,9 @@ def getinnerframes(tb, context=1):
name, a list of lines of context, and index within the context."""
framelist = []
while tb:
frameinfo = (tb.tb_frame,) + getframeinfo(tb, context)
framelist.append(FrameInfo(*frameinfo))
traceback_info = getframeinfo(tb, context)
frameinfo = (tb.tb_frame,) + traceback_info
framelist.append(FrameInfo(*frameinfo, positions=traceback_info.positions))
tb = tb.tb_next
return framelist

View File

@ -7,6 +7,7 @@ import inspect
import io
import linecache
import os
import dis
from os.path import normcase
import _pickle
import pickle
@ -361,14 +362,23 @@ class TestInterpreterStack(IsTestBase):
def test_stack(self):
self.assertTrue(len(mod.st) >= 5)
self.assertEqual(revise(*mod.st[0][1:]),
frame1, frame2, frame3, frame4, *_ = mod.st
frameinfo = revise(*frame1[1:])
self.assertEqual(frameinfo,
(modfile, 16, 'eggs', [' st = inspect.stack()\n'], 0))
self.assertEqual(revise(*mod.st[1][1:]),
self.assertEqual(frame1.positions, dis.Positions(16, 16, 9, 24))
frameinfo = revise(*frame2[1:])
self.assertEqual(frameinfo,
(modfile, 9, 'spam', [' eggs(b + d, c + f)\n'], 0))
self.assertEqual(revise(*mod.st[2][1:]),
self.assertEqual(frame2.positions, dis.Positions(9, 9, 4, 22))
frameinfo = revise(*frame3[1:])
self.assertEqual(frameinfo,
(modfile, 43, 'argue', [' spam(a, b, c)\n'], 0))
self.assertEqual(revise(*mod.st[3][1:]),
self.assertEqual(frame3.positions, dis.Positions(43, 43, 12, 25))
frameinfo = revise(*frame4[1:])
self.assertEqual(frameinfo,
(modfile, 39, 'abuse', [' self.argue(a, b, c)\n'], 0))
self.assertEqual(frame4.positions, dis.Positions(39, 39, 8, 27))
# Test named tuple fields
record = mod.st[0]
self.assertIs(record.frame, mod.fr)
@ -380,12 +390,16 @@ class TestInterpreterStack(IsTestBase):
def test_trace(self):
self.assertEqual(len(git.tr), 3)
self.assertEqual(revise(*git.tr[0][1:]),
frame1, frame2, frame3, = git.tr
self.assertEqual(revise(*frame1[1:]),
(modfile, 43, 'argue', [' spam(a, b, c)\n'], 0))
self.assertEqual(revise(*git.tr[1][1:]),
self.assertEqual(frame1.positions, dis.Positions(43, 43, 12, 25))
self.assertEqual(revise(*frame2[1:]),
(modfile, 9, 'spam', [' eggs(b + d, c + f)\n'], 0))
self.assertEqual(revise(*git.tr[2][1:]),
self.assertEqual(frame2.positions, dis.Positions(9, 9, 4, 22))
self.assertEqual(revise(*frame3[1:]),
(modfile, 18, 'eggs', [' q = y / 0\n'], 0))
self.assertEqual(frame3.positions, dis.Positions(18, 18, 8, 13))
def test_frame(self):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr)

View File

@ -0,0 +1,8 @@
Change the frame-related functions in the :mod:`inspect` module to return a
regular object (that is backwards compatible with the old tuple-like interface)
that include the extended :pep:`657` position information (end line number,
column and end column). The affected functions are: :func:`inspect.getframeinfo`,
:func:`inspect.getouterframes`, :func:`inspect.getinnerframes`, :func:`inspect.stack` and
:func:`inspect.trace`. Patch by Pablo Galindo.