Skip to content

Commit 39e6187

Browse files
committed
gh-44968: Add "Reload from Disk" feature to IDLE
1 parent 1281be1 commit 39e6187

File tree

4 files changed

+174
-0
lines changed

4 files changed

+174
-0
lines changed

Lib/idlelib/idle_test/test_iomenu.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,130 @@ def test_fixnewlines_end(self):
5757
eq(text.get('1.0', 'end-1c'), 'a\n')
5858
eq(fix(), 'a'+io.eol_convention)
5959

60+
def test_reload_no_file(self):
61+
# Test reload when no file is associated
62+
import tempfile
63+
import os
64+
from unittest.mock import Mock
65+
66+
io = self.io
67+
# Ensure no filename is set
68+
io.filename = None
69+
70+
# Mock the messagebox.showinfo
71+
orig_showinfo = iomenu.messagebox.showinfo
72+
showinfo_called = []
73+
def mock_showinfo(*args, **kwargs):
74+
showinfo_called.append((args, kwargs))
75+
iomenu.messagebox.showinfo = mock_showinfo
76+
77+
try:
78+
result = io.reload(None)
79+
self.assertEqual(result, "break")
80+
self.assertEqual(len(showinfo_called), 1)
81+
self.assertIn("No File", showinfo_called[0][0])
82+
finally:
83+
iomenu.messagebox.showinfo = orig_showinfo
84+
85+
def test_reload_with_file(self):
86+
# Test reload with an actual file
87+
import tempfile
88+
import os
89+
90+
io = self.io
91+
text = io.editwin.text
92+
93+
# Create a temporary file
94+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as f:
95+
f.write("# Original content\n")
96+
temp_filename = f.name
97+
98+
try:
99+
# Load the file
100+
io.loadfile(temp_filename)
101+
self.assertEqual(text.get('1.0', 'end-1c'), "# Original content\n")
102+
103+
# Modify the file content externally
104+
with open(temp_filename, 'w') as f:
105+
f.write("# Modified content\n")
106+
107+
# Reload should update the content
108+
result = io.reload(None)
109+
self.assertEqual(result, "break")
110+
self.assertEqual(text.get('1.0', 'end-1c'), "# Modified content\n")
111+
finally:
112+
os.unlink(temp_filename)
113+
114+
def test_reload_with_unsaved_changes_cancel(self):
115+
# Test reload with unsaved changes and user cancels
116+
import tempfile
117+
import os
118+
119+
io = self.io
120+
text = io.editwin.text
121+
122+
# Create a temporary file
123+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as f:
124+
f.write("# Original content\n")
125+
temp_filename = f.name
126+
127+
try:
128+
# Load the file
129+
io.loadfile(temp_filename)
130+
131+
# Make unsaved changes
132+
text.insert('end-1c', "\n# Unsaved change")
133+
io.set_saved(False)
134+
135+
# Mock askokcancel to return False (cancel)
136+
orig_askokcancel = iomenu.messagebox.askokcancel
137+
iomenu.messagebox.askokcancel = lambda *args, **kwargs: False
138+
139+
try:
140+
result = io.reload(None)
141+
self.assertEqual(result, "break")
142+
# Content should not change
143+
self.assertIn("# Unsaved change", text.get('1.0', 'end-1c'))
144+
finally:
145+
iomenu.messagebox.askokcancel = orig_askokcancel
146+
finally:
147+
os.unlink(temp_filename)
148+
149+
def test_reload_with_unsaved_changes_confirm(self):
150+
# Test reload with unsaved changes and user confirms
151+
import tempfile
152+
import os
153+
154+
io = self.io
155+
text = io.editwin.text
156+
157+
# Create a temporary file
158+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as f:
159+
f.write("# Original content\n")
160+
temp_filename = f.name
161+
162+
try:
163+
# Load the file
164+
io.loadfile(temp_filename)
165+
166+
# Make unsaved changes
167+
text.insert('end-1c', "\n# Unsaved change")
168+
io.set_saved(False)
169+
170+
# Mock askokcancel to return True (confirm)
171+
orig_askokcancel = iomenu.messagebox.askokcancel
172+
iomenu.messagebox.askokcancel = lambda *args, **kwargs: True
173+
174+
try:
175+
result = io.reload(None)
176+
self.assertEqual(result, "break")
177+
# Content should be reverted to original
178+
self.assertEqual(text.get('1.0', 'end-1c'), "# Original content\n")
179+
finally:
180+
iomenu.messagebox.askokcancel = orig_askokcancel
181+
finally:
182+
os.unlink(temp_filename)
183+
60184

61185
def _extension_in_filetypes(extension):
62186
return any(

Lib/idlelib/iomenu.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(self, editwin):
3131
self.save_as)
3232
self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
3333
self.save_a_copy)
34+
self.__id_reload = self.text.bind("<<reload-window>>", self.reload)
3435
self.fileencoding = 'utf-8'
3536
self.__id_print = self.text.bind("<<print-window>>", self.print_window)
3637

@@ -40,6 +41,7 @@ def close(self):
4041
self.text.unbind("<<save-window>>", self.__id_save)
4142
self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
4243
self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
44+
self.text.unbind("<<reload-window>>", self.__id_reload)
4345
self.text.unbind("<<print-window>>", self.__id_print)
4446
# Break cycles
4547
self.editwin = None
@@ -237,6 +239,49 @@ def save_a_copy(self, event):
237239
self.updaterecentfileslist(filename)
238240
return "break"
239241

242+
def reload(self, event):
243+
"""Reload the file from disk, discarding any unsaved changes.
244+
245+
If the file has unsaved changes, ask the user to confirm.
246+
"""
247+
if not self.filename:
248+
messagebox.showinfo(
249+
"No File",
250+
"This window has no associated file to reload.",
251+
parent=self.text)
252+
self.text.focus_set()
253+
return "break"
254+
255+
if not self.get_saved():
256+
confirm = messagebox.askokcancel(
257+
title="Reload File",
258+
message=f"Discard changes to {self.filename}?",
259+
default=messagebox.CANCEL,
260+
parent=self.text)
261+
if not confirm:
262+
self.text.focus_set()
263+
return "break"
264+
265+
# Save cursor position
266+
insert_pos = self.text.index("insert")
267+
yview_pos = self.text.yview()
268+
269+
# Reload the file
270+
if self.loadfile(self.filename):
271+
# Try to restore cursor position if the file still has that line
272+
try:
273+
self.text.mark_set("insert", insert_pos)
274+
self.text.see("insert")
275+
# Restore vertical scroll position
276+
self.text.yview_moveto(yview_pos[0])
277+
except Exception:
278+
# If position doesn't exist anymore, go to top
279+
self.text.mark_set("insert", "1.0")
280+
self.text.see("insert")
281+
282+
self.text.focus_set()
283+
return "break"
284+
240285
def writefile(self, filename):
241286
text = self.fixnewlines()
242287
chars = self.encode(text)

Lib/idlelib/mainmenu.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
('_Save', '<<save-window>>'),
3232
('Save _As...', '<<save-window-as-file>>'),
3333
('Save Cop_y As...', '<<save-copy-of-window-as-file>>'),
34+
('Re_load from Disk', '<<reload-window>>'),
3435
None,
3536
('Prin_t Window', '<<print-window>>'),
3637
None,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add "Reload from Disk" menu item to IDLE's File menu. This allows users to
2+
easily reload a file from disk, discarding any unsaved changes in the editor.
3+
The feature is particularly useful when working with version control systems
4+
or when external tools modify files. Patch by ashm-dev.

0 commit comments

Comments
 (0)