Skip to content

Commit ec64640

Browse files
csabellaterryjreedy
andcommitted
bpo-32989: IDLE - fix bad editor call of pyparse method (GH-5968)
Fix comments and add tests for editor newline_and_indent_event method. Remove unused None default for function parameter of pyparse find_good_parse_start method and code triggered by that default. Co-authored-by: Terry Jan Reedy <[email protected]>
1 parent 8698b34 commit ec64640

File tree

6 files changed

+154
-39
lines changed

6 files changed

+154
-39
lines changed

Lib/idlelib/NEWS.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ Released on 2020-10-05?
33
======================================
44

55

6+
bpo-32989: Add tests for editor newline_and_indent_event method.
7+
Remove dead code from pyparse find_good_parse_start method.
8+
69
bpo-38943: Fix autocomplete windows not always appearing on some
710
systems. Patch by Johnny Najera.
811

Lib/idlelib/editor.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,38 +1342,51 @@ def smart_indent_event(self, event):
13421342
text.undo_block_stop()
13431343

13441344
def newline_and_indent_event(self, event):
1345+
"""Insert a newline and indentation after Enter keypress event.
1346+
1347+
Properly position the cursor on the new line based on information
1348+
from the current line. This takes into account if the current line
1349+
is a shell prompt, is empty, has selected text, contains a block
1350+
opener, contains a block closer, is a continuation line, or
1351+
is inside a string.
1352+
"""
13451353
text = self.text
13461354
first, last = self.get_selection_indices()
13471355
text.undo_block_start()
1348-
try:
1356+
try: # Close undo block and expose new line in finally clause.
13491357
if first and last:
13501358
text.delete(first, last)
13511359
text.mark_set("insert", first)
13521360
line = text.get("insert linestart", "insert")
1361+
1362+
# Count leading whitespace for indent size.
13531363
i, n = 0, len(line)
13541364
while i < n and line[i] in " \t":
1355-
i = i+1
1365+
i += 1
13561366
if i == n:
1357-
# the cursor is in or at leading indentation in a continuation
1358-
# line; just inject an empty line at the start
1367+
# The cursor is in or at leading indentation in a continuation
1368+
# line; just inject an empty line at the start.
13591369
text.insert("insert linestart", '\n')
13601370
return "break"
13611371
indent = line[:i]
1362-
# strip whitespace before insert point unless it's in the prompt
1372+
1373+
# Strip whitespace before insert point unless it's in the prompt.
13631374
i = 0
13641375
while line and line[-1] in " \t" and line != self.prompt_last_line:
13651376
line = line[:-1]
1366-
i = i+1
1377+
i += 1
13671378
if i:
13681379
text.delete("insert - %d chars" % i, "insert")
1369-
# strip whitespace after insert point
1380+
1381+
# Strip whitespace after insert point.
13701382
while text.get("insert") in " \t":
13711383
text.delete("insert")
1372-
# start new line
1384+
1385+
# Insert new line.
13731386
text.insert("insert", '\n')
13741387

1375-
# adjust indentation for continuations and block
1376-
# open/close first need to find the last stmt
1388+
# Adjust indentation for continuations and block open/close.
1389+
# First need to find the last statement.
13771390
lno = index2line(text.index('insert'))
13781391
y = pyparse.Parser(self.indentwidth, self.tabwidth)
13791392
if not self.prompt_last_line:
@@ -1383,7 +1396,7 @@ def newline_and_indent_event(self, event):
13831396
rawtext = text.get(startatindex, "insert")
13841397
y.set_code(rawtext)
13851398
bod = y.find_good_parse_start(
1386-
self._build_char_in_string_func(startatindex))
1399+
self._build_char_in_string_func(startatindex))
13871400
if bod is not None or startat == 1:
13881401
break
13891402
y.set_lo(bod or 0)
@@ -1399,26 +1412,26 @@ def newline_and_indent_event(self, event):
13991412

