Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
100 changes: 91 additions & 9 deletions Lib/idlelib/idle_test/test_iomenu.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"Test , coverage 17%."

from idlelib import iomenu
import builtins
import os
import tempfile
import unittest
from unittest.mock import patch

from test.support import requires
from tkinter import Tk

from idlelib import iomenu, util
from idlelib.editor import EditorWindow
from idlelib import util
from idlelib.idle_test.mock_idle import Func

# Fail if either tokenize.open and t.detect_encoding does not exist.
# These are used in loadfile and encode.
# Also used in pyshell.MI.execfile and runscript.tabnanny.
from tokenize import open, detect_encoding
from tokenize import open as tokenize_open, detect_encoding
# Remove when we have proper tests that use both.


Expand All @@ -36,6 +39,14 @@ def tearDownClass(cls):
cls.root.destroy()
del cls.root

def _create_tempfile(self, content: str) -> str:
Copy link
Member

Choose a reason for hiding this comment

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

Don't use type annotations.

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
def _create_tempfile(self, content: str) -> str:
def _create_tempfile(self, content):

fd, filename = tempfile.mkstemp(suffix='.py')
os.close(fd)
self.addCleanup(os.unlink, filename)
with builtins.open(filename, 'w', encoding='utf-8') as f:
f.write(content)
return filename
Comment on lines +43 to +48
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
fd, filename = tempfile.mkstemp(suffix='.py')
os.close(fd)
self.addCleanup(os.unlink, filename)
with builtins.open(filename, 'w', encoding='utf-8') as f:
f.write(content)
return filename
filename = tempfile.mktemp()
self.addCleanup(os_helper.unlink, filename)
with open(filename, "w") as f:
f.write(content)
return filename

We don't care about secure temporary file for a test.


def test_init(self):
self.assertIs(self.io.editwin, self.editwin)

Expand All @@ -45,17 +56,88 @@ def test_fixnewlines_end(self):
fix = io.fixnewlines
text = io.editwin.text

# Make the editor temporarily look like Shell.
self.editwin.interp = None
shelltext = '>>> if 1'
self.editwin.get_prompt_text = Func(result=shelltext)
eq(fix(), shelltext) # Get... call and '\n' not added.
eq(fix(), shelltext) # Get... call and '\n' not added.
Copy link
Member

Choose a reason for hiding this comment

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

Ok, this will be the last time I'm going to say this but please never change unrelated code. Do not use an autoformatter, or configure your IDE so that it doesn't autoformat on save, or ask your LLM not to do any change.

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
eq(fix(), shelltext) # Get... call and '\n' not added.
eq(fix(), shelltext) # Get... call and '\n' not added.

del self.editwin.interp, self.editwin.get_prompt_text

text.insert(1.0, 'a')
eq(fix(), 'a'+io.eol_convention)
eq(fix(), 'a' + io.eol_convention)
eq(text.get('1.0', 'end-1c'), 'a\n')
eq(fix(), 'a'+io.eol_convention)
eq(fix(), 'a' + io.eol_convention)

def test_reload_no_file(self):
io = self.io
io.filename = None

with patch('idlelib.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])
Comment on lines +74 to +79
Copy link
Member

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.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])
with patch('idlelib.iomenu.messagebox.showinfo') as mock_showinfo:
result = io.reload(None)
self.assertEqual(result, "break")
mock_showinfo.assert_called_once()
args, _ = mock_showinfo.call_args
self.assertIn("File Not Found", args[0])


def test_reload_with_file(self):
io = self.io
text = io.editwin.text
original_content = "# Original content\n"
modified_content = "# Modified content\n"

filename = self._create_tempfile(original_content)
io.filename = filename

with patch('idlelib.iomenu.messagebox.showerror') as mock_showerror:
io.loadfile(io.filename)
self.assertEqual(text.get('1.0', 'end-1c'), original_content)

with builtins.open(filename, 'w', encoding='utf-8') as f:
Copy link
Member

Choose a reason for hiding this comment

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

Why using builtins.open now that you have tokenize_open?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure tokenize_open can only open the file in read mode here

Copy link
Member

Choose a reason for hiding this comment

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

It's not about tokenize_open. It's about just using the plain open. There is no need to use builtins.open now that open is not shadowed.

f.write(modified_content)

result = io.reload(None)

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

def test_reload_with_unsaved_changes_cancel(self):
io = self.io
text = io.editwin.text
original_content = "# Original content\n"
unsaved_content = original_content + "\n# Unsaved change"

filename = self._create_tempfile(original_content)
io.filename = filename
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")
self.assertEqual(text.get('1.0', 'end-1c'), unsaved_content)
mock_ask.assert_called_once()

def test_reload_with_unsaved_changes_confirm(self):
io = self.io
text = io.editwin.text
original_content = "# Original content\n"

filename = self._create_tempfile(original_content)
io.filename = filename
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")
self.assertEqual(text.get('1.0', 'end-1c'), original_content)
mock_ask.assert_called_once()


def _extension_in_filetypes(extension):
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>>'),
('_Reload 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