From 17ba1654ba65ea1c280c5f8cd471b337ac50da68 Mon Sep 17 00:00:00 2001 From: drakeo338 Date: Thu, 1 Oct 2026 09:52:16 +0000 Subject: [PATCH 1/2] gh-158556: Fix inspect.getsource() for a lambda after a multiline string getblock() tokenized lines starting in the middle of a multiline string, so the closing delimiter was read as an opening one and tokenize raised TokenError. --- Lib/inspect.py | 4 ++++ Lib/test/test_inspect/test_inspect.py | 15 +++++++++++++++ ...2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 3683f8c3fd53308..88bffbc7f37a568 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1154,6 +1154,10 @@ def getblock(lines): blockfinder.tokeneater(*_token) except (EndOfBlock, IndentationError): pass + except tokenize.TokenError: + # The lines may start in the middle of a multiline string. + if blockfinder.started: + raise except SyntaxError as e: if "unmatched" not in e.msg: raise e from None diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 38c7a9bde823d9d..3806a7292d7ae1c 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1120,6 +1120,21 @@ def test_dataclass(self): self.assertRaises(OSError, inspect.getsource, mod2.dc370) self.assertRaises(OSError, inspect.getsource, mod2.dc371) + def test_lambda_after_multiline_string_end(self): + # gh-158556: a lambda on the same line as the closing delimiter of a + # multiline string must not make getblock() fail while tokenizing. + src = textwrap.dedent('''\ + pair = ( + """text + """, lambda: None + ) + ''') + with ready_to_import('lambda_after_string', src) as (name, path): + module = import_helper.import_module(name) + lines, lnum = inspect.getsourcelines(module.pair[1]) + self.assertEqual(lines, ['""", lambda: None\n']) + self.assertEqual(lnum, 3) + class TestBlockComments(GetSourceBase): fodderModule = mod diff --git a/Misc/NEWS.d/next/Library/2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst b/Misc/NEWS.d/next/Library/2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst new file mode 100644 index 000000000000000..d7d601b135510a2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst @@ -0,0 +1,2 @@ +Fix :func:`inspect.getsource` and :func:`inspect.getsourcelines` for a lambda +that is on the same line as the closing delimiter of a multiline string. From 4b785ddfb299d14ac30e45e55dc88c6f8eee0173 Mon Sep 17 00:00:00 2001 From: drakeo338 Date: Thu, 1 Oct 2026 10:00:07 +0000 Subject: [PATCH 2/2] gh-158556: Start inspect.getsourcelines() at the beginning of a multiline string A lambda or generator expression that begins on the line where a multiline string ends was tokenized starting in the middle of that string, so the closing delimiter was read as an opening one. getblock() either raised tokenize.TokenError or returned a block that was too long. Start the block on the line where the string begins instead. --- Lib/inspect.py | 40 +++++++++++++++++-- Lib/test/test_inspect/inspect_fodder2.py | 26 ++++++++++++ Lib/test/test_inspect/test_inspect.py | 14 ++++++- ...-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst | 3 +- 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 88bffbc7f37a568..c812a5522814306 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1154,10 +1154,6 @@ def getblock(lines): blockfinder.tokeneater(*_token) except (EndOfBlock, IndentationError): pass - except tokenize.TokenError: - # The lines may start in the middle of a multiline string. - if blockfinder.started: - raise except SyntaxError as e: if "unmatched" not in e.msg: raise e from None @@ -1168,6 +1164,31 @@ def getblock(lines): pass return lines[:blockfinder.last] +def _find_string_start(lines, lnum): + """Return the index of the line where lines[lnum] begins, or the index + of the line where a multiline string that is still open at the start of + lines[lnum] begins. + """ + # A line that starts inside a string must contain the closing quote. + if '"' not in lines[lnum] and "'" not in lines[lnum]: + return lnum + starts = [] + try: + for tok in tokenize.generate_tokens(iter(lines[:lnum + 1]).__next__): + if tok.start[0] > lnum: + break + if tok.type in (tokenize.FSTRING_START, tokenize.TSTRING_START): + starts.append(tok.start[0] - 1) + elif tok.type in (tokenize.FSTRING_END, tokenize.TSTRING_END): + starts.pop() + elif (tok.end[0] > lnum and + tok.type in (tokenize.STRING, tokenize.FSTRING_MIDDLE, + tokenize.TSTRING_MIDDLE)): + return starts[0] if starts else tok.start[0] - 1 + except (tokenize.TokenError, SyntaxError): + pass + return lnum + def getsourcelines(object): """Return a list of source lines and starting line number for an object. @@ -1187,6 +1208,17 @@ def getsourcelines(object): (isframe(object) and object.f_code.co_name == "")): return lines, 0 else: + # A lambda or generator expression may start in the middle of a + # multiline string, which cannot be tokenized without the line + # where the string begins. + if isframe(object): + object = object.f_code + elif ismethod(object): + object = object.__func__ + if isfunction(object): + object = object.__code__ + if iscode(object) and object.co_name.startswith('<'): + lnum = _find_string_start(lines, lnum) return getblock(lines[lnum:]), lnum + 1 def getsource(object): diff --git a/Lib/test/test_inspect/inspect_fodder2.py b/Lib/test/test_inspect/inspect_fodder2.py index 157e12167b5d270..1fcd8ed03d589fe 100644 --- a/Lib/test/test_inspect/inspect_fodder2.py +++ b/Lib/test/test_inspect/inspect_fodder2.py @@ -401,3 +401,29 @@ def func400(): return 401 pass # end of file + +# line 405 +string_then_lambda = ( + """text +""", lambda: None +) + +def after_string_then_lambda(): + """A docstring after the lambda.""" + +# line 414 +string_then_multiline_lambda = ( + '''text +''', lambda: [1, + 2] +) + +string_then_fstring_lambda = ( + f"""text +{1}""", lambda: None +) + +string_then_genexpr = ( + """text +""", (i for i in range(3)) +) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 3806a7292d7ae1c..3c52d03301e4b12 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1071,6 +1071,16 @@ def test_nested_lambda(self): # Test inspect.getsource with a nested lambda function. self.assertSourceEqual(mod2.nested_lambda, 291, 292) + def test_lambda_after_multiline_string(self): + # gh-158556: the lambda starts on the line where a multiline string + # ends, and a later docstring must not change the result. + self.assertSourceEqual(mod2.string_then_lambda[1], 407, 408) + self.assertSourceEqual(mod2.string_then_multiline_lambda[1], 416, 418) + self.assertSourceEqual(mod2.string_then_fstring_lambda[1], 422, 423) + + def test_genexpr_after_multiline_string(self): + self.assertSourceEqual(mod2.string_then_genexpr[1].gi_code, 427, 428) + def test_onelinefunc(self): # Test inspect.getsource with a regular one-line function. self.assertSourceEqual(mod2.onelinefunc, 37, 37) @@ -1132,8 +1142,8 @@ def test_lambda_after_multiline_string_end(self): with ready_to_import('lambda_after_string', src) as (name, path): module = import_helper.import_module(name) lines, lnum = inspect.getsourcelines(module.pair[1]) - self.assertEqual(lines, ['""", lambda: None\n']) - self.assertEqual(lnum, 3) + self.assertEqual(lines, [' """text\n', '""", lambda: None\n']) + self.assertEqual(lnum, 2) class TestBlockComments(GetSourceBase): fodderModule = mod diff --git a/Misc/NEWS.d/next/Library/2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst b/Misc/NEWS.d/next/Library/2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst index d7d601b135510a2..9d979768eca4ec1 100644 --- a/Misc/NEWS.d/next/Library/2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst +++ b/Misc/NEWS.d/next/Library/2026-10-01-12-00-00.gh-issue-158556.Lm3kQz.rst @@ -1,2 +1,3 @@ Fix :func:`inspect.getsource` and :func:`inspect.getsourcelines` for a lambda -that is on the same line as the closing delimiter of a multiline string. +or generator expression that starts on the line where a multiline string ends. +The returned source now begins on the line where that string starts.