Skip to content

Commit f174d32

Browse files
authored
Merge branch 'main' into gh-151903-optimize-contextmanager
2 parents e4f2608 + 3f5491a commit f174d32

16 files changed

Lines changed: 168 additions & 18 deletions

Lib/idlelib/idle_test/test_multicall.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ def test_yview(self):
4343
mctext = self.mc(self.root)
4444
self.assertIs(mctext.yview.__func__, Text.yview)
4545

46+
def test_event_delete_unbound_sequence(self):
47+
# gh-89360: deleting a sequence that was not added to a virtual
48+
# event is ignored instead of raising ValueError.
49+
mctext = self.mc(self.root)
50+
mctext.event_add('<<tester>>', '<Control-Key-a>')
51+
info = mctext.event_info('<<tester>>')
52+
self.assertEqual(len(info), 1)
53+
54+
# A different sequence, never added: a no-op, not an error.
55+
mctext.event_delete('<<tester>>', '<Control-Key-b>')
56+
self.assertEqual(mctext.event_info('<<tester>>'), info)
57+
58+
# The added sequence can still be deleted normally.
59+
mctext.event_delete('<<tester>>', '<Control-Key-a>')
60+
self.assertNotIn(info[0], mctext.event_info('<<tester>>'))
61+
4662

4763
if __name__ == '__main__':
4864
unittest.main(verbosity=2)

Lib/idlelib/idle_test/test_pyshell.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Plus coverage of test_warning. Was 20% with test_openshell.
33

44
from idlelib import pyshell
5+
import os
56
import unittest
67
from test.support import requires
78
from tkinter import Tk
@@ -28,6 +29,14 @@ def test_restart_line_narrow(self):
2829
self.assertEqual(pyshell.restart_line(width, ''), expect)
2930
self.assertEqual(pyshell.restart_line(taglen+2, ''), expect+' =')
3031

32+
def test_fix_user_path(self):
33+
# gh-134300: the idlelib directory is removed, other entries kept.
34+
eq = self.assertEqual
35+
idlelib_dir = os.path.dirname(os.path.abspath(pyshell.__file__))
36+
eq(pyshell.fix_user_path(['', '/a', idlelib_dir, '/b']), ['', '/a', '/b'])
37+
eq(pyshell.fix_user_path(['/a', '/b']), ['/a', '/b'])
38+
eq(pyshell.fix_user_path([idlelib_dir]), [])
39+
3140

3241
class PyShellFileListTest(unittest.TestCase):
3342

Lib/idlelib/idle_test/test_replace.py

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,32 +246,108 @@ def test_replace_backwards(self):
246246
equal(text.get('1.2', '1.5'), 'was')
247247

248248
def test_replace_all(self):
249+
# The default mode, forward with wrap around, replaces every
250+
# match, both below and above the current position.
251+
equal = self.assertEqual
249252
text = self.text
250253
pv = self.engine.patvar
251254
rv = self.dialog.replvar
252255
replace_all = self.dialog.replace_all
253256

254-
text.insert('insert', '\n')
255-
text.insert('insert', text.get('1.0', 'end')*100)
256-
pv.set('is')
257-
rv.set('was')
257+
text.delete('1.0', 'end')
258+
text.insert('1.0', 'a\na\na\n')
259+
text.mark_set('insert', '2.1')
260+
pv.set('a')
261+
rv.set('b')
258262
replace_all()
259-
self.assertNotIn('is', text.get('1.0', 'end'))
263+
equal(text.get('1.0', '3.end'), 'b\nb\nb') # Wrapped around.
260264

265+
# An empty regular expression is reported as an error.
261266
self.engine.revar.set(True)
262267
pv.set('')
263268
replace_all()
264269
self.assertIn('error', showerror.title)
265270
self.assertIn('Empty', showerror.message)
266271

272+
# An invalid replacement expression is reported as an error,
273+
# and nothing is replaced.
274+
text.delete('1.0', 'end')
275+
text.insert('1.0', 'asT')
267276
pv.set('[s][T]')
268277
rv.set('\\')
269278
replace_all()
279+
self.assertIn('error', showerror.title)
280+
self.assertIn('Invalid Replace Expression', showerror.message)
281+
equal(text.get('1.0', '1.end'), 'asT')
270282

