From f33497b9d72a363107cc88c195d5e19f5cc077ac Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 22 Mar 2022 10:07:27 +0000 Subject: [PATCH 1/2] handle frame.f_lineno is None in inspect.getframeinfo --- Lib/inspect.py | 31 +++++++++++++++++-- .../2022-03-22-10-16-11.bpo-45563.o-oP2m.rst | 1 + 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-03-22-10-16-11.bpo-45563.o-oP2m.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index c52469e63861a22..fa8731747830d64 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1609,10 +1609,10 @@ def getframeinfo(frame, context=1): to return, which are centered around the current line.""" if istraceback(frame): positions = _get_code_position_from_tb(frame) - lineno = frame.tb_lineno + lineno = _tblineno(frame) frame = frame.tb_frame else: - lineno = frame.f_lineno + lineno = getlineno(frame) positions = _get_code_position(frame.f_code, frame.f_lasti) if positions[0] is None: @@ -1642,9 +1642,34 @@ def getframeinfo(frame, context=1): return Traceback(filename, lineno, frame.f_code.co_name, lines, index, positions=dis.Positions(*positions)) + +def _tblineno(tb): + tb_lineno = tb.tb_lineno + if tb_lineno is not None: + return tb_lineno + + return _lasti2lineno(tb.tb_frame.f_code, tb.tb_lasti) + + +def _lasti2lineno(code, lasti): + prev_line = code.co_firstlineno + + for start, next_line in dis.findlinestarts(code): + if lasti < start: + return prev_line + prev_line = next_line + + return prev_line + def getlineno(frame): """Get the line number from a frame object, allowing for optimization.""" - return frame.f_lineno + # FrameType.f_lineno is now a descriptor that often grovels co_lnotab + f_lineno = frame.f_lineno + if f_lineno is not None: + return f_lineno + + return _lasti2lineno(frame.f_code, frame.f_lasti) + _FrameInfo = namedtuple('_FrameInfo', ('frame',) + Traceback._fields) class FrameInfo(_FrameInfo): diff --git a/Misc/NEWS.d/next/Library/2022-03-22-10-16-11.bpo-45563.o-oP2m.rst b/Misc/NEWS.d/next/Library/2022-03-22-10-16-11.bpo-45563.o-oP2m.rst new file mode 100644 index 000000000000000..d4e0076be67e11f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-03-22-10-16-11.bpo-45563.o-oP2m.rst @@ -0,0 +1 @@ +handle frame.f_lineno is None in inspect.getframeinfo From 5a78fd52c0ed53029023da94edf70ea56757bef1 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Wed, 2 Sep 2026 10:22:16 +0100 Subject: [PATCH 2/2] Report a missing line number instead of reconstructing one Per review, do not try to recover a line number for a frame stopped on an instruction that carries no line: just skip the source-context lookup that assumed lineno was an int, and let getframeinfo() report lineno, code_context and index as None. The previous approach did not work anyway. _lasti2lineno() walked dis.findlinestarts(), which since 3.11 yields None line numbers of its own, so it returned None for exactly the frames it was meant to fix. Add a regression test that reproduces the original TypeError. --- Lib/inspect.py | 33 +++---------------- Lib/test/test_inspect/test_inspect.py | 33 +++++++++++++++++++ .../2022-03-22-10-16-11.bpo-45563.o-oP2m.rst | 5 ++- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index fa8731747830d64..45b7e47f05da7a1 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1609,10 +1609,10 @@ def getframeinfo(frame, context=1): to return, which are centered around the current line.""" if istraceback(frame): positions = _get_code_position_from_tb(frame) - lineno = _tblineno(frame) + lineno = frame.tb_lineno frame = frame.tb_frame else: - lineno = getlineno(frame) + lineno = frame.f_lineno positions = _get_code_position(frame.f_code, frame.f_lasti) if positions[0] is None: @@ -1626,7 +1626,7 @@ def getframeinfo(frame, context=1): raise TypeError('{!r} is not a frame or traceback object'.format(frame)) filename = getsourcefile(frame) or getfile(frame) - if context > 0: + if context > 0 and lineno is not None: start = lineno - 1 - context//2 try: lines, lnum = findsource(frame) @@ -1642,34 +1642,9 @@ def getframeinfo(frame, context=1): return Traceback(filename, lineno, frame.f_code.co_name, lines, index, positions=dis.Positions(*positions)) - -def _tblineno(tb): - tb_lineno = tb.tb_lineno - if tb_lineno is not None: - return tb_lineno - - return _lasti2lineno(tb.tb_frame.f_code, tb.tb_lasti) - - -def _lasti2lineno(code, lasti): - prev_line = code.co_firstlineno - - for start, next_line in dis.findlinestarts(code): - if lasti < start: - return prev_line - prev_line = next_line - - return prev_line - def getlineno(frame): """Get the line number from a frame object, allowing for optimization.""" - # FrameType.f_lineno is now a descriptor that often grovels co_lnotab - f_lineno = frame.f_lineno - if f_lineno is not None: - return f_lineno - - return _lasti2lineno(frame.f_code, frame.f_lasti) - + return frame.f_lineno _FrameInfo = namedtuple('_FrameInfo', ('frame',) + Traceback._fields) class FrameInfo(_FrameInfo): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index df5843abfcb8753..e8f5d949a35fa72 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -846,6 +846,39 @@ def test_getframeinfo_get_first_line(self): self.assertEqual(frame_info.code_context[0], "# line 1\n") self.assertEqual(frame_info.code_context[1], "'A module docstring.'\n") + def test_getframeinfo_no_lineno(self): + # gh-89726: some instructions carry no line number, so a frame + # stopped on one has f_lineno set to None. getframeinfo() must + # report that rather than failing. + def f(): + try: + raise ValueError + except ValueError: + pass + + infos = [] + + def trace(frame, event, arg): + if frame.f_code is f.__code__ and frame.f_lineno is None: + infos.append(inspect.getframeinfo(frame)) + frame.f_trace_opcodes = True + return trace + + old_trace = sys.gettrace() + sys.settrace(trace) + try: + f() + finally: + sys.settrace(old_trace) + + self.assertTrue(infos, 'no frame with f_lineno set to None') + for info in infos: + with self.subTest(info=info): + self.assertIsNone(info.lineno) + self.assertIsNone(info.code_context) + self.assertIsNone(info.index) + self.assertEqual(info.function, 'f') + def test_getsource(self): self.assertSourceEqual(git.abuse, 29, 39) self.assertSourceEqual(mod.StupidGit, 21, 51) diff --git a/Misc/NEWS.d/next/Library/2022-03-22-10-16-11.bpo-45563.o-oP2m.rst b/Misc/NEWS.d/next/Library/2022-03-22-10-16-11.bpo-45563.o-oP2m.rst index d4e0076be67e11f..aa32e5e1e76028d 100644 --- a/Misc/NEWS.d/next/Library/2022-03-22-10-16-11.bpo-45563.o-oP2m.rst +++ b/Misc/NEWS.d/next/Library/2022-03-22-10-16-11.bpo-45563.o-oP2m.rst @@ -1 +1,4 @@ -handle frame.f_lineno is None in inspect.getframeinfo +:func:`inspect.getframeinfo` no longer raises :exc:`TypeError` for a frame +stopped on an instruction that carries no line number, such as while tracing +opcodes. ``lineno``, ``code_context`` and ``index`` are reported as ``None`` +instead.