https://github.com/python/cpython/commit/20734734802d7505823c8f2eeafd003d5b7fe4f1
commit: 20734734802d7505823c8f2eeafd003d5b7fe4f1
branch: main
author: Serhiy Storchaka <[email protected]>
committer: terryjreedy <[email protected]>
date: 2026-09-22T14:51:55-04:00
summary:
gh-85560: Fix IDLE paren matching with "else" or "yield" at the start of a
continuation line (#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.
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]