Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 34 additions & 3 deletions Lib/idlelib/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,19 +1116,50 @@ def _close(self):

def last_mtime(self):
file = self.io.filename
return os.path.getmtime(file) if file else 0
if not file:
return None
try:
return os.path.getmtime(file)
except OSError:
# File is gone or cannot be stat'ed.
return None

def focus_in_event(self, event):
mtime = self.last_mtime()
if self.mtime != mtime:
if mtime == self.mtime:
return
if self.mtime is not None and mtime is None:
# The file was there and is now gone; reloading cannot work.
self.deleted_file_event(event)
else:
self.mtime = mtime
if self. askyesno(
if self.askyesno(
'Reload', '"%s"\n\nThis script has been modified by another program.'
'\nDo you want to reload it?' % self.io.filename, parent=self.text):
self.io.loadfile(self.io.filename)
else:
self.set_saved(False)

def deleted_file_event(self, event):
# The file was deleted or renamed while open; ask what to do with the
# buffer instead of offering a reload that could only fail. Forget the
# old mtime before showing the dialog so a FocusIn delivered while this
# dialog (or a Close/Save As sub-dialog) is open does not reopen it; a
# successful Save As restores it via set_saved(True).
self.mtime = None
dialog = simpledialog.SimpleDialog(
self.text,
title='File Deleted',
text='"%s"\n\nThis file no longer exists.' % self.io.filename,
buttons=('Close', 'Save As', 'Ignore'),
default=1,
cancel=2)
choice = dialog.go()
Comment thread
serhiy-storchaka marked this conversation as resolved.
if choice == 0:
self.close()
elif choice == 1:
self.io.save_as(event)

def load_extensions(self):
self.extensions = {}
self.load_standard_extensions()
Expand Down
80 changes: 80 additions & 0 deletions Lib/idlelib/idle_test/test_editor.py
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
Expand Down Expand Up @@ -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))

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)
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.
Loading