283+
# A pattern that is not present replaces nothing.
271284
self.engine.revar.set(False)
285+
text.delete('1.0', 'end')
286+
text.insert('1.0', 'unchanged')
272287
pv.set('text which is not present')
273288
rv.set('foobar')
274289
replace_all()
290+
equal(text.get('1.0', '1.end'), 'unchanged')
291+
292+
def test_replace_all_backwards_no_wrap(self):
293+
# gh-71956: 'up' without wrap replaces all matches from the start
294+
# of the text down to the current position, not just one up.
295+
equal = self.assertEqual
296+
text = self.text
297+
pv = self.engine.patvar
298+
rv = self.dialog.replvar
299+
replace_all = self.dialog.replace_all
300+
self.engine.backvar.set(True)
301+
self.engine.wrapvar.set(False)
302+
303+
text.delete('1.0', 'end')
304+
text.insert('1.0', 'a\na\na\n')
305+
text.mark_set('insert', '2.1')
306+
pv.set('a')
307+
rv.set('b')
308+
replace_all()
309+
equal(text.get('1.0', '1.end'), 'b') # Above the cursor.
310+
equal(text.get('2.0', '2.end'), 'b') # At the cursor.
311+
equal(text.get('3.0', '3.end'), 'a') # Below the cursor, untouched.
312+
313+
def test_replace_all_forwards_no_wrap(self):
314+
# 'down' without wrap replaces all matches from the current
315+
# position to the end of the text, and none before it.
316+
equal = self.assertEqual
317+
text = self.text
318+
pv = self.engine.patvar
319+
rv = self.dialog.replvar
320+
replace_all = self.dialog.replace_all
321+
self.engine.backvar.set(False)
322+
self.engine.wrapvar.set(False)
323+
324+
text.delete('1.0', 'end')
325+
text.insert('1.0', 'a\na\na\n')
326+
text.mark_set('insert', '2.1')
327+
pv.set('a')
328+
rv.set('b')
329+
replace_all()
330+
equal(text.get('1.0', '1.end'), 'a') # Before the cursor, untouched.
331+
equal(text.get('2.0', '2.end'), 'a') # Before the cursor, untouched.
332+
equal(text.get('3.0', '3.end'), 'b') # After the cursor.
333+
334+
def test_replace_all_backwards_wrap(self):
335+
# With wrap around, an 'up' search also replaces every match.
336+
equal = self.assertEqual
337+
text = self.text
338+
pv = self.engine.patvar
339+
rv = self.dialog.replvar
340+
replace_all = self.dialog.replace_all
341+
self.engine.backvar.set(True)
342+
self.engine.wrapvar.set(True)
343+
344+
text.delete('1.0', 'end')
345+
text.insert('1.0', 'a\na\na\n')
346+
text.mark_set('insert', '2.1')
347+
pv.set('a')
348+
rv.set('b')
349+
replace_all()
350+
equal(text.get('1.0', '3.end'), 'b\nb\nb')
275351

276352
def test_default_command(self):
277353
text = self.text

Lib/idlelib/idle_test/test_stackviewer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def test_init(self):
3535
isi(stackviewer.sc, ScrolledCanvas)
3636
isi(stackviewer.item, stackviewer.StackTreeItem)
3737
isi(stackviewer.node, TreeNode)
38+
top = stackviewer.sc.frame.winfo_toplevel()
39+
self.assertEqual(top.winfo_class(), 'Idle')
3840

3941

4042
if __name__ == '__main__':

Lib/idlelib/idle_test/test_window.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def test_init(self):
3939
win = window.ListedToplevel(self.root)
4040
self.assertIn(win, window.registry)
4141
self.assertEqual(win.focused_widget, win)
42+
self.assertEqual(win.winfo_class(), 'Idle')
43+
44+
def test_init_class_override(self):
45+
win = window.ListedToplevel(self.root, class_='Other')
46+
self.assertEqual(win.winfo_class(), 'Other')
4247

4348

4449
if __name__ == '__main__':

Lib/idlelib/multicall.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,10 +386,11 @@ def event_delete(self, virtual, *sequences):
386386
if triplet is None:
387387
#print("Tkinter event_delete: %s" % seq, file=sys.__stderr__)
388388
widget.event_delete(self, virtual, seq)
389-
else:
389+
elif triplet in triplets:
390390
if func is not None:
391391
self.__binders[triplet[1]].unbind(triplet, func)
392392
triplets.remove(triplet)
393+
# Else the sequence is not bound; ignore it (gh-89360).
393394

394395
def event_info(self, virtual=None):
395396
if virtual is None or virtual not in self.__eventinfo:

Lib/idlelib/pyshell.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,17 @@ def restart_line(width, filename): # See bpo-38141.
408408
return tag[:-2] # Remove ' ='.
409409

410410

411+
def fix_user_path(path):
412+
"""Return path without the idlelib directory (gh-134300).
413+
414+
That directory is on sys.path when idle.py is run as a script.
415+
Otherwise user code could import idlelib submodules as top-level
416+
modules, such as "import help".
417+
"""
418+
idlelib_dir = os.path.dirname(os.path.abspath(__file__))
419+
return [p for p in path if p != idlelib_dir]
420+
421+
411422
class ModifiedInterpreter(InteractiveInterpreter):
412423

