Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
39e6187
gh-44968: Add "Reload from Disk" feature to IDLE
ashm-dev Nov 14, 2025
9dacd3d
Use builtins.open for file operations in tests
ashm-dev Nov 14, 2025
884758b
Update Lib/idlelib/iomenu.py
ashm-dev Nov 15, 2025
d6f54a2
Simplify file reload by removing cursor restoration
ashm-dev Nov 15, 2025
66ab8ce
Merge branch 'main' into idle
ashm-dev Nov 15, 2025
c10c5b9
Fix typo in 'File Not Found' message
ashm-dev Nov 15, 2025
7a88c34
Update error message in test for file reload
ashm-dev Nov 15, 2025
5614c28
fix import
ashm-dev Nov 15, 2025
3ce95d3
Merge remote-tracking branch 'origin/idle' into idle
ashm-dev Nov 15, 2025
545ba86
fix: move local imports to global
ashm-dev Nov 15, 2025
5f5e769
test_iomenu: use unittest.mock.patch and call_args
ashm-dev Nov 15, 2025
da37468
IDLE tests: use addCleanup for tempfile removal in test_iomenu
ashm-dev Nov 15, 2025
66e0495
remove comments
ashm-dev Nov 15, 2025
a154c9f
refactor(tests): Use mock.patch for messagebox dialogs
ashm-dev Nov 15, 2025
bd28bb4
add news
ashm-dev Nov 15, 2025
fe997ec
rewrite tests
ashm-dev Nov 15, 2025
0f1795b
Merge branch 'main' into idle
ashm-dev Nov 15, 2025
7421699
Update Doc/whatsnew/3.15.rst
ashm-dev Nov 15, 2025
21c5a9c
Update Misc/NEWS.d/next/IDLE/2025-11-14-20-58-55.gh-issue-44968.rL9dK…
ashm-dev Nov 15, 2025
ad17754
Update Doc/whatsnew/3.15.rst
ashm-dev Nov 15, 2025
8672683
Update Doc/whatsnew/3.15.rst
ashm-dev Nov 15, 2025
a1e94ca
fix
ashm-dev Nov 15, 2025
9485351
Merge remote-tracking branch 'origin/idle' into idle
ashm-dev Nov 15, 2025
1788b80
fix tsan
ashm-dev Nov 16, 2025
953dc8d
fix tests
ashm-dev Nov 16, 2025
a689fd1
Merge branch 'main' into idle
ashm-dev Nov 17, 2025
d27c71b
Merge branch 'main' into idle
ashm-dev Nov 17, 2025
a77a5c0
Merge branch 'main' into idle
ashm-dev Nov 18, 2025
18a30d2
Update Lib/idlelib/idle_test/test_iomenu.py
ashm-dev Nov 19, 2025
885f2cf
Update Lib/idlelib/idle_test/test_iomenu.py
ashm-dev Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions Lib/idlelib/idle_test/test_iomenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from idlelib import iomenu
import unittest
import builtins
from test.support import requires
from tkinter import Tk
from idlelib.editor import EditorWindow
Expand Down Expand Up @@ -57,6 +58,130 @@ def test_fixnewlines_end(self):
eq(text.get('1.0', 'end-1c'), 'a\n')
eq(fix(), 'a'+io.eol_convention)

def test_reload_no_file(self):
# Test reload when no file is associated
import tempfile
import os
from unittest.mock import Mock

io = self.io
# Ensure no filename is set
io.filename = None

# Mock the messagebox.showinfo
orig_showinfo = iomenu.messagebox.showinfo
showinfo_called = []
def mock_showinfo(*args, **kwargs):
showinfo_called.append((args, kwargs))
iomenu.messagebox.showinfo = mock_showinfo

try:
result = io.reload(None)
self.assertEqual(result, "break")
self.assertEqual(len(showinfo_called), 1)
self.assertIn("No File", showinfo_called[0][0])
finally:
iomenu.messagebox.showinfo = orig_showinfo

def test_reload_with_file(self):
# Test reload with an actual file
import tempfile
import os

io = self.io
text = io.editwin.text

# Create a temporary file
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as f:
f.write("# Original content\n")
temp_filename = f.name

try:
# Load the file
io.loadfile(temp_filename)
self.assertEqual(text.get('1.0', 'end-1c'), "# Original content\n")

# Modify the file content externally
with builtins.open(temp_filename, 'w') as f:
f.write("# Modified content\n")

# Reload should update the content
result = io.reload(None)
self.assertEqual(result, "break")
self.assertEqual(text.get('1.0', 'end-1c'), "# Modified content\n")
finally:
os.unlink(temp_filename)

def test_reload_with_unsaved_changes_cancel(self):
# Test reload with unsaved changes and user cancels
import tempfile
import os

