-
-
Notifications
You must be signed in to change notification settings - Fork 37k
gh-153480: Stop IDLE crashing when a file open in the editor is deleted #153481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
terryjreedy
merged 6 commits into
python:main
from
tonghuaroot:idlelib-editor-last-mtime-oserror
Sep 27, 2026
+118
−3
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5a78710
gh-153480: Stop IDLE crashing when a file open in the editor is deleted
tonghuaroot 2f20bc2
Merge branch 'main' into idlelib-editor-last-mtime-oserror
serhiy-storchaka a57c59b
gh-153480: Return None from last_mtime() and handle a deleted file
tonghuaroot e84056b
Fix end-of-file newline in editor.py and test_editor.py
tonghuaroot ceffab2
gh-153480: Clear mtime in the deleted-file dialog before Save As
tonghuaroot 9cabce6
gh-153480: Clear mtime before the deleted-file dialog for all choices
tonghuaroot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,11 @@ | ||||||||||||||||
| "Test editor, coverage 53%." | ||||||||||||||||
|
|
||||||||||||||||
| from idlelib import editor | ||||||||||||||||
| import os | ||||||||||||||||
| import tempfile | ||||||||||||||||
| import types | ||||||||||||||||
| import unittest | ||||||||||||||||
| from pathlib import Path | ||||||||||||||||
| from collections import namedtuple | ||||||||||||||||
| from unittest import mock | ||||||||||||||||
| from test.support import requires | ||||||||||||||||
|
|
@@ -301,5 +305,81 @@ def test_rmenu_check_copy(self): | |||||||||||||||
| eq(self.window.rmenu_check_cut(), 'normal') | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| class LastMtimeTest(unittest.TestCase): | ||||||||||||||||
| # Exercise last_mtime as an unbound method on a stub; no GUI needed. | ||||||||||||||||
|
|
||||||||||||||||
| def test_existing_file_returns_mtime(self): | ||||||||||||||||
| with tempfile.TemporaryDirectory() as d: | ||||||||||||||||
| p = os.path.join(d, 'f.py') | ||||||||||||||||
| Path(p).touch() | ||||||||||||||||
| stub = types.SimpleNamespace(io=types.SimpleNamespace(filename=p)) | ||||||||||||||||
| self.assertEqual(Editor.last_mtime(stub), os.path.getmtime(p)) | ||||||||||||||||
|
|
||||||||||||||||
| def test_deleted_file_returns_none(self): | ||||||||||||||||
| with tempfile.TemporaryDirectory() as d: | ||||||||||||||||
| p = os.path.join(d, 'gone.py') | ||||||||||||||||
| Path(p).touch() | ||||||||||||||||
| os.remove(p) | ||||||||||||||||
| stub = types.SimpleNamespace(io=types.SimpleNamespace(filename=p)) | ||||||||||||||||
| self.assertIsNone(Editor.last_mtime(stub)) | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+318
to
+325
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Serhiy, is simply deleting one of the two specified method what you requested?
Suggested change
I am fine with not replacing the working code. |
||||||||||||||||
| def test_not_yet_created_filename(self): | ||||||||||||||||
| # __init__ calls last_mtime() before self.mtime is set, so last_mtime() | ||||||||||||||||
| # must not read self.mtime (the stub has no mtime attribute). | ||||||||||||||||
| stub = types.SimpleNamespace( | ||||||||||||||||
| io=types.SimpleNamespace(filename='/no/such/file.py')) | ||||||||||||||||
| self.assertIsNone(Editor.last_mtime(stub)) | ||||||||||||||||
|
|
||||||||||||||||
| def test_no_filename_returns_none(self): | ||||||||||||||||
| stub = types.SimpleNamespace(io=types.SimpleNamespace(filename=None)) | ||||||||||||||||
| self.assertIsNone(Editor.last_mtime(stub)) | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| class DeletedFileEventTest(unittest.TestCase): | ||||||||||||||||
| # Exercise the deleted-file handling as unbound methods; dialog is mocked. | ||||||||||||||||
|
|
||||||||||||||||
| def make_stub(self): | ||||||||||||||||
| return types.SimpleNamespace( | ||||||||||||||||
| mtime=1.0, | ||||||||||||||||
| text=None, | ||||||||||||||||
| io=types.SimpleNamespace(filename='/gone.py', save_as=mock.Mock()), | ||||||||||||||||
| close=mock.Mock(), | ||||||||||||||||
| set_saved=mock.Mock(), | ||||||||||||||||
| deleted_file_event=mock.Mock(), | ||||||||||||||||
| askyesno=mock.Mock(), | ||||||||||||||||
| last_mtime=lambda: None) | ||||||||||||||||
|
|
||||||||||||||||
| def test_focus_in_routes_deleted_to_dialog(self): | ||||||||||||||||
| stub = self.make_stub() | ||||||||||||||||
| Editor.focus_in_event(stub, 'event') | ||||||||||||||||
| stub.deleted_file_event.assert_called_once_with('event') | ||||||||||||||||
| stub.askyesno.assert_not_called() | ||||||||||||||||
|
|
||||||||||||||||
| def _run_choice(self, choice): | ||||||||||||||||
| stub = self.make_stub() | ||||||||||||||||
| with mock.patch.object(editor.simpledialog, 'SimpleDialog') as SD: | ||||||||||||||||
| SD.return_value.go.return_value = choice | ||||||||||||||||
| Editor.deleted_file_event(stub, 'event') | ||||||||||||||||
| return stub | ||||||||||||||||
|
|
||||||||||||||||
| def test_close_choice_closes_window(self): | ||||||||||||||||
| stub = self._run_choice(0) | ||||||||||||||||
| self.assertTrue(stub.close.called) | ||||||||||||||||
| # mtime is cleared before Close so the queued FocusIn does not reprompt. | ||||||||||||||||
| self.assertIsNone(stub.mtime) | ||||||||||||||||
|
|
||||||||||||||||
| def test_save_as_choice_clears_mtime_and_saves(self): | ||||||||||||||||
| stub = self._run_choice(1) | ||||||||||||||||
| stub.io.save_as.assert_called_once_with('event') | ||||||||||||||||
| # A cancelled Save As leaves mtime None so it does not reprompt. | ||||||||||||||||
| self.assertIsNone(stub.mtime) | ||||||||||||||||
|
|
||||||||||||||||
| def test_ignore_choice_clears_mtime(self): | ||||||||||||||||
| stub = self._run_choice(2) | ||||||||||||||||
| self.assertIsNone(stub.mtime) | ||||||||||||||||
| stub.io.save_as.assert_not_called() | ||||||||||||||||
| stub.set_saved.assert_not_called() | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| if __name__ == '__main__': | ||||||||||||||||
| unittest.main(verbosity=2) | ||||||||||||||||
4 changes: 4 additions & 0 deletions
4
Misc/NEWS.d/next/IDLE/2026-07-10-19-41-00.gh-issue-153480.EdMt1x.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Fix IDLE failing to start when opening a file which does not exist yet. | ||
| Fix a traceback when a file open in the IDLE editor is deleted by another | ||
| program; IDLE now asks whether to close the window, save the file elsewhere, | ||
| or ignore it. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.