https://github.com/python/cpython/commit/8cceb109378b9e80cf5db0577f37589ad7d2c9ee commit: 8cceb109378b9e80cf5db0577f37589ad7d2c9ee branch: 3.13 author: Miss Islington (bot) <[email protected]> committer: terryjreedy <[email protected]> date: 2026-09-22T19:20:00Z summary:
[3.13] gh-85560: Fix IDLE paren matching with "else" or "yield" at the start of a continuation line (GH-157693) (#157961) gh-85560: Fix IDLE paren matching with "else" or "yield" at the start of a continuation line (GH-157693) `pyparse._synchre` took any 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 (`(1 if x\n else 0)`, `(\n yield x)`) was not found by Show Surrounding Parens. An `else` statement is always followed by a colon, a conditional expression never, so `else` now counts as a statement start only when followed by `:`. A `yield` statement cannot be told from a yield expression, so `yield` is removed from the list; it only served to shorten the parsed text. On the other hand, `with`, `del`, `global`, `nonlocal`, `pass` and `finally`, which cannot start a continuation line, are added. (cherry picked from commit 20734734802d7505823c8f2eeafd003d5b7fe4f1) Co-authored-by: Serhiy Storchaka <[email protected]> files: A Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst M Lib/idlelib/idle_test/test_pyparse.py M Lib/idlelib/pyparse.py diff --git a/Lib/idlelib/idle_test/test_pyparse.py b/Lib/idlelib/idle_test/test_pyparse.py index 384db566ac76cd..601e8a93fdfa85 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,41 @@ 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. + 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' + ' x = 1\n') + eq(start(char_in_string_false), 15) + setcode('if x:\n' + ' pass\n' + 'else : # comment\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 8545c63e1435d4..2271b787f341d6 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 @@ -34,7 +34,12 @@ | except | raise | import - | yield + | with + | del + | global + | nonlocal + | pass + | finally ) \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 new file mode 100644 index 00000000000000..9e20622bb535b0 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-17-18-00-00.gh-issue-85560.elseexpr.rst @@ -0,0 +1,3 @@ +Fix IDLE failing to find the opening parenthesis when a continuation line +inside the parentheses starts with ``else`` of a conditional expression or +with ``yield``. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
