diff --git a/.github/workflows/reusable-wasi.yml b/.github/workflows/reusable-wasi.yml index b0dda62d0b291c2..fd263e8aafd8f58 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/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 8946ee7a02da480..1d33f593fffe943 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 diff --git a/Lib/_strptime.py b/Lib/_strptime.py index 59ac96745aa15e2..3311ec888714696 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/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 4c94d9be69e95e1..a5d1aaeb5a946f4 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 3c5f99f98f0bc2d..367c63dc01126a3 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/Lib/test/test_strptime.py b/Lib/test/test_strptime.py index e95cc6db170e243..d70837ea63f5474 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/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 000000000000000..f1126adb1a069e6 --- /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/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 000000000000000..70d64aa09a6dee4 --- /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. 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 000000000000000..1b38ffd26ea20c3 --- /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. diff --git a/Platforms/WASI/config.site-wasm32-wasi b/Platforms/WASI/config.site-wasm32-wasi index c5d8b3e205db26f..88c54907880e73b 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 6a6d5713ee96739..151c528c87670d5 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"