413424
def __init__(self, tkconsole):
@@ -568,6 +579,7 @@ def transfer_path(self, with_cwd=False):
568579
path.extend(sys.path)
569580
else:
570581
path = sys.path
582+
path = fix_user_path(path) # gh-134300
571583

572584
self.runcommand("""if 1:
573585
import sys as _sys
@@ -644,7 +656,7 @@ def remote_stack_viewer(self):
644656
return
645657
item = debugobj_r.StubObjectTreeItem(self.rpcclt, oid)
646658
from idlelib.tree import ScrolledCanvas, TreeNode
647-
top = Toplevel(self.tkconsole.root)
659+
top = Toplevel(self.tkconsole.root, class_='Idle')
648660
theme = idleConf.CurrentTheme()
649661
background = idleConf.GetHighlight(theme, 'normal')['background']
650662
sc = ScrolledCanvas(top, bg=background, highlightthickness=0)

Lib/idlelib/replace.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,13 @@ def _replace_expand(self, m, repl):
122122
def replace_all(self, event=None):
123123
"""Handle the Replace All button.
124124
125-
Search text for occurrences of the Find value and replace
126-
each of them. The 'wrap around' value controls the start
127-
point for searching. If wrap isn't set, then the searching
128-
starts at the first occurrence after the current selection;
129-
if wrap is set, the replacement starts at the first line.
130-
The replacement is always done top-to-bottom in the text.
125+
Search text for occurrences of the Find value and replace each
126+
of them. The 'wrap around' and direction values control which
127+
occurrences are replaced. With wrap around, every occurrence is
128+
replaced. Without it, a forward search replaces occurrences from
129+
the current position to the end of the text, and a backward search
130+
replaces occurrences from the beginning of the text to the current
131+
position. The replacement is always done top-to-bottom.
131132
"""
132133
prog = self.engine.getprog()
133134
if not prog:
@@ -142,22 +143,32 @@ def replace_all(self, event=None):
142143
text.tag_remove("hit", "1.0", "end")
143144
line = res[0]
144145
col = res[1].start()
146+
# For a backward search without wrap, replace top-to-bottom from
147+
# the start of the text down to the first match at or above the
148+
# current position (gh-71956). A mark tracks that stop point.
149+
stop = None
145150
if self.engine.iswrap():
146151
line = 1
147152
col = 0
153+
elif self.engine.isback():
154+
stop = "replace_all_stop"
155+
text.mark_set(stop, "%d.%d" % (line, res[1].end()))
156+
line = 1
157+
col = 0
148158
ok = True
149159
first = last = None
150160
# XXX ought to replace circular instead of top-to-bottom when wrapping
151161
text.undo_block_start()
152162
while res := self.engine.search_forward(
153163
text, prog, line, col, wrap=False, ok=ok):
154164
line, m = res
155-
chars = text.get("%d.0" % line, "%d.0" % (line+1))
165+
i, j = m.span()
166+
if stop is not None and text.compare("%d.%d" % (line, i), ">=", stop):
167+
break
156168
orig = m.group()
157169
new = self._replace_expand(m, repl)
158170
if new is None:
159171
break
160-
i, j = m.span()
161172
first = "%d.%d" % (line, i)
162173
last = "%d.%d" % (line, j)
163174
if new == orig:
@@ -170,6 +181,8 @@ def replace_all(self, event=None):
170181
text.insert(first, new, self.insert_tags)
171182
col = i + len(new)
172183
ok = False
184+
if stop is not None:
185+
text.mark_unset(stop)
173186
text.undo_block_stop()
174187
if first and last:
175188
self.show_hit(first, last)

Lib/idlelib/stackviewer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
def StackBrowser(root, exc, flist=None, top=None):
1212
global sc, item, node # For testing.
1313
if top is None:
14-
top = tk.Toplevel(root)
14+
top = tk.Toplevel(root, class_='Idle')
1515
sc = ScrolledCanvas(top, bg="white", highlightthickness=0)
1616
sc.frame.pack(expand=1, fill="both")
1717
item = StackTreeItem(exc, flist)

Lib/idlelib/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
def fix_scaling(root): # Called in filelist _test, pyshell, and run.
2525
"""Scale fonts on HiDPI displays, once per process."""
2626
import tkinter.font
27-
scaling = float(root.tk.call('tk', 'scaling'))
27+
scaling = root.tk_scaling()
2828
if scaling > 1.4:
2929
for name in tkinter.font.names(root):
3030
font = tkinter.font.Font(root=root, name=name, exists=True)

0 commit comments

Comments
 (0)