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

pablogsal webhook-mailer at python.org
Fri Apr 22 22:17:16 EDT 2022


https://github.com/python/cpython/commit/0daa99f68b7b9f02b37a2f34508f33ae66d95fc4
commit: 0daa99f68b7b9f02b37a2f34508f33ae66d95fc4
branch: main
author: Pablo Galindo Salgado <Pablogsal at gmail.com>
committer: pablogsal <Pablogsal at gmail.com>
date: 2022-04-23T03:16:48+01:00
summary:

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

files:
A Misc/NEWS.d/next/Library/2022-04-14-13-11-37.gh-issue-88116.j_SybE.rst
M Doc/library/inspect.rst
M Doc/whatsnew/3.11.rst
M Lib/inspect.py
M Lib/test/test_inspect.py

diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 8ee2c070cccf5..575b3088900e1 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -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
 ------------------------------
diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst
index 653d32aaf540b..5b53fbe0dda6f 100644
--- a/Doc/whatsnew/3.11.rst
+++ b/Doc/whatsnew/3.11.rst
@@ -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
 ------
 
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 9c1283ab3734b..5bc9c04b22e23 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -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
 
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index 9e3c77056d70a..115e97b77e079 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -7,6 +7,7 @@
 import io
 import linecache
 import os
+import dis
 from os.path import normcase
 import _pickle
 import pickle
@@ -361,14 +362,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, 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 @@ 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, 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)
diff --git a/Misc/NEWS.d/next/Library/2022-04-14-13-11-37.gh-issue-88116.j_SybE.rst b/Misc/NEWS.d/next/Library/2022-04-14-13-11-37.gh-issue-88116.j_SybE.rst
new file mode 100644
index 0000000000000..1b7bd6929e959
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-04-14-13-11-37.gh-issue-88116.j_SybE.rst
@@ -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.
+
+



More information about the Python-checkins mailing list