Skip to content

Commit

Permalink
pythongh-88166: Enhance the inspect frame APIs to use the extended po…
Browse files Browse the repository at this point in the history
…sition information
  • Loading branch information
pablogsal committed Apr 18, 2022
1 parent 0fc3517 commit 40fcb4e
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 22 deletions.
91 changes: 83 additions & 8 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1164,16 +1164,81 @@ 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.
:class:`FrameInfo` object. 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 frame object 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 ``code_context`` list.

.. attribute:: positions

A tuple 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 namedtuple 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 ``code_context`` list.

.. attribute:: positions

A tuple 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
Expand Down Expand Up @@ -1207,9 +1272,11 @@ 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 inspead of a named tuple.

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

Expand All @@ -1223,6 +1290,8 @@ line.
``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)

Expand All @@ -1236,6 +1305,8 @@ line.
``FrameInfo(frame, filename, lineno, function, code_context, index)``
is returned.

.. versionchanged:: 3.11
A list of :class:`FrameInfo` objects is returned.

.. function:: currentframe()

Expand All @@ -1260,6 +1331,8 @@ line.
``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)

Expand All @@ -1273,6 +1346,8 @@ line.
``FrameInfo(frame, filename, lineno, function, code_context, index)``
is returned.

.. versionchanged:: 3.11
A list of :class:`FrameInfo` objects is returned.

Fetching attributes statically
------------------------------
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,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:`getframeinfo`, :func:`getouterframes`, :func:`getinnerframes`,
:func:`stack` and :func:`trace`. (Contributed by Pablo Galindo in
:issue:`88116`)

locale
------

Expand Down
59 changes: 52 additions & 7 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1638,7 +1638,29 @@ 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()
return next(itertools.islice(positions_gen, instruction_index // 2, None))

def getframeinfo(frame, context=1):
"""Get information about a frame or traceback object.
Expand All @@ -1649,10 +1671,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

if not isframe(frame):
raise TypeError('{!r} is not a frame or traceback object'.format(frame))

Expand All @@ -1670,14 +1702,25 @@ 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, tuple(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.
Expand All @@ -1686,8 +1729,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

Expand All @@ -1698,8 +1742,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

Expand Down
27 changes: 20 additions & 7 deletions Lib/test/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,23 @@ def test_abuse_done(self):

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, (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, (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, (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, (39, 39, 8, 27))
# Test named tuple fields
record = mod.st[0]
self.assertIs(record.frame, mod.fr)
Expand All @@ -380,12 +389,16 @@ def test_stack(self):

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, (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, (9, 9, 4, 22))
self.assertEqual(revise(*frame3[1:]),
(modfile, 18, 'eggs', [' q = y / 0\n'], 0))
self.assertEqual(frame3.positions, (18, 18, 8, 13))

def test_frame(self):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr)
Expand Down
Original file line number Diff line number Diff line change
@@ -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:`getframeinfo`,
:func:`getouterframes`, :func:`getinnerframes`, :func:`stack` and
:func:`trace`. Patch by Pablo Galindo


0 comments on commit 40fcb4e

Please sign in to comment.