Skip to content
Merged
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
3 changes: 2 additions & 1 deletion Include/internal/pycore_magic_number.h
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ Known values:
Python 3.16a1 3703 (Replace DELETE_GLOBAL with PUSH_NULL; STORE_GLOBAL)
Python 3.16a1 3704 (Replace DELETE_ATTR with PUSH_NULL; STORE_ATTR)
Python 3.16a1 3705 (Add INTRINSIC_ADD_CONDITIONAL_ANNOTATION)
Python 3.16a1 3706 (Add INTRINSIC_MATCH_CLASS_ISINSTANCE)

Python 3.17 will start with 3750

Expand All @@ -312,7 +313,7 @@ Known values:

*/

#define PYC_MAGIC_NUMBER 3705
#define PYC_MAGIC_NUMBER 3706
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
(little-endian) and then appending b'\r\n'. */
#define PYC_MAGIC_NUMBER_TOKEN \
Expand Down
37 changes: 34 additions & 3 deletions Lib/idlelib/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,19 +1120,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()
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 @@ -321,5 +325,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)
14 changes: 10 additions & 4 deletions Lib/idlelib/idle_test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ def __eq__(self, other):
('int.reel', AttributeError,
"type object 'int' has no attribute 'reel'. "
"Did you mean '.real' instead of '.reel'?\n"), # More in 3.15.
(r'raise NameError("123\n456")', NameError, "123\n456\n"),
)

@force_not_colorized
def test_get_message(self):
for code, exc, msg in self.data:
with self.subTest(code=code):
try:
eval(compile(code, '', 'eval'))
exec(compile(code, '', 'exec'))
except exc:
typ, val, tb = sys.exc_info()
actual = run.get_message_lines(typ, val, tb)[0]
Expand All @@ -68,15 +69,20 @@ def test_get_message(self):
new_callable=lambda: (lambda t, e: None))
def test_get_multiple_message(self, mock):
d = self.data
data2 = ((d[0], d[1]), (d[1], d[2]), (d[2], d[0]))
data2 = ((d[0], d[1]),
(d[1], d[2]),
(d[2], d[3]),
(d[3], d[0]),
(d[1], d[3]),
(d[0], d[2]))
subtests = 0
for (code1, exc1, msg1), (code2, exc2, msg2) in data2:
with self.subTest(codes=(code1,code2)):
try:
eval(compile(code1, '', 'eval'))
exec(compile(code1, '', 'exec'))
except exc1:
try:
eval(compile(code2, '', 'eval'))
exec(compile(code2, '', 'exec'))
except exc2:
with captured_stderr() as output:
run.print_exception()
Expand Down
12 changes: 2 additions & 10 deletions Lib/idlelib/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
f'''{sys.executable} -c "__import__('idlelib.run').run.main()"'''
'.run' is needed because __import__ returns idlelib, not idlelib.run.
"""
import contextlib
import functools
import io
import linecache
Expand Down Expand Up @@ -244,15 +243,8 @@ def show_socket_error(err, address):


def get_message_lines(typ, exc, tb):
"Return line composing the exception message."
if typ in (AttributeError, NameError):
# 3.10+ hints are not directly accessible from python (#44026).
err = io.StringIO()
with contextlib.redirect_stderr(err):
sys.__excepthook__(typ, exc, tb)
return [err.getvalue().split("\n")[-2] + "\n"]
else:
return traceback.format_exception_only(typ, exc)
"Return lines of the exception message, with any suggestion."
return list(traceback.TracebackException(typ, exc, tb).format_exception_only())


def print_exception():
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1424,6 +1424,31 @@ def test_reversed_dict_after_clear_and_restore(self):
for it in iterators:
self.assertEqual(list(it), [])

def test_reversed_dict_keys_changed_during_iteration(self):
d = dict.fromkeys(range(10))
for i in range(7):
del d[i]

iterators = (
reversed(d),
reversed(d.keys()),
reversed(d.values()),
reversed(d.items()),
)
for it in iterators:
next(it)

# Same size as before, but with different keys below
# the iterators' current position.
d.clear()
d.update(dict.fromkeys(range(10)))
for i in range(3, 10):
del d[i]

for it in iterators:
with self.assertRaisesRegex(RuntimeError, 'keys changed'):
list(it)

def test_dict_copy_order(self):
# bpo-34320
od = collections.OrderedDict([('a', 1), ('b', 2)])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Reverse iterators over a :class:`dict` and its views now raise
:exc:`RuntimeError` if the dictionary's keys change during iteration, like
forward iterators, instead of yielding entries for the new keys.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix display of :exc:`NameError` and :exc:`AttributeError` with multi-line message.
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.
6 changes: 6 additions & 0 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -6303,6 +6303,12 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self)
value = entry_ptr->me_value;
}
}
// We found an element, but did not expect it
if (di->len == 0) {
PyErr_SetString(PyExc_RuntimeError,
"dictionary keys changed during iteration");
goto fail;
}
di->di_pos = i-1;
di->len--;

Expand Down
Loading