From 2ba2a5c60b50a915f3c97cb3ae28af5a98e356c4 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 17 Sep 2026 20:42:48 +0300 Subject: [PATCH 1/3] gh-85560: Fix IDLE paren matching with "else" at the start of a continuation line pyparse took a line starting with "else" for the start of a statement, so the parenthesis containing a conditional expression continued on such a line was not found. Now "else" is a statement start only if it is followed by a colon. --- Lib/idlelib/idle_test/test_pyparse.py | 17 +++++++++++++++++ Lib/idlelib/pyparse.py | 2 +- ...6-09-17-18-00-00.gh-issue-85560.elseexpr.rst | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst diff --git a/Lib/idlelib/idle_test/test_pyparse.py b/Lib/idlelib/idle_test/test_pyparse.py index 384db566ac76cdb..df05d6f91fa458e 100644 --- a/Lib/idlelib/idle_test/test_pyparse.py +++ b/Lib/idlelib/idle_test/test_pyparse.py @@ -118,6 +118,23 @@ def char_in_string_false(index): return False # split line test. eq(start(is_char_in_string=lambda index: index < pos), pos) + # gh-85560: 'else' of a conditional expression at the start of + # a continuation line does not start a statement. + setcode('def f():\n' + ' return (1 if x\n' + ' else 0)\n') + eq(start(char_in_string_false), 9) # Start of 'return' line. + setcode('if x:\n' + ' pass\n' + 'else:\n' + ' pass\n') + eq(start(char_in_string_false), 15) + setcode('if x:\n' + ' pass\n' + 'else : # comment\n' + ' pass\n') + eq(start(char_in_string_false), 15) + def test_set_lo(self): code = ( '"""This is a module docstring"""\n' diff --git a/Lib/idlelib/pyparse.py b/Lib/idlelib/pyparse.py index 8545c63e1435d46..b01ebc9cd86bb98 100644 --- a/Lib/idlelib/pyparse.py +++ b/Lib/idlelib/pyparse.py @@ -22,7 +22,7 @@ ^ [ \t]* (?: while - | else + | else (?= [ \t]* : ) # not "else" of a conditional expression | def | return | assert diff --git a/Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst b/Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst new file mode 100644 index 000000000000000..7f004ceaab11ddc --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst @@ -0,0 +1,2 @@ +Fix IDLE failing to find the opening parenthesis when a continuation line +inside the parentheses starts with ``else`` of a conditional expression. From 12f8dd8a5b126a11690e00ac683d27112d630307 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 17 Sep 2026 20:46:42 +0300 Subject: [PATCH 2/3] gh-85560: Fix IDLE paren matching with "else" or "yield" at the start of a continuation line pyparse took a line starting with "else" or "yield" for the start of a statement, so the parenthesis containing a conditional expression or a yield expression continued on such a line was not found. Now "else" is a statement start only if it is followed by a colon, and "yield" is not a statement start. --- Lib/idlelib/idle_test/test_pyparse.py | 5 +++++ Lib/idlelib/pyparse.py | 1 - .../IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/test_pyparse.py b/Lib/idlelib/idle_test/test_pyparse.py index df05d6f91fa458e..4005a158bd1c704 100644 --- a/Lib/idlelib/idle_test/test_pyparse.py +++ b/Lib/idlelib/idle_test/test_pyparse.py @@ -134,6 +134,11 @@ def char_in_string_false(index): return False 'else : # comment\n' ' pass\n') eq(start(char_in_string_false), 15) + # A yield expression can start a continuation line too. + setcode('def f():\n' + ' x = (\n' + ' yield y)\n') + eq(start(char_in_string_false), 0) def test_set_lo(self): code = ( diff --git a/Lib/idlelib/pyparse.py b/Lib/idlelib/pyparse.py index b01ebc9cd86bb98..8b4388c983fc16d 100644 --- a/Lib/idlelib/pyparse.py +++ b/Lib/idlelib/pyparse.py @@ -34,7 +34,6 @@ | except | raise | import - | yield ) \b """, re.VERBOSE | re.MULTILINE).search diff --git a/Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst b/Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst index 7f004ceaab11ddc..9e20622bb535b0c 100644 --- a/Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst +++ b/Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst @@ -1,2 +1,3 @@ Fix IDLE failing to find the opening parenthesis when a continuation line -inside the parentheses starts with ``else`` of a conditional expression. +inside the parentheses starts with ``else`` of a conditional expression or +with ``yield``. From 041a12f9d5786cf375a53e05329db7fdf48e7d7a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 17 Sep 2026 20:53:36 +0300 Subject: [PATCH 3/3] Add more statement starters to pyparse._synchre "with", "del", "global", "nonlocal", "pass" and "finally" cannot start a continuation line, so they are safe synchronization points, and let find_good_parse_start() cut the parsed text closer to its end. --- Lib/idlelib/idle_test/test_pyparse.py | 27 +++++++++++++++++---------- Lib/idlelib/pyparse.py | 6 ++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Lib/idlelib/idle_test/test_pyparse.py b/Lib/idlelib/idle_test/test_pyparse.py index 4005a158bd1c704..601e8a93fdfa858 100644 --- a/Lib/idlelib/idle_test/test_pyparse.py +++ b/Lib/idlelib/idle_test/test_pyparse.py @@ -78,7 +78,7 @@ def char_in_string_false(index): return False ' b=True):\n' ' pass\n' ) - pos0, pos = 33, 42 # Start of 'class...', ' def' lines. + pos0, pos, pos1 = 33, 42, 94 # Start of 'class', 'def', 'pass' lines. # Passing no value or non-callable should fail (issue 32989). with self.assertRaises(TypeError): @@ -91,8 +91,8 @@ def char_in_string_false(index): return False self.assertIsNone(start(is_char_in_string=lambda index: True)) # Make all text look like it's not in a string. This means that it - # found a good start position. - eq(start(char_in_string_false), pos) + # found a good start position: the last statement. + eq(start(char_in_string_false), pos1) # If the beginning of the def line is not in a string, then it # returns that as the index. @@ -100,9 +100,9 @@ def char_in_string_false(index): return False # If the beginning of the def line is in a string, then it # looks for a previous index. eq(start(is_char_in_string=lambda index: index >= pos), pos0) - # If everything before the 'def' is in a string, then returns None. - # The non-continuation def line returns 44 (see below). - eq(start(is_char_in_string=lambda index: index < pos), None) + # If everything before the 'def' is in a string, then returns + # the start of the 'pass' line. + eq(start(is_char_in_string=lambda index: index < pos), pos1) # Code without extra line break in def line - mostly returns the same # values. @@ -111,12 +111,13 @@ def char_in_string_false(index): return False ' def __init__(self, a, b=True):\n' ' pass\n' ) # Does not affect class, def positions. - eq(start(char_in_string_false), pos) + pos1 = 77 # Start of 'pass' line. + eq(start(char_in_string_false), pos1) eq(start(is_char_in_string=lambda index: index > pos), pos) eq(start(is_char_in_string=lambda index: index >= pos), pos0) # When the def line isn't split, this returns which doesn't match the # split line test. - eq(start(is_char_in_string=lambda index: index < pos), pos) + eq(start(is_char_in_string=lambda index: index < pos), pos1) # gh-85560: 'else' of a conditional expression at the start of # a continuation line does not start a statement. @@ -127,18 +128,24 @@ def char_in_string_false(index): return False setcode('if x:\n' ' pass\n' 'else:\n' - ' pass\n') + ' x = 1\n') eq(start(char_in_string_false), 15) setcode('if x:\n' ' pass\n' 'else : # comment\n' - ' pass\n') + ' x = 1\n') eq(start(char_in_string_false), 15) # A yield expression can start a continuation line too. setcode('def f():\n' ' x = (\n' ' yield y)\n') eq(start(char_in_string_false), 0) + # Other statements which cannot start a continuation line. + for stmt in ('with x:', 'del x', 'global x', 'nonlocal x', 'pass', + 'finally:'): + with self.subTest(stmt=stmt): + setcode(f'if x:\n pass\n{stmt}\n') + eq(start(char_in_string_false), 15) def test_set_lo(self): code = ( diff --git a/Lib/idlelib/pyparse.py b/Lib/idlelib/pyparse.py index 8b4388c983fc16d..2271b787f341d64 100644 --- a/Lib/idlelib/pyparse.py +++ b/Lib/idlelib/pyparse.py @@ -34,6 +34,12 @@ | except | raise | import + | with + | del + | global + | nonlocal + | pass + | finally ) \b """, re.VERBOSE | re.MULTILINE).search