14001413
c = y.get_continuation_type()
14011414
if c != pyparse.C_NONE:
1402-
# The current stmt hasn't ended yet.
1415+
# The current statement hasn't ended yet.
14031416
if c == pyparse.C_STRING_FIRST_LINE:
1404-
# after the first line of a string; do not indent at all
1417+
# After the first line of a string do not indent at all.
14051418
pass
14061419
elif c == pyparse.C_STRING_NEXT_LINES:
1407-
# inside a string which started before this line;
1408-
# just mimic the current indent
1420+
# Inside a string which started before this line;
1421+
# just mimic the current indent.
14091422
text.insert("insert", indent)
14101423
elif c == pyparse.C_BRACKET:
1411-
# line up with the first (if any) element of the
1424+
# Line up with the first (if any) element of the
14121425
# last open bracket structure; else indent one
14131426
# level beyond the indent of the line with the
1414-
# last open bracket
1427+
# last open bracket.
14151428
self.reindent_to(y.compute_bracket_indent())
14161429
elif c == pyparse.C_BACKSLASH:
1417-
# if more than one line in this stmt already, just
1430+
# If more than one line in this statement already, just
14181431
# mimic the current indent; else if initial line
14191432
# has a start on an assignment stmt, indent to
14201433
# beyond leftmost =; else to beyond first chunk of
1421-
# non-whitespace on initial line
1434+
# non-whitespace on initial line.
14221435
if y.get_num_lines_in_stmt() > 1:
14231436
text.insert("insert", indent)
14241437
else:
@@ -1427,9 +1440,9 @@ def newline_and_indent_event(self, event):
14271440
assert 0, "bogus continuation type %r" % (c,)
14281441
return "break"
14291442

1430-
# This line starts a brand new stmt; indent relative to
1443+
# This line starts a brand new statement; indent relative to
14311444
# indentation of initial line of closest preceding
1432-
# interesting stmt.
1445+
# interesting statement.
14331446
indent = y.get_base_indent_string()
14341447
text.insert("insert", indent)
14351448
if y.is_block_opener():

Lib/idlelib/idle_test/test_editor.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from idlelib import editor
44
import unittest
5+
from collections import namedtuple
56
from test.support import requires
67
from tkinter import Tk
78

@@ -91,5 +92,103 @@ def test_tabwidth_8(self):
9192
)
9293

9394

95+
class IndentAndNewlineTest(unittest.TestCase):
96+
97+
@classmethod
98+
def setUpClass(cls):
99+
requires('gui')
100+
cls.root = Tk()
101+
cls.root.withdraw()
102+
cls.window = Editor(root=cls.root)
103+
cls.window.indentwidth = 2
104+
cls.window.tabwidth = 2
105+
106+
@classmethod
107+
def tearDownClass(cls):
108+
cls.window._close()
109+
del cls.window
110+
cls.root.update_idletasks()
111+
for id in cls.root.tk.call('after', 'info'):
112+
cls.root.after_cancel(id)
113+
cls.root.destroy()
114+
del cls.root
115+
116+
def insert(self, text):
117+
t = self.window.text
118+
t.delete('1.0', 'end')
119+
t.insert('end', text)
120+
# Force update for colorizer to finish.
121+
t.update()
122+
123+
def test_indent_and_newline_event(self):
124+
eq = self.assertEqual
125+
w = self.window
126+
text = w.text
127+
get = text.get
128+
nl = w.newline_and_indent_event
129+
130+
TestInfo = namedtuple('Tests', ['label', 'text', 'expected', 'mark'])
131+
132+
tests = (TestInfo('Empty line inserts with no indent.',
133+
' \n def __init__(self):',
134+
'\n \n def __init__(self):\n',
135+
'1.end'),
136+
TestInfo('Inside bracket before space, deletes space.',
137+
' def f1(self, a, b):',
138+
' def f1(self,\n a, b):\n',
139+
'1.14'),
140+
TestInfo('Inside bracket after space, deletes space.',
141+
' def f1(self, a, b):',
142+
' def f1(self,\n a, b):\n',
143+
'1.15'),
144+
TestInfo('Inside string with one line - no indent.',
145+
' """Docstring."""',
146+
' """Docstring.\n"""\n',
147+
'1.15'),
148+
TestInfo('Inside string with more than one line.',
149+
' """Docstring.\n Docstring Line 2"""',
150+
' """Docstring.\n Docstring Line 2\n """\n',
151+
'2.18'),
152+
TestInfo('Backslash with one line.',
153+
'a =\\',
154+
'a =\\\n \n',
155+
'1.end'),
156+
TestInfo('Backslash with more than one line.',
157+
'a =\\\n multiline\\',
158+
'a =\\\n multiline\\\n \n',
159+
'2.end'),
160+
TestInfo('Block opener - indents +1 level.',
161+
' def f1(self):\n pass',
162+
' def f1(self):\n \n pass\n',
163+
'1.end'),
164+
TestInfo('Block closer - dedents -1 level.',
165+
' def f1(self):\n pass',
166+
' def f1(self):\n pass\n \n',
167+
'2.end'),
168+
)
169+
170+
w.prompt_last_line = ''
171+
for test in tests:
172+
with self.subTest(label=test.label):
173+
self.insert(test.text)
174+
text.mark_set('insert', test.mark)
175+
nl(event=None)
176+
eq(get('1.0', 'end'), test.expected)
177+
178+
# Selected text.
179+
self.insert(' def f1(self, a, b):\n return a + b')
180+
text.tag_add('sel', '1.17', '1.end')
181+
nl(None)
182+
# Deletes selected text before adding new line.
183+
eq(get('1.0', 'end'), ' def f1(self, a,\n \n return a + b\n')
184+
185+
# Preserves the whitespace in shell prompt.
186+
w.prompt_last_line = '>>> '
187+
self.insert('>>> \t\ta =')
188+
text.mark_set('insert', '1.5')
189+
nl(None)
190+
eq(get('1.0', 'end'), '>>> \na =\n')
191+
192+
94193
if __name__ == '__main__':
95194
unittest.main(verbosity=2)

