From f080ac326e5e601553874fdcedc8b8045dd1353e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 6 Aug 2025 22:49:40 +0300 Subject: [PATCH 1/3] gh-137477: Fix inspect.getblock() for generator expressions This fixes also inspect.getsourcelines() and inspect.getsource(). --- Lib/inspect.py | 22 ++++++++++++------- Lib/test/test_inspect/inspect_fodder2.py | 20 +++++++++++++++++ Lib/test/test_inspect/test_inspect.py | 6 +++++ ...-08-06-23-16-42.gh-issue-137477.bk6BDV.rst | 2 ++ 4 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 183e67fabf966e..86ffc85ee87431 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1065,15 +1065,21 @@ def __init__(self): def tokeneater(self, type, token, srowcol, erowcol, line): if not self.started and not self.indecorator: - # skip any decorators - if token == "@": - self.indecorator = True - # look for the first "def", "class" or "lambda" - elif token in ("def", "class", "lambda"): - if token == "lambda": + if type != tokenize.INDENT: + # skip any decorators + if token == "@": + self.indecorator = True + elif token == "async": + pass + # look for the first "def", "class" or "lambda" + elif token in ("def", "class", "lambda"): + if token == "lambda": + self.islambda = True + self.started = True + else: self.islambda = True - self.started = True - self.passline = True # skip to the end of the line + self.started = True + self.passline = True # skip to the end of the line elif type == tokenize.NEWLINE: self.passline = False # stop skipping when a NEWLINE is seen self.last = srowcol[0] diff --git a/Lib/test/test_inspect/inspect_fodder2.py b/Lib/test/test_inspect/inspect_fodder2.py index 43fda6622537fc..1de283f672d362 100644 --- a/Lib/test/test_inspect/inspect_fodder2.py +++ b/Lib/test/test_inspect/inspect_fodder2.py @@ -369,3 +369,23 @@ class dc364: # line 369 dc370 = dataclasses.make_dataclass('dc370', (('x', int), ('y', int))) dc371 = dataclasses.make_dataclass('dc370', (('x', int), ('y', int)), module=__name__) + +import inspect +import itertools + +# line 376 +ge377 = ( + inspect.currentframe() + for i in itertools.count() +) + +# line 382 +def func383(): + # line 384 + ge385 = ( + inspect.currentframe() + for i in itertools.count() + ) + return ge385 + +pass # end of file diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 30e01b8cd87a75..e0d8e9d5e1e301 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1189,12 +1189,18 @@ def test_nested_class_definition_inside_async_function(self): self.assertSourceEqual(run(mod2.func225), 226, 227) self.assertSourceEqual(mod2.cls226, 231, 235) + self.assertSourceEqual(mod2.cls226.func232, 232, 235) self.assertSourceEqual(run(mod2.cls226().func232), 233, 234) def test_class_definition_same_name_diff_methods(self): self.assertSourceEqual(mod2.cls296, 296, 298) self.assertSourceEqual(mod2.cls310, 310, 312) + def test_generator_expression(self): + self.assertSourceEqual(next(mod2.ge377), 377, 380) + self.assertSourceEqual(next(mod2.func383()), 385, 388) + + class TestNoEOL(GetSourceBase): def setUp(self): self.tempdir = TESTFN + '_dir' diff --git a/Misc/NEWS.d/next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst b/Misc/NEWS.d/next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst new file mode 100644 index 00000000000000..1f8ddaed4ca789 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst @@ -0,0 +1,2 @@ +Fix :func:`inspect.getblock`, :func:`inspect.getsourcelines` and +:func:`inspect.getsource` for generator expressions. From de883b92f7715688e266d1be3883609cb745edbf Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 6 Aug 2025 23:56:50 +0300 Subject: [PATCH 2/3] Update 2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst --- .../next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst b/Misc/NEWS.d/next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst index 1f8ddaed4ca789..a6e097ea026293 100644 --- a/Misc/NEWS.d/next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst +++ b/Misc/NEWS.d/next/Library/2025-08-06-23-16-42.gh-issue-137477.bk6BDV.rst @@ -1,2 +1,2 @@ -Fix :func:`inspect.getblock`, :func:`inspect.getsourcelines` and +Fix :func:`!inspect.getblock`, :func:`inspect.getsourcelines` and :func:`inspect.getsource` for generator expressions. From 70ddc909043e36aa27a3d8dca72c64a80902d3a7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 8 Aug 2025 12:42:28 +0300 Subject: [PATCH 3/3] Add comments and simplify the code. --- Lib/inspect.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 86ffc85ee87431..7e0d4dcd897777 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1056,7 +1056,7 @@ class BlockFinder: """Provide a tokeneater() method to detect the end of a code block.""" def __init__(self): self.indent = 0 - self.islambda = False + self.islambda = False # used also for generator expressions self.started = False self.passline = False self.indecorator = False @@ -1065,21 +1065,18 @@ def __init__(self): def tokeneater(self, type, token, srowcol, erowcol, line): if not self.started and not self.indecorator: - if type != tokenize.INDENT: - # skip any decorators - if token == "@": - self.indecorator = True - elif token == "async": - pass - # look for the first "def", "class" or "lambda" - elif token in ("def", "class", "lambda"): - if token == "lambda": - self.islambda = True - self.started = True - else: - self.islambda = True - self.started = True - self.passline = True # skip to the end of the line + if type == tokenize.INDENT or token == "async": + pass + # skip any decorators + elif token == "@": + self.indecorator = True + else: + # For "def" and "class" scan to the end of the block. + # For "lambda" and generator expression scan to + # the end of the logical line. + self.islambda = token not in ("def", "class") + self.started = True + self.passline = True # skip to the end of the line elif type == tokenize.NEWLINE: self.passline = False # stop skipping when a NEWLINE is seen self.last = srowcol[0]