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

View file

@ -326,6 +326,14 @@ inspect
* Add :func:`inspect.ismethodwrapper` for checking if the type of an object is a * Add :func:`inspect.ismethodwrapper` for checking if the type of an object is a
:class:`~types.MethodWrapperType`. (Contributed by Hakan Çelik in :issue:`29418`.) :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 locale
------ ------

View file

@ -1638,7 +1638,30 @@ def getclosurevars(func):
# -------------------------------------------------- stack frame extraction # -------------------------------------------------- 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): def getframeinfo(frame, context=1):
"""Get information about a frame or traceback object. """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 The optional second argument specifies the number of lines of context
to return, which are centered around the current line.""" to return, which are centered around the current line."""
if istraceback(frame): if istraceback(frame):
positions = _get_code_position_from_tb(frame)
lineno = frame.tb_lineno lineno = frame.tb_lineno
frame = frame.tb_frame frame = frame.tb_frame
else: else:
lineno = frame.f_lineno 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): if not isframe(frame):
raise TypeError('{!r} is not a frame or traceback object'.format(frame)) raise TypeError('{!r} is not a frame or traceback object'.format(frame))
@ -1670,14 +1703,26 @@ def getframeinfo(frame, context=1):
else: else:
lines = index = None 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): def getlineno(frame):
"""Get the line number from a frame object, allowing for optimization.""" """Get the line number from a frame object, allowing for optimization."""
# FrameType.f_lineno is now a descriptor that grovels co_lnotab # FrameType.f_lineno is now a descriptor that grovels co_lnotab
return frame.f_lineno 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): def getouterframes(frame, context=1):
"""Get a list of records for a frame and all higher (calling) frames. """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.""" name, a list of lines of context, and index within the context."""
framelist = [] framelist = []
while frame: while frame:
frameinfo = (frame,) + getframeinfo(frame, context) traceback_info = getframeinfo(frame, context)
framelist.append(FrameInfo(*frameinfo)) frameinfo = (frame,) + traceback_info
framelist.append(FrameInfo(*frameinfo, positions=traceback_info.positions))
frame = frame.f_back frame = frame.f_back
return framelist return framelist
@ -1698,8 +1744,9 @@ def getinnerframes(tb, context=1):
name, a list of lines of context, and index within the context.""" name, a list of lines of context, and index within the context."""
framelist = [] framelist = []
while tb: while tb:
frameinfo = (tb.tb_frame,) + getframeinfo(tb, context) traceback_info = getframeinfo(tb, context)
framelist.append(FrameInfo(*frameinfo)) frameinfo = (tb.tb_frame,) + traceback_info
framelist.append(FrameInfo(*frameinfo, positions=traceback_info.positions))
tb = tb.tb_next tb = tb.tb_next
return framelist return framelist

View file

@ -7,6 +7,7 @@ import inspect
import io import io
import linecache import linecache
import os import os
import dis
from os.path import normcase from os.path import normcase
import _pickle import _pickle
import pickle import pickle
@ -361,14 +362,23 @@ class TestInterpreterStack(IsTestBase):
def test_stack(self): def test_stack(self):
self.assertTrue(len(mod.st) >= 5) 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)) (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)) (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)) (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)) (modfile, 39, 'abuse', [' self.argue(a, b, c)\n'], 0))
self.assertEqual(frame4.positions, dis.Positions(39, 39, 8, 27))
# Test named tuple fields # Test named tuple fields
record = mod.st[0] record = mod.st[0]
self.assertIs(record.frame, mod.fr) self.assertIs(record.frame, mod.fr)
@ -380,12 +390,16 @@ class TestInterpreterStack(IsTestBase):
def test_trace(self): def test_trace(self):
self.assertEqual(len(git.tr), 3) 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)) (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)) (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)) (modfile, 18, 'eggs', [' q = y / 0\n'], 0))
self.assertEqual(frame3.positions, dis.Positions(18, 18, 8, 13))
def test_frame(self): def test_frame(self):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr) 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.