Skip to content
Open
Show file tree
Hide file tree
Changes from 20 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
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,14 @@ http.cookies
(Contributed by Nick Burns and Senthil Kumaran in :gh:`92936`.)


idlelib
Copy link
Member

Choose a reason for hiding this comment

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

@terryjreedy For IDLE changes, do we add it here or somewhere else in general?

-------

* Add a "Reload from Disk" item to the File menu. This allows discarding
unsaved changes and reloading the current version of the file from the disk.
(Contributed by Shamil Abdulaev in :gh:`44968`.)


inspect
-------

Expand Down
78 changes: 78 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
from unittest.mock import patch, mock_open
from test.support import requires
from tkinter import Tk
from idlelib.editor import EditorWindow
Expand Down Expand Up @@ -57,6 +58,83 @@ 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
io = self.io
io.filename = None

with patch.object(iomenu.messagebox, 'showinfo') as mock_showinfo:
result = io.reload(None)
self.assertEqual(result, "break")
mock_showinfo.assert_called_once()
args, kwargs = mock_showinfo.call_args
self.assertIn("File Not Found", args[0])

def test_reload_with_file(self):
# Test reload with an actual file
io = self.io
text = io.editwin.text
io.filename = "/dummy/path/test.py"

original_content = "# Original content\n"
modified_content = "# Modified content\n"

m = mock_open()
m.side_effect = [
mock_open(read_data=original_content).return_value,
mock_open(read_data=modified_content).return_value,
]

with patch('builtins.open', m):
io.loadfile(io.filename)
self.assertEqual(text.get('1.0', 'end-1c'), original_content)
result = io.reload(None)

self.assertEqual(result, "break")
self.assertEqual(text.get('1.0', 'end-1c'), modified_content)

def test_reload_with_unsaved_changes_cancel(self):
# Test reload with unsaved changes and user cancels
io = self.io
text = io.editwin.text
io.filename = "/dummy/path/test.py"
original_content = "# Original content\n"
unsaved_content = original_content + "\n# Unsaved change"

# Mock the initial file load.
with patch('builtins.open', mock_open(read_data=original_content)):
io.loadfile(io.filename)

text.insert('end', "\n# Unsaved change")
io.set_saved(False)

with patch('idlelib.iomenu.messagebox.askokcancel', return_value=False) as mock_ask:
Copy link
Member

Choose a reason for hiding this comment

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

Please wrap lines under 80 chars.

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
with patch('idlelib.iomenu.messagebox.askokcancel', return_value=False) as mock_ask:
with patch('idlelib.iomenu.messagebox.askokcancel',
return_value=False) as mock_ask:

result = io.reload(None)
self.assertEqual(result, "break")
# Content should not change.
self.assertEqual(text.get('1.0', 'end-1c'), unsaved_content)
mock_ask.assert_called_once()

def test_reload_with_unsaved_changes_confirm(self):
# Test reload with unsaved changes and user confirms
io = self.io
text = io.editwin.text
io.filename = "/dummy/path/test.py"
original_content = "# Original content\n"

with patch('builtins.open', mock_open(read_data=original_content)):
io.loadfile(io.filename)
text.insert('end', "\n# Unsaved change")
io.set_saved(False)

with patch('idlelib.iomenu.messagebox.askokcancel', return_value=True) as mock_ask:
result = io.reload(None)

self.assertEqual(result, "break")
# Content should be reverted to original.
self.assertEqual(text.get('1.0', 'end-1c'), original_content)
mock_ask.assert_called_once()


def _extension_in_filetypes(extension):
return any(
Expand Down
31 changes: 31 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,35 @@ 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(
"File Not Found",
"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"

# 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

self.loadfile(self.filename)

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,3 @@
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

Patch by Shamil Abdulaev.
Loading