io = self.io
text = io.editwin.text

# Create a temporary file
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as f:
f.write("# Original content\n")
temp_filename = f.name

try:
# Load the file
io.loadfile(temp_filename)

# Make unsaved changes
text.insert('end-1c', "\n# Unsaved change")
io.set_saved(False)

# Mock askokcancel to return False (cancel)
orig_askokcancel = iomenu.messagebox.askokcancel
iomenu.messagebox.askokcancel = lambda *args, **kwargs: False

try:
result = io.reload(None)
self.assertEqual(result, "break")
# Content should not change
self.assertIn("# Unsaved change", text.get('1.0', 'end-1c'))
finally:
iomenu.messagebox.askokcancel = orig_askokcancel
finally:
os.unlink(temp_filename)

def test_reload_with_unsaved_changes_confirm(self):
# Test reload with unsaved changes and user confirms
import tempfile
import os

io = self.io
text = io.editwin.text

# Create a temporary file
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as f:
f.write("# Original content\n")
temp_filename = f.name

try:
# Load the file
io.loadfile(temp_filename)

# Make unsaved changes
text.insert('end-1c', "\n# Unsaved change")
io.set_saved(False)

# Mock askokcancel to return True (confirm)
orig_askokcancel = iomenu.messagebox.askokcancel
iomenu.messagebox.askokcancel = lambda *args, **kwargs: True

try:
result = io.reload(None)
self.assertEqual(result, "break")
# Content should be reverted to original
self.assertEqual(text.get('1.0', 'end-1c'), "# Original content\n")
finally:
iomenu.messagebox.askokcancel = orig_askokcancel
finally:
os.unlink(temp_filename)


def _extension_in_filetypes(extension):
return any(
Expand Down
45 changes: 45 additions & 0 deletions Lib/idlelib/iomenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def __init__(self, editwin):
self.save_as)
self.__id_savecopy = self.text.bind("<<save-copy-of-window-as-file>>",
self.save_a_copy)
self.__id_reload = self.text.bind("<<reload-window>>", self.reload)
self.fileencoding = 'utf-8'
self.__id_print = self.text.bind("<<print-window>>", self.print_window)

Expand All @@ -40,6 +41,7 @@ def close(self):
self.text.unbind("<<save-window>>", self.__id_save)
self.text.unbind("<<save-window-as-file>>",self.__id_saveas)
self.text.unbind("<<save-copy-of-window-as-file>>", self.__id_savecopy)
self.text.unbind("<<reload-window>>", self.__id_reload)
self.text.unbind("<<print-window>>", self.__id_print)
# Break cycles
self.editwin = None
Expand Down Expand Up @@ -237,6 +239,49 @@ def save_a_copy(self, event):
self.updaterecentfileslist(filename)
return "break"

def reload(self, event):
"""Reload the file from disk, discarding any unsaved changes.

If the file has unsaved changes, ask the user to confirm.
"""
if not self.filename:
messagebox.showinfo(
"No File",
"This window has no associated file to reload.",
parent=self.text)
self.text.focus_set()
return "break"

if not self.get_saved():
confirm = messagebox.askokcancel(
title="Reload File",
message=f"Discard changes to {self.filename}?",
default=messagebox.CANCEL,
parent=self.text)
if not confirm:
self.text.focus_set()
return "break"

# Save cursor position
insert_pos = self.text.index("insert")
yview_pos = self.text.yview()

# Reload the file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Reload the file

Trivial comment

if self.loadfile(self.filename):
# Try to restore cursor position if the file still has that line
try:
self.text.mark_set("insert", insert_pos)
self.text.see("insert")
# Restore vertical scroll position
self.text.yview_moveto(yview_pos[0])
except Exception:
# If position doesn't exist anymore, go to top
self.text.mark_set("insert", "1.0")
self.text.see("insert")

self.text.focus_set()
return "break"

def writefile(self, filename):
text = self.fixnewlines()
chars = self.encode(text)
Expand Down
1 change: 1 addition & 0 deletions Lib/idlelib/mainmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
('_Save', '<<save-window>>'),
('Save _As...', '<<save-window-as-file>>'),
('Save Cop_y As...', '<<save-copy-of-window-as-file>>'),
('Re_load from Disk', '<<reload-window>>'),
None,
('Prin_t Window', '<<print-window>>'),
None,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add "Reload from Disk" menu item to IDLE's File menu. This allows users to
easily reload a file from disk, discarding any unsaved changes in the editor.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
easily reload a file from disk, discarding any unsaved changes in the editor.
reload a file from disk, discarding unsaved changes in the editor.

Remove unnecessary words

The feature is particularly useful when working with version control systems
or when external tools modify files. Patch by ashm-dev.
Loading