Lib/idlelib/idle_test/test_pyparse.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_trans(self):
1818
# trans is the production instance of ParseMap, used in _study1
1919
parser = pyparse.Parser(4, 4)
2020
self.assertEqual('\t a([{b}])b"c\'d\n'.translate(pyparse.trans),
21-
'xxx(((x)))x"x\'x\n')
21+
'xxx(((x)))x"x\'x\n')
2222

2323

2424
class PyParseTest(unittest.TestCase):
@@ -61,14 +61,17 @@ def test_find_good_parse_start(self):
6161

6262
# Split def across lines.
6363
setcode('"""This is a module docstring"""\n'
64-
'class C():\n'
65-
' def __init__(self, a,\n'
66-
' b=True):\n'
67-
' pass\n'
68-
)
64+
'class C():\n'
65+
' def __init__(self, a,\n'
66+
' b=True):\n'
67+
' pass\n'
68+
)
6969

70-
# No value sent for is_char_in_string().
71-
self.assertIsNone(start())
70+
# Passing no value or non-callable should fail (issue 32989).
71+
with self.assertRaises(TypeError):
72+
start()
73+
with self.assertRaises(TypeError):
74+
start(False)
7275

7376
# Make text look like a string. This returns pos as the start
7477
# position, but it's set to None.
@@ -91,10 +94,10 @@ def test_find_good_parse_start(self):
9194
# Code without extra line break in def line - mostly returns the same
9295
# values.
9396
setcode('"""This is a module docstring"""\n'
94-
'class C():\n'
95-
' def __init__(self, a, b=True):\n'
96-
' pass\n'
97-
)
97+
'class C():\n'
98+
' def __init__(self, a, b=True):\n'
99+
' pass\n'
100+
)
98101
eq(start(is_char_in_string=lambda index: False), 44)
99102
eq(start(is_char_in_string=lambda index: index > 44), 44)
100103
eq(start(is_char_in_string=lambda index: index >= 44), 33)

Lib/idlelib/pyparse.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,7 @@ def set_code(self, s):
133133
self.code = s
134134
self.study_level = 0
135135

136-
def find_good_parse_start(self, is_char_in_string=None,
137-
_synchre=_synchre):
136+
def find_good_parse_start(self, is_char_in_string, _synchre=_synchre):
138137
"""
139138
Return index of a good place to begin parsing, as close to the
140139
end of the string as possible. This will be the start of some
@@ -149,10 +148,6 @@ def find_good_parse_start(self, is_char_in_string=None,
149148
"""
150149
code, pos = self.code, None
151150

152-
if not is_char_in_string:
153-
# no clue -- make the caller pass everything
154-
return None
155-
156151
# Peek back from the end for a good place to start,
157152
# but don't try too often; pos will be left None, or
158153
# bumped to a legitimate synch point.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add tests for editor newline_and_indent_event method.
2+
Remove dead code from pyparse find_good_parse_start method.

0 commit comments

Comments
 (0)