[3.14] gh-153480: Stop IDLE crashing when a file open in the editor i… · python/cpython@cbc7753 · GitHub
Skip to content

Commit cbc7753

Browse files
miss-islingtontonghuarootserhiy-storchaka
authored
[3.14] gh-153480: Stop IDLE crashing when a file open in the editor is deleted (GH-153481) (#158262)
gh-153480: Stop IDLE crashing when a file open in the editor is deleted (GH-153481) EditorWindow.focus_in_event calls last_mtime on every <FocusIn> event, which passed the open file path to os.path.getmtime with no guard. When another program deleted or renamed the file, refocusing the IDLE window let a FileNotFoundError escape the Tk event handler. last_mtime now returns the last known mtime when the path is inaccessible, so refocusing no longer raises and does not prompt to reload a file that is gone. See PR discussion for more. --------- (cherry picked from commit 6af40a6) Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent c2d1138 commit cbc7753

3 files changed

Lines changed: 118 additions & 3 deletions

File tree

‎Lib/idlelib/editor.py‎

Lines changed: 34 additions & 3 deletions

‎Lib/idlelib/idle_test/test_editor.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"Test editor, coverage 53%."
22

33
from idlelib import editor
4+
import os
5+
import tempfile
6+
import types
47
import unittest
8+
from pathlib import Path
59
from collections import namedtuple
610
from unittest import mock
711
from test.support import requires
@@ -321,5 +325,81 @@ def test_rmenu_check_copy(self):
321325
eq(self.window.rmenu_check_cut(), 'normal')
322326

323327

328+
class LastMtimeTest(unittest.TestCase):
329+
# Exercise last_mtime as an unbound method on a stub; no GUI needed.
330+
331+
def test_existing_file_returns_mtime(self):
332+
with tempfile.TemporaryDirectory() as d:
333+
p = os.path.join(d, 'f.py')
334+
Path(p).touch()
335+
stub = types.SimpleNamespace(io=types.SimpleNamespace(filename=p))
336+
self.assertEqual(Editor.last_mtime(stub), os.path.getmtime(p))
337+
338+
def test_deleted_file_returns_none(self):
339+
with tempfile.TemporaryDirectory() as d:
340+
p = os.path.join(d, 'gone.py')
341+
Path(p).touch()
342+
os.remove(p)
343+
stub = types.SimpleNamespace(io=types.SimpleNamespace(filename=p))
344+
self.assertIsNone(Editor.last_mtime(stub))
345+
346+
def test_not_yet_created_filename(self):
347+
# __init__ calls last_mtime() before self.mtime is set, so last_mtime()
348+
# must not read self.mtime (the stub has no mtime attribute).
349+
stub = types.SimpleNamespace(
350+
io=types.SimpleNamespace(filename='/no/such/file.py'))
351+
self.assertIsNone(Editor.last_mtime(stub))
352+
353+
def test_no_filename_returns_none(self):
354+
stub = types.SimpleNamespace(io=types.SimpleNamespace(filename=None))
355+
self.assertIsNone(Editor.last_mtime(stub))
356+
357+
358+
class DeletedFileEventTest(unittest.TestCase):
359+
# Exercise the deleted-file handling as unbound methods; dialog is mocked.
360+
361+
def make_stub(self):
362+
return types.SimpleNamespace(
363+
mtime=1.0,
364+
text=None,
365+
io=types.SimpleNamespace(filename='/gone.py', save_as=mock.Mock()),
366+
close=mock.Mock(),
367+
set_saved=mock.Mock(),
368+
deleted_file_event=mock.Mock(),
369+
askyesno=mock.Mock(),
370+
last_mtime=lambda: None)
371+
372+
def test_focus_in_routes_deleted_to_dialog(self):
373+
stub = self.make_stub()
374+
Editor.focus_in_event(stub, 'event')
375+
stub.deleted_file_event.assert_called_once_with('event')
376+
stub.askyesno.assert_not_called()
377+
378+
def _run_choice(self, choice):
379+
stub = self.make_stub()
380+
with mock.patch.object(editor.simpledialog, 'SimpleDialog') as SD:
381+
SD.return_value.go.return_value = choice
382+
Editor.deleted_file_event(stub, 'event')
383+
return stub
384+
385+
def test_close_choice_closes_window(self):
386+
stub = self._run_choice(0)
387+
self.assertTrue(stub.close.called)
388+
# mtime is cleared before Close so the queued FocusIn does not reprompt.
389+
self.assertIsNone(stub.mtime)
390+
391+
def test_save_as_choice_clears_mtime_and_saves(self):
392+
stub = self._run_choice(1)
393+
stub.io.save_as.assert_called_once_with('event')
394+
# A cancelled Save As leaves mtime None so it does not reprompt.
395+
self.assertIsNone(stub.mtime)
396+
397+
def test_ignore_choice_clears_mtime(self):
398+
stub = self._run_choice(2)
399+
self.assertIsNone(stub.mtime)
400+
stub.io.save_as.assert_not_called()
401+
stub.set_saved.assert_not_called()
402+
403+
324404
if __name__ == '__main__':
325405
unittest.main(verbosity=2)
Lines changed: 4 additions & 0 deletions

0 commit comments

Comments
 (0)