From a3e389d46d9ce9e60d2eb6fdf2f903b5f90b0f4e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 21 Sep 2026 14:03:09 -0400 Subject: [PATCH 1/4] gh-92347: Document pitfalls of `ctypes.pointer` iteration (GH-157801) --- Doc/library/ctypes.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 8946ee7a02da48..1d33f593fffe94 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -997,6 +997,15 @@ Generally you only use this feature if you receive a pointer from a C function, and you *know* that the pointer actually points to an array instead of a single item. +.. warning:: + + Because pointer objects support subscription, they implicitly support + :term:`iteration `. Unless doing this in a controlled manner, + such as by manually calling :func:`next` on a :func:`pointer` iterator, this + will typically lead to infinite loops or crashes, because ctypes has no way + of knowing when to stop iteration. In other words, a ``pointer`` iterator + will infinitely yield arbitrary memory. + Behind the scenes, the :func:`pointer` function does more than simply create pointer instances, it has to create pointer *types* first. This is done with the :func:`POINTER` function, which accepts any :mod:`!ctypes` type, and returns a From 98169145419470d243231c5d4992d006a971a8e3 Mon Sep 17 00:00:00 2001 From: Vasiliy Kiryanov Date: Mon, 21 Sep 2026 16:01:23 -0400 Subject: [PATCH 2/4] gh-141540: Quote unconverted data in `strptime` error messages (#157832) Co-authored-by: Stan Ulbrych --- Lib/_strptime.py | 16 ++++++++-------- Lib/test/test_strptime.py | 11 +++++++++-- ...026-09-19-20-31-58.gh-issue-141540.phc605.rst | 3 +++ 3 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-19-20-31-58.gh-issue-141540.phc605.rst diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 59ac96745aa15e..3311ec88871469 100644 --- a/Lib/_strptime.py +++ b/Lib/_strptime.py @@ -564,15 +564,15 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): del err bad_directive = bad_directive.replace('\\s', '') if not bad_directive: - raise ValueError("stray %% in format '%s'" % format) from None + raise ValueError(f"stray % in format {format!r}") from None bad_directive = bad_directive.replace('\\', '', 1) - raise ValueError("'%s' is a bad directive in format '%s'" % - (bad_directive, format)) from None + raise ValueError(f"{bad_directive!r} is a bad directive " + f"in format {format!r}") from None _regex_cache[format] = format_regex found = format_regex.match(data_string) if not found: - raise ValueError("time data %r does not match format %r" % - (data_string, format)) + raise ValueError(f"time data {data_string!r} does not match " + f"format {format!r}") if len(data_string) != found.end(): rest = data_string[found.end():] # Specific check for '%:z' directive @@ -582,9 +582,9 @@ def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"): and rest[0] != ":" ): raise ValueError( - f"Missing colon in %:z before '{rest}', got '{data_string}'" + f"Missing colon in %:z before {rest!r}, got {data_string!r}" ) - raise ValueError("unconverted data remains: %s" % rest) + raise ValueError(f"unconverted data remains: {rest!r}") iso_year = year = None month = day = 1 @@ -700,7 +700,7 @@ def parse_int(s): z = z[:3] + z[4:] if len(z) > 5: if z[5] != ':': - msg = f"Inconsistent use of : in {found_dict[group_key]}" + msg = f"Inconsistent use of : in {found_dict[group_key]!r}" raise ValueError(msg) z = z[:5] + z[6:] hours = int(z[1:3]) diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index e95cc6db170e24..d70837ea63f547 100644 --- a/Lib/test/test_strptime.py +++ b/Lib/test/test_strptime.py @@ -236,7 +236,8 @@ def test_ValueError(self): directive = bad_format[1:].rstrip() with (self.subTest(format=bad_format), self.assertRaisesRegex(ValueError, - f"'{re.escape(directive)}' is a bad directive in format ")): + f"{re.escape(repr(directive))} is a bad directive " + f"in format ")): _strptime._strptime_time("2005", bad_format) msg_week_no_year_or_weekday = r"ISO week directive '%V' must be used with " \ @@ -303,6 +304,11 @@ def test_unconverteddata(self): # Check ValueError is raised when there is unconverted data self.assertRaises(ValueError, _strptime._strptime_time, "10 12", "%m") + # gh-141540: a trailing newline must be visible in the message + with self.assertRaisesRegex(ValueError, + r"unconverted data remains: '\\n'"): + _strptime._strptime_time("2001-02-03\n", "%Y-%m-%d") + def roundtrip(self, fmt, position, time_tuple=None): """Helper fxn in testing.""" if time_tuple is None: @@ -451,7 +457,8 @@ def test_bad_offset(self): with self.assertRaises(ValueError) as err: _strptime._strptime("-01:3030", "%z") - self.assertEqual("Inconsistent use of : in -01:3030", str(err.exception)) + self.assertEqual("Inconsistent use of : in '-01:3030'", + str(err.exception)) with self.assertRaises(ValueError) as err: _strptime._strptime("-01:3030", "%:z") self.assertEqual("Missing colon in %:z before '30', got '-01:3030'", diff --git a/Misc/NEWS.d/next/Library/2026-09-19-20-31-58.gh-issue-141540.phc605.rst b/Misc/NEWS.d/next/Library/2026-09-19-20-31-58.gh-issue-141540.phc605.rst new file mode 100644 index 00000000000000..1b38ffd26ea20c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-19-20-31-58.gh-issue-141540.phc605.rst @@ -0,0 +1,3 @@ +Quote the unconverted data and the format in the error messages of +:func:`time.strptime` and :meth:`datetime.datetime.strptime` so that +whitespace such as a trailing newline is visible. From 02fae7a9594953c1d8b298b08d6faa99a5c18975 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 21 Sep 2026 13:24:38 -0700 Subject: [PATCH 3/4] GH-157923: Add support for WASI SDK 34 (#157924) --- .github/workflows/reusable-wasi.yml | 2 +- .../next/Build/2026-09-21-10-50-31.gh-issue-157923.OffPYn.rst | 1 + Platforms/WASI/config.site-wasm32-wasi | 4 ++++ Platforms/WASI/config.toml | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Build/2026-09-21-10-50-31.gh-issue-157923.OffPYn.rst diff --git a/.github/workflows/reusable-wasi.yml b/.github/workflows/reusable-wasi.yml index b0dda62d0b291c..fd263e8aafd8f5 100644 --- a/.github/workflows/reusable-wasi.yml +++ b/.github/workflows/reusable-wasi.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-26.04-arm timeout-minutes: 60 env: - WASMTIME_VERSION: 38.0.3 + WASMTIME_VERSION: 48.0.2 CROSS_BUILD_WASI: cross-build/wasm32-wasip1 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/Misc/NEWS.d/next/Build/2026-09-21-10-50-31.gh-issue-157923.OffPYn.rst b/Misc/NEWS.d/next/Build/2026-09-21-10-50-31.gh-issue-157923.OffPYn.rst new file mode 100644 index 00000000000000..f1126adb1a069e --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-09-21-10-50-31.gh-issue-157923.OffPYn.rst @@ -0,0 +1 @@ +Support WASI SDK 34. diff --git a/Platforms/WASI/config.site-wasm32-wasi b/Platforms/WASI/config.site-wasm32-wasi index c5d8b3e205db26..88c54907880e73 100644 --- a/Platforms/WASI/config.site-wasm32-wasi +++ b/Platforms/WASI/config.site-wasm32-wasi @@ -57,3 +57,7 @@ ac_cv_func_fchmod=no ac_cv_func_fchmodat=no ac_cv_func_statvfs=no ac_cv_func_fstatvfs=no + +# WASI SDK 34 declares some things that simply error out. +ac_cv_func_pipe2=no +ac_cv_func_pthread_getcpuclockid=no diff --git a/Platforms/WASI/config.toml b/Platforms/WASI/config.toml index 6a6d5713ee9673..151c528c87670d 100644 --- a/Platforms/WASI/config.toml +++ b/Platforms/WASI/config.toml @@ -2,5 +2,5 @@ # This allows for blanket copying of the WASI build code between supported # Python versions. [targets] -wasi-sdk = 33 +wasi-sdk = 34 host-triple = "wasm32-wasip1" From 212e6035133957a66f1a823e012b4dc2a5158ce8 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 22 Sep 2026 00:00:06 +0300 Subject: [PATCH 4/4] gh-75234: Fix keyboard selection in the lists of IDLE Settings (#157588) The Up and Down keys move the selection in the help sources and key bindings lists, but not the anchor. So the buttons that act on the selected item stayed disabled, and acted on the anchored item instead of the selected one. Move the anchor on the key events, as the font list already does. Co-Authored-By: Claude Opus 5 (1M context) * Select the first key binding when the list is loaded --------- Co-authored-by: Claude Opus 5 (1M context) --- Lib/idlelib/configdialog.py | 41 ++++++++++++++----- Lib/idlelib/idle_test/test_configdialog.py | 41 +++++++++++++++++-- ...6-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst | 2 + 3 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2026-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 4c94d9be69e95e..a5d1aaeb5a946f 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -1146,8 +1146,8 @@ def create_page_keys(self): selected keyset. The keybindings are loaded in load_keys_list() and are pairs of (event, [keys]) where keys can be a list of one or more key combinations to bind to the same event. - Mouse button 1 click invokes on_bindingslist_select(), which - allows button_new_keys to be clicked. + Mouse button 1 click or Up or Down key invokes + on_bindingslist_select(), which allows button_new_keys to be clicked. So, an item is selected in listbindings, which activates button_new_keys, and clicking button_new_keys calls function @@ -1221,9 +1221,12 @@ def create_page_keys(self): scroll_target_y = Scrollbar(frame_target) scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) self.bindingslist = Listbox( - frame_target, takefocus=FALSE, exportselection=FALSE) + frame_target, takefocus=True, exportselection=FALSE) self.bindingslist.bind('', self.on_bindingslist_select) + self.bindingslist.bind('', self.on_bindingslist_select) + self.bindingslist.bind('', + self.on_bindingslist_select) scroll_target_y['command'] = self.bindingslist.yview scroll_target_x['command'] = self.bindingslist.xview self.bindingslist['yscrollcommand'] = scroll_target_y.set @@ -1427,7 +1430,14 @@ def save_as_new_key_set(self): self.create_new_key_set(new_keys_name) def on_bindingslist_select(self, event): - "Activate button to assign new keys to selected action." + """Activate button to assign new keys to selected action. + + Event can result from either mouse click or Up or Down key. + The keys move the selection, but not the anchor used by + get_new_keys and var_changed_keybinding. + """ + if event.type.name == 'KeyRelease': + self.bindingslist.selection_anchor(ACTIVE) self.button_new_keys.state(('!disabled',)) def create_new_key_set(self, new_key_set_name): @@ -1465,9 +1475,8 @@ def load_keys_list(self, keyset_name): An action/key binding can be selected to change the key binding. """ - reselect = False + list_index = 0 if self.bindingslist.curselection(): - reselect = True list_index = self.bindingslist.index(ANCHOR) keyset = idleConf.GetKeySet(keyset_name) # 'set' is dict mapping virtual event to list of key events. @@ -1482,10 +1491,11 @@ def load_keys_list(self, keyset_name): if bind_name in changes['keys'][keyset_name]: key = changes['keys'][keyset_name][bind_name] self.bindingslist.insert(END, bind_name+' - '+key) - if reselect: - self.bindingslist.see(list_index) - self.bindingslist.select_set(list_index) - self.bindingslist.select_anchor(list_index) + self.bindingslist.see(list_index) + self.bindingslist.select_set(list_index) + self.bindingslist.select_anchor(list_index) + self.bindingslist.activate(list_index) + self.button_new_keys.state(('!disabled',)) @staticmethod def save_new_key_set(keyset_name, keyset): @@ -2124,6 +2134,8 @@ def create_frame_help(self): scroll_helplist['command'] = self.helplist.yview self.helplist['yscrollcommand'] = scroll_helplist.set self.helplist.bind('', self.help_source_selected) + self.helplist.bind('', self.help_source_selected) + self.helplist.bind('', self.help_source_selected) frame_buttons = Frame(self) self.button_helplist_edit = Button( @@ -2146,7 +2158,14 @@ def create_frame_help(self): self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) def help_source_selected(self, event): - "Handle event for selecting additional help." + """Handle event for selecting additional help. + + Event can result from either mouse click or Up or Down key. + The keys move the selection, but not the anchor used by + helplist_item_edit and helplist_item_remove. + """ + if event.type.name == 'KeyRelease': + self.helplist.selection_anchor(ACTIVE) self.set_add_delete_state() def set_add_delete_state(self): diff --git a/Lib/idlelib/idle_test/test_configdialog.py b/Lib/idlelib/idle_test/test_configdialog.py index 3c5f99f98f0bc2..367c63dc01126a 100644 --- a/Lib/idlelib/idle_test/test_configdialog.py +++ b/Lib/idlelib/idle_test/test_configdialog.py @@ -9,7 +9,9 @@ import unittest from unittest import mock from idlelib.idle_test.mock_idle import Func -from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL) +from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL, + EventType) +from types import SimpleNamespace from idlelib import config from idlelib.configdialog import idleConf, changes, tracers @@ -1060,6 +1062,14 @@ def test_on_bindingslist_select(self): self.assertEqual(b.get('anchor'), 'find') self.assertNotIn('disabled', d.button_new_keys.state()) + # gh-75234: Up and Down keys move the active item, but not the + # anchor; the handler moves the anchor. + d.button_new_keys.state(('disabled',)) + b.activate(0) + d.on_bindingslist_select(SimpleNamespace(type=EventType.KeyRelease)) + self.assertEqual(b.get('anchor'), 'copy') + self.assertNotIn('disabled', d.button_new_keys.state()) + def test_create_new_key_set_and_save_new_key_set(self): eq = self.assertEqual d = self.page @@ -1110,11 +1120,14 @@ def test_load_keys_list(self): 'force-open-completions - ', 'spam - ') - # No current selection. + # No current selection: select the first item. + d.button_new_keys.state(('disabled',)) d.load_keys_list('my keys') eq(b.get(0, 'end'), expected) - eq(b.get('anchor'), '') - eq(b.curselection(), ()) + eq(b.get('anchor'), 'copy - ') + eq(b.curselection(), (0, )) + eq(b.index('active'), 0) + self.assertNotIn('disabled', d.button_new_keys.state()) # Check selection. b.selection_set(1) @@ -1584,6 +1597,26 @@ def test_helplist_item_remove(self): eq(fr.user_helplist, []) self.assertTrue(fr.upc.called == fr.set.called == 1) + def test_helplist_item_remove_keyboard_selection(self): + # gh-75234: Up and Down keys move the active item, but not the + # anchor; the handler moves the anchor. + eq = self.assertEqual + fr = self.frame + fr.helplist.delete(0, 'end') + fr.helplist.insert('end', 'name1', 'name2') + fr.helplist.selection_anchor(0) + fr.helplist.selection_set(1) + fr.helplist.activate(1) + fr.user_helplist.clear() + fr.user_helplist.extend([('name1', 'file1'), ('name2', 'file2')]) + fr.set.called = fr.upc.called = 0 + + fr.help_source_selected(SimpleNamespace(type=EventType.KeyRelease)) + eq(fr.helplist.get('anchor'), 'name2') + fr.helplist_item_remove() + eq(fr.helplist.get(0, 'end'), ('name1',)) + eq(fr.user_helplist, [('name1', 'file1')]) + def test_update_help_changes(self): fr = self.frame self.addCleanup(setattr, fr, 'update_help_changes', Func()) # Re-mask method. diff --git a/Misc/NEWS.d/next/IDLE/2026-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst b/Misc/NEWS.d/next/IDLE/2026-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst new file mode 100644 index 00000000000000..70d64aa09a6dee --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2026-09-15-20-14-08.gh-issue-75234.O8ZNFV.rst @@ -0,0 +1,2 @@ +Fix editing help sources and key bindings in the IDLE Settings dialog after +selecting them with the keyboard.