Skip to content

Commit 2073473

Browse files
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.
1 parent 9777e8a commit 2073473

3 files changed

Lines changed: 47 additions & 10 deletions

File tree

‎Lib/idlelib/idle_test/test_pyparse.py‎

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def char_in_string_false(index): return False
7878
' b=True):\n'
7979
' pass\n'
8080
)
81-
pos0, pos = 33, 42 # Start of 'class...', ' def' lines.
81+
pos0, pos, pos1 = 33, 42, 94 # Start of 'class', 'def', 'pass' lines.
8282

8383
# Passing no value or non-callable should fail (issue 32989).
8484
with self.assertRaises(TypeError):
@@ -91,18 +91,18 @@ def char_in_string_false(index): return False
9191
self.assertIsNone(start(is_char_in_string=lambda index: True))
9292

9393
# Make all text look like it's not in a string. This means that it
94-
# found a good start position.
95-
eq(start(char_in_string_false), pos)
94+
# found a good start position: the last statement.
95+
eq(start(char_in_string_false), pos1)
9696

9797
# If the beginning of the def line is not in a string, then it
9898
# returns that as the index.
9999
eq(start(is_char_in_string=lambda index: index > pos), pos)
100100
# If the beginning of the def line is in a string, then it
101101
# looks for a previous index.
102102
eq(start(is_char_in_string=lambda index: index >= pos), pos0)
103-
# If everything before the 'def' is in a string, then returns None.
104-
# The non-continuation def line returns 44 (see below).
105-
eq(start(is_char_in_string=lambda index: index < pos), None)
103+
# If everything before the 'def' is in a string, then returns
104+
# the start of the 'pass' line.
105+
eq(start(is_char_in_string=lambda index: index < pos), pos1)
106106

107107
# Code without extra line break in def line - mostly returns the same
108108
# values.
@@ -111,12 +111,41 @@ def char_in_string_false(index): return False
111111
' def __init__(self, a, b=True):\n'
112112
' pass\n'
113113
) # Does not affect class, def positions.
114-
eq(start(char_in_string_false), pos)
114+
pos1 = 77 # Start of 'pass' line.
115+
eq(start(char_in_string_false), pos1)
115116
eq(start(is_char_in_string=lambda index: index > pos), pos)
116117
eq(start(is_char_in_string=lambda index: index >= pos), pos0)
117118
# When the def line isn't split, this returns which doesn't match the
118119
# split line test.
119-
eq(start(is_char_in_string=lambda index: index < pos), pos)
120+
eq(start(is_char_in_string=lambda index: index < pos), pos1)
121+
122+
# gh-85560: 'else' of a conditional expression at the start of
123+
# a continuation line does not start a statement.
124+
setcode('def f():\n'
125+
' return (1 if x\n'
126+
' else 0)\n')
127+
eq(start(char_in_string_false), 9) # Start of 'return' line.
128+
setcode('if x:\n'
129+
' pass\n'
130+
'else:\n'
131+
' x = 1\n')
132+
eq(start(char_in_string_false), 15)
133+
setcode('if x:\n'
134+
' pass\n'
135+
'else : # comment\n'
136+
' x = 1\n')
137+
eq(start(char_in_string_false), 15)
138+
# A yield expression can start a continuation line too.
139+
setcode('def f():\n'
140+
' x = (\n'
141+
' yield y)\n')
142+
eq(start(char_in_string_false), 0)
143+
# Other statements which cannot start a continuation line.
144+
for stmt in ('with x:', 'del x', 'global x', 'nonlocal x', 'pass',
145+
'finally:'):
146+
with self.subTest(stmt=stmt):
147+
setcode(f'if x:\n pass\n{stmt}\n')
148+
eq(start(char_in_string_false), 15)
120149

121150
def test_set_lo(self):
122151
code = (

‎Lib/idlelib/pyparse.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
^
2323
[ \t]*
2424
(?: while
25-
| else
25+
| else (?= [ \t]* : ) # not "else" of a conditional expression
2626
| def
2727
| return
2828
| assert
@@ -34,7 +34,12 @@
3434
| except
3535
| raise
3636
| import
37-
| yield
37+
| with
38+
| del
39+
| global
40+
| nonlocal
41+
| pass
42+
| finally
3843
)
3944
\b
4045
""", re.VERBOSE | re.MULTILINE).search
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix IDLE failing to find the opening parenthesis when a continuation line
2+
inside the parentheses starts with ``else`` of a conditional expression or
3+
with ``yield``.

0 commit comments

Comments
 (0)