From 40fcb4eb377fa0a9c687b93facc88d3dfe86970a Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Thu, 14 Apr 2022 13:08:32 +0100 Subject: [PATCH] gh-88166: Enhance the inspect frame APIs to use the extended position information --- Doc/library/inspect.rst | 91 +++++++++++++++++-- Doc/whatsnew/3.11.rst | 8 ++ Lib/inspect.py | 59 ++++++++++-- Lib/test/test_inspect.py | 27 ++++-- ...2-04-14-13-11-37.gh-issue-88116.j_SybE.rst | 8 ++ 5 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-04-14-13-11-37.gh-issue-88116.j_SybE.rst diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 8ee2c070cccf59..e5558afe03d1b4 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -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 @@ -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) @@ -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) @@ -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() @@ -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) @@ -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 ------------------------------ diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 894ec8a9d0d928..63821f2c770b1c 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -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 ------ diff --git a/Lib/inspect.py b/Lib/inspect.py index 9c1283ab3734bf..58b40cec50c3dd 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -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. @@ -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)) @@ -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. @@ -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 @@ -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 diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 9e3c77056d70a0..002434cc36d030 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -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) @@ -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) 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 00000000000000..1d0acc481b064f --- /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:`getframeinfo`, +:func:`getouterframes`, :func:`getinnerframes`, :func:`stack` and +:func:`trace`. Patch by Pablo Galindo + +