From fcfa919d9c1b3a65c78e99e8ed6615dd4d583404 Mon Sep 17 00:00:00 2001 From: Russell Davis <551404+russelldavis@users.noreply.github.com> Date: Tue, 11 Aug 2026 06:08:15 -0700 Subject: [PATCH 01/11] gh-84687: Add filename to the error raised by os.exec* (GH-19915) Co-authored-by: Serhiy Storchaka --- Lib/os.py | 7 +++ Lib/test/test_os/test_os.py | 46 +++++++++++++++++-- ...0-05-05-06-05-24.gh-issue-84687.ggjoGl.rst | 3 ++ Modules/posixmodule.c | 2 +- 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst diff --git a/Lib/os.py b/Lib/os.py index a5e1d8055569988..87547e369db817c 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -643,11 +643,13 @@ def _execvpe(file, args, env=None): argrest = (args,) env = environ + file = fspath(file) if path.dirname(file): exec_func(file, *argrest) return saved_exc = None path_list = get_exec_path(env) + orig_file = file if name != 'nt': file = fsencode(file) path_list = map(fsencode, path_list) @@ -663,6 +665,11 @@ def _execvpe(file, args, env=None): saved_exc = e if saved_exc is not None: raise saved_exc + # At this point, last_exc.filename contains the full path of whatever + # directory happened to be last in path_list. Set it to the filename that + # was passed in, which is what the caller will expect. This is what + # subprocess does too (see err_filename in Popen._execute_child()). + last_exc.filename = orig_file raise last_exc diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index bcf83a314f1a6eb..4c1ab96065587e5 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2636,12 +2636,50 @@ def mock_execve(name, *args): @unittest.skipUnless(hasattr(os, 'execv'), "need os.execv()") +@unittest.skipIf(support.is_emscripten, + "Emscripten always fails with ENOEXEC") +@unittest.skipIf(support.is_android, + "PATH contains an inaccessible directory on Android") class ExecTests(unittest.TestCase): - @unittest.skipIf(USING_LINUXTHREADS, - "avoid triggering a linuxthreads bug: see issue #4970") + def _test_bad_program(self, do_exec, exc_type=OSError): + bad_filenames = ['nosuchapp', FakePath('nosuchapp')] + if os.name != 'nt': + # Bytes program names are not supported on Windows. + bad_filenames += [b'nosuchapp', FakePath(b'nosuchapp')] + for bad_filename in bad_filenames: + with self.subTest(bad_filename): + with self.assertRaises(exc_type) as ctx: + do_exec(bad_filename) + self.assertEqual(ctx.exception.filename, + os.fspath(bad_filename)) + self.assertIn('nosuchapp', str(ctx.exception)) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execv_with_bad_program(self): + self._test_bad_program(lambda name: os.execv(name, ['nosuchapp'])) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execvp_with_bad_program(self): + self._test_bad_program(lambda name: os.execvp(name, ['nosuchapp'])) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execve_with_bad_program(self): + self._test_bad_program(lambda name: os.execve(name, ['nosuchapp'], {})) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") def test_execvpe_with_bad_program(self): - self.assertRaises(OSError, os.execvpe, 'no such app-', - ['no such app-'], None) + self._test_bad_program(lambda name: os.execvpe(name, ['nosuchapp'], {})) + + @unittest.skipUnless(os.name == 'posix', 'POSIX specific test') + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execvp_with_bad_path_entry(self): + # A regular file in PATH makes the exec fail with ENOTDIR. + create_file(os_helper.TESTFN) + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + with os_helper.EnvironmentVarGuard() as env: + env['PATH'] = os.path.abspath(os_helper.TESTFN) + self._test_bad_program(lambda name: os.execvp(name, ['nosuchapp']), + NotADirectoryError) def test_execv_with_bad_arglist(self): self.assertRaises(ValueError, os.execv, 'notepad', ()) diff --git a/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst new file mode 100644 index 000000000000000..b6dd108136f6dc9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst @@ -0,0 +1,3 @@ +The :func:`os.exec\* ` functions now set the +:attr:`~OSError.filename` attribute of the raised :exc:`FileNotFoundError` +or :exc:`NotADirectoryError` to the program name passed by the caller. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index db65d5862440655..a9dd8647545bc9c 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -7520,7 +7520,7 @@ os_execv_impl(PyObject *module, path_t *path, PyObject *argv) /* If we get here it's definitely an error */ - posix_error(); + posix_path_error(path); free_string_array(argvlist, argc); return NULL; } From f3be08aa6f6f8fbb48b532dc75845be5b3c83a8b Mon Sep 17 00:00:00 2001 From: PhysicistJohn <54456354+PhysicistJohn@users.noreply.github.com> Date: Tue, 11 Aug 2026 06:41:05 -0700 Subject: [PATCH 02/11] gh-154568: Fix array unpickling of little-endian float16 (#154569) Fix array._array_reconstructor() ignoring requested byte order for float16. The slow-path decoder for IEEE_754_FLOAT16_LE/BE computed the byte-order flag by comparing mformat_code against IEEE_754_FLOAT_LE (the 32-bit float constant) instead of IEEE_754_FLOAT16_LE. Since the float16 mformat codes are never equal to that constant, the comparison was always false, so the decoder always treated input as big-endian regardless of what was requested. Adds a regression test. --- Lib/test/test_array.py | 14 ++++++++++++++ ...00-00.gh-issue-154568.float16-reconstructor.rst | 1 + Modules/arraymodule.c | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index b5f6603defde5c5..d9b608fb23d0c60 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -209,6 +209,10 @@ def test_numbers(self): [-1<<63, (1<<63)-1, 0]), (['l'], SIGNED_INT64_BE, '>qqq', [-1<<63, (1<<63)-1, 0]), + (['e'], IEEE_754_FLOAT16_LE, 'eeee', + [1.0, float('inf'), float('-inf'), -0.0]), (['f'], IEEE_754_FLOAT_LE, 'ffff', @@ -239,6 +243,16 @@ def test_numbers(self): self.assertEqual(a, b, msg="{0!r} != {1!r}; testcase={2!r}".format(a, b, testcase)) + def test_float16_endianness(self): + # gh-154568: array_reconstructor() slow-path decoder for + # IEEE_754_FLOAT16_LE ignored the encoding. + le_bytes = struct.pack('e', 1.5) + b_le = array_reconstructor(array.array, 'd', IEEE_754_FLOAT16_LE, le_bytes) + b_be = array_reconstructor(array.array, 'd', IEEE_754_FLOAT16_BE, be_bytes) + self.assertEqual(b_le.tolist(), [1.5]) + self.assertEqual(b_be.tolist(), [1.5]) + def test_unicode(self): teststr = "Bonne Journ\xe9e \U0002030a\U00020347" testcases = ( diff --git a/Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst b/Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst new file mode 100644 index 000000000000000..85c0c81231b6aea --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst @@ -0,0 +1 @@ +Fix :mod:`array` unpickling of little-endian float16. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 68486c66575933a..39a399d7a49cf54 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -2281,7 +2281,7 @@ array__array_reconstructor_impl(PyObject *module, PyTypeObject *arraytype, case IEEE_754_FLOAT16_LE: case IEEE_754_FLOAT16_BE: { Py_ssize_t i; - int le = (mformat_code == IEEE_754_FLOAT_LE) ? 1 : 0; + int le = (mformat_code == IEEE_754_FLOAT16_LE) ? 1 : 0; Py_ssize_t itemcount = Py_SIZE(items) / 2; const char *memstr = PyBytes_AS_STRING(items); From 837627dc96dc557e1655690d9f59892725ed85b1 Mon Sep 17 00:00:00 2001 From: Meng Date: Tue, 11 Aug 2026 21:57:16 +0800 Subject: [PATCH 03/11] gh-108518: Do not cancel remaining calls in Executor.map() on error (GH-109497) If a call raises an exception, the remaining calls are no longer cancelled and iteration can be continued. Use the close() method of the returned iterator to cancel them. Co-authored-by: Meng Xiangzhuo Co-authored-by: Serhiy Storchaka --- Doc/library/concurrent.futures.rst | 13 +++++- Doc/whatsnew/3.16.rst | 9 ++++ Lib/concurrent/futures/_base.py | 28 +++++++++++- Lib/concurrent/futures/process.py | 11 ++++- Lib/test/test_concurrent_futures/executor.py | 45 +++++++++++++++---- ...-02-04-13-56-48.gh-issue-108518.6NCPk_.rst | 3 ++ 6 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-02-04-13-56-48.gh-issue-108518.6NCPk_.rst diff --git a/Doc/library/concurrent.futures.rst b/Doc/library/concurrent.futures.rst index cadf841b43537e5..1f34dc66228e308 100644 --- a/Doc/library/concurrent.futures.rst +++ b/Doc/library/concurrent.futures.rst @@ -61,11 +61,18 @@ Executor Objects The returned iterator raises a :exc:`TimeoutError` if :meth:`~iterator.__next__` is called and the result isn't available after *timeout* seconds from the original call to :meth:`Executor.map`. - *timeout* can be an int or a float. If *timeout* is not specified or + *timeout* can be an int or a float. + It cancels all future calls of *fn* and closes the iterator. + If *timeout* is not specified or ``None``, there is no limit to the wait time. If a *fn* call raises an exception, then that exception will be raised when its value is retrieved from the iterator. + It does not cancel future calls of *fn*. + + The returned iterator has method :meth:`!close` which cancels all + future calls of *fn* and discards the results of already finished calls + if they are available. When using :class:`ProcessPoolExecutor`, this method chops *iterables* into a number of chunks which it submits to the pool as separate @@ -82,6 +89,10 @@ Executor Objects .. versionchanged:: 3.14 Added the *buffersize* parameter. + .. versionchanged:: next + The returned iterator is no longer automatically closed if a *fn* + call raises an exception. + .. method:: shutdown(wait=True, *, cancel_futures=False) Signal the executor that it should free any resources that it is using diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 9edbccf3adef9e5..c16f4ca04f757f9 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -276,6 +276,15 @@ ctypes (Contributed by Peter Bierma in :gh:`153903`.) +concurrent.futures +------------------ + +* The iterator returned by :meth:`concurrent.futures.Executor.map` is no longer + automatically closed if a function call raises an exception. + Use method :meth:`!close` to explicitly close the iterator. + (Contributed by xzmeng and Serhiy Storchaka in :gh:`108518`.) + + encodings --------- diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index 43774066c5a9f00..cc335d9aa1ea55d 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -309,7 +309,11 @@ def wait(fs, timeout=None, return_when=ALL_COMPLETED): def _result_or_cancel(fut, timeout=None): try: try: - return fut.result(timeout) + return (fut.result(timeout), None) + except TimeoutError: + raise + except BaseException as exc: + return (None, exc) finally: fut.cancel() finally: @@ -592,6 +596,7 @@ def _get_snapshot(self): __class_getitem__ = classmethod(types.GenericAlias) + class Executor(object): """This is an abstract base class for concrete asynchronous executors.""" @@ -638,7 +643,10 @@ def map(self, fn, *iterables, timeout=None, chunksize=1, buffersize=None): raise TypeError("buffersize must be an integer or None") if buffersize is not None and buffersize < 1: raise ValueError("buffersize must be None or > 0") + return _MapResultIterator(self._map(fn, *iterables, timeout=timeout, + buffersize=buffersize)) + def _map(self, fn, *iterables, timeout=None, buffersize=None): if timeout is not None: end_time = timeout + time.monotonic() @@ -701,6 +709,24 @@ def __exit__(self, exc_type, exc_val, exc_tb): return False +class _MapResultIterator: + """The iterator returned by map().""" + def __init__(self, gen): + self.gen = gen + + def __iter__(self): + return self + + def __next__(self): + value, exc = next(self.gen) + if exc is not None: + raise exc + return value + + def close(self): + self.gen.close() + + class BrokenExecutor(RuntimeError): """ Raised when an executor has become non-functional after a severe failure. diff --git a/Lib/concurrent/futures/process.py b/Lib/concurrent/futures/process.py index e3b6c4a5305615a..c130259acb737ab 100644 --- a/Lib/concurrent/futures/process.py +++ b/Lib/concurrent/futures/process.py @@ -200,7 +200,14 @@ def _process_chunk(fn, chunk): This function is run in a separate process. """ - return [fn(*args) for args in chunk] + results = [] + for args in chunk: + try: + result = (fn(*args), None) + except BaseException as exc: + result = (None, exc) + results.append(result) + return results def _sendback_result(result_queue, work_id, result=None, exception=None, @@ -963,7 +970,7 @@ def map(self, fn, *iterables, timeout=None, chunksize=1, buffersize=None): itertools.batched(zip(*iterables), chunksize), timeout=timeout, buffersize=buffersize) - return _chain_from_iterable_of_lists(results) + return _base._MapResultIterator(_chain_from_iterable_of_lists(results)) def shutdown(self, wait=True, *, cancel_futures=False): with self._shutdown_lock: diff --git a/Lib/test/test_concurrent_futures/executor.py b/Lib/test/test_concurrent_futures/executor.py index a37c4d45f07b173..5d9f27c83bf9a81 100644 --- a/Lib/test/test_concurrent_futures/executor.py +++ b/Lib/test/test_concurrent_futures/executor.py @@ -71,21 +71,30 @@ def test_map(self): @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_exception(self): - i = self.executor.map(divmod, [1, 1, 1, 1], [2, 3, 0, 5]) - self.assertEqual(i.__next__(), (0, 1)) - self.assertEqual(i.__next__(), (0, 1)) - with self.assertRaises(ZeroDivisionError): - i.__next__() + i = self.executor.map(divmod, [5, 5, 5, 5], [2, 3, 0, 5]) + self.assertEqual(next(i), (2, 1)) + self.assertEqual(next(i), (1, 2)) + self.assertRaises(ZeroDivisionError, next, i) + self.assertEqual(next(i), (1, 0)) + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) + + i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5], chunksize=3) + self.assertEqual(next(i), (2, 1)) + self.assertRaises(ZeroDivisionError, next, i) + self.assertEqual(next(i), (1, 2)) + self.assertEqual(next(i), (1, 0)) + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.requires_resource('walltime') def test_map_timeout(self): results = [] + i = self.executor.map(time.sleep, [0, 0, 6], timeout=5) try: - for i in self.executor.map(time.sleep, - [0, 0, 6], - timeout=5): - results.append(i) + for result in i: + results.append(result) except futures.TimeoutError: pass else: @@ -95,6 +104,24 @@ def test_map_timeout(self): # take longer than the specified timeout. self.assertIn(results, ([None, None], [None], [])) + # The remaining calls are cancelled, so the iterator is exhausted. + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) + + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + def test_map_close(self): + i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5]) + self.assertEqual(next(i), (2, 1)) + i.close() + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) + + i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5], chunksize=3) + self.assertEqual(next(i), (2, 1)) + i.close() + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) + def test_map_buffersize_type_validation(self): for buffersize in ("foo", 2.0): with self.subTest(buffersize=buffersize): diff --git a/Misc/NEWS.d/next/Library/2024-02-04-13-56-48.gh-issue-108518.6NCPk_.rst b/Misc/NEWS.d/next/Library/2024-02-04-13-56-48.gh-issue-108518.6NCPk_.rst new file mode 100644 index 000000000000000..d7cf5ba88fa1868 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-04-13-56-48.gh-issue-108518.6NCPk_.rst @@ -0,0 +1,3 @@ +The iterator returned by :meth:`concurrent.futures.Executor.map` is no longer +automatically closed if a function call raises an exception. +Use method :meth:`!close` to explicitly close the iterator. From 7c072c1fcc3c04535dde873a7709781be2794583 Mon Sep 17 00:00:00 2001 From: Maciej Olko Date: Tue, 11 Aug 2026 16:38:42 +0200 Subject: [PATCH 04/11] Docs: simplify `turtle` tutorial's star example code (#155232) Co-authored-by: Stan Ulbrych --- Doc/library/turtle.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index d7839e2f81172c5..49099aea4152c14 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -79,8 +79,8 @@ In a Python shell, import all the objects of the ``turtle`` module:: from turtle import * -If you run into a ``No module named '_tkinter'`` error, you'll have to -install the :mod:`Tk interface package ` on your system. +If you run into a ``Standard library module '_tkinter' was not found`` error, +you'll have to install the :mod:`Tk interface package ` on your system. Basic drawing @@ -167,14 +167,16 @@ filling can be turned on and off:: Next we'll create a loop:: + start = pos() + while True: forward(200) left(170) - if abs(pos()) < 1: + if distance(start) < 1: break -``abs(pos()) < 1`` is a good way to know when the turtle is back at its -home position. +``distance(start) < 1`` is a good way to know when the turtle is back at its +start position. Finally, complete the filling:: From 4b04d5abcd8476af4c4a67fcf2263c856fdabb47 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 17:45:45 +0300 Subject: [PATCH 05/11] gh-76303: Improve documentation of the -x command line option (GH-155559) Expand the documentation for the -x command-line option to explain its purpose and usage for turning Python scripts into Windows batch files, with examples of batch file header lines. Co-authored-by: Rory Glenn Co-authored-by: Brian Schubert --- Doc/using/cmdline.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 677fbbae3f4219a..bd75afc18d03aea 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -519,7 +519,25 @@ Miscellaneous options .. option:: -x Skip the first line of the source, allowing use of non-Unix forms of - ``#!cmd``. This is intended for a DOS specific hack only. + ``#!cmd``. + + This can be used to turn a Python script into a Windows batch file. + Similarly to adding a shebang line and setting the executable bit on Unix, + the extension of the Python script can be changed to ``.bat`` and the + following line can be added at the start of the script: + + .. code-block:: batch + + @py -x "%~f0" %* & exit /b + + Or, to specify the path to the Python interpreter explicitly: + + .. code-block:: batch + + @"C:\Path\to\python.exe" -x "%~f0" %* & exit /b + + Unlike a shebang line which is a Python comment, this line is not valid + Python syntax, and the :option:`-x` option is needed to skip it. .. option:: -X From d65bf5174423a0abc288d9fa5f3ff0fcaf2bc49f Mon Sep 17 00:00:00 2001 From: An Long Date: Tue, 11 Aug 2026 23:47:52 +0900 Subject: [PATCH 06/11] gh-86768: Raise OSError when seeking a pipe on Windows (GH-133137) Previously os.lseek() and file seek() silently succeeded for pipes, and seekable() wrongly returned True. Co-authored-by: Serhiy Storchaka --- Doc/whatsnew/3.16.rst | 7 +++++++ Lib/test/test_os/test_os.py | 8 ++++++++ Lib/test/test_winapi.py | 2 +- .../2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst | 6 ++++++ Modules/_io/fileio.c | 9 ++++++++- Modules/posixmodule.c | 13 ++++++++++--- 6 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index c16f4ca04f757f9..b017535b96979d9 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -828,6 +828,13 @@ that may require changes to your code. :exc:`TypeError`. (Contributed by Serhiy Storchaka in :gh:`152587`.) +* On Windows, seeking a pipe now fails instead of silently appearing to + succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`, + and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence, + opening a pipe in a read-write binary mode (``'r+b'`` or ``'w+b'``) now + raises :exc:`io.UnsupportedOperation` unless buffering is disabled. + (Contributed by An Long in :gh:`86768`.) + Build changes ============= diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 4c1ab96065587e5..7a49cfa0c29ec5b 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2993,6 +2993,14 @@ def test_ftruncate(self): def test_lseek(self): self.check(os.lseek, 0, 0) + @unittest.skipUnless(hasattr(os, 'lseek'), 'test needs os.lseek()') + @unittest.skipUnless(hasattr(os, 'pipe'), "need os.pipe()") + def test_lseek_on_pipe(self): + rfd, wfd = os.pipe() + self.addCleanup(os.close, rfd) + self.addCleanup(os.close, wfd) + self.assertRaises(OSError, os.lseek, rfd, 123, os.SEEK_END) + @unittest.skipUnless(hasattr(os, 'read'), 'test needs os.read()') def test_read(self): self.check(os.read, 1) diff --git a/Lib/test/test_winapi.py b/Lib/test/test_winapi.py index 0ae03a3bf505f73..60f7881a0b0ab99 100644 --- a/Lib/test/test_winapi.py +++ b/Lib/test/test_winapi.py @@ -152,7 +152,7 @@ def test_namedpipe(self): # Pipe instance is available, so this passes _winapi.WaitNamedPipe(pipe_name, 0) - with open(pipe_name, 'w+b') as pipe2: + with open(pipe_name, 'w+b', buffering=0) as pipe2: # No instances available, so this times out # (WinError 121 does not get mapped to TimeoutError) with self.assertRaises(OSError): diff --git a/Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst b/Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst new file mode 100644 index 000000000000000..6085f47c49d5ed7 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst @@ -0,0 +1,6 @@ +:func:`os.lseek` and :meth:`~io.IOBase.seek` of file objects now raise +:exc:`OSError` for pipes on Windows, and :meth:`~io.IOBase.seekable` now +returns ``False`` for them. Previously seeking a pipe silently appeared to +succeed. As a consequence, opening a pipe in a read-write binary mode +(``'r+b'`` or ``'w+b'``) now raises :exc:`io.UnsupportedOperation` unless +buffering is disabled. diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 3aeb30dfe24a357..e8e9c132dd3465e 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -992,7 +992,14 @@ portable_lseek(fileio *self, PyObject *posobj, int whence, bool suppress_pipe_er Py_BEGIN_ALLOW_THREADS _Py_BEGIN_SUPPRESS_IPH #ifdef MS_WINDOWS - res = _lseeki64(fd, pos, whence); + HANDLE h = (HANDLE)_get_osfhandle(fd); + if (h != INVALID_HANDLE_VALUE && GetFileType(h) == FILE_TYPE_PIPE) { + res = -1; + errno = ESPIPE; + } + else { + res = _lseeki64(fd, pos, whence); + } #else res = lseek(fd, pos, whence); #endif diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index a9dd8647545bc9c..9e84fd400527ea2 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -12037,7 +12037,7 @@ static Py_off_t os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) /*[clinic end generated code: output=971e1efb6b30bd2f input=32ea0788da7cb44b]*/ { - Py_off_t result; + Py_off_t result = -1; #ifdef SEEK_SET /* Turn 0, 1, 2 into SEEK_{SET,CUR,END} */ @@ -12051,14 +12051,21 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) Py_BEGIN_ALLOW_THREADS _Py_BEGIN_SUPPRESS_IPH #ifdef MS_WINDOWS - result = _lseeki64(fd, position, how); + HANDLE h = (HANDLE)_get_osfhandle(fd); + if (h != INVALID_HANDLE_VALUE && GetFileType(h) == FILE_TYPE_PIPE) { + errno = ESPIPE; + } + else { + result = _lseeki64(fd, position, how); + } #else result = lseek(fd, position, how); #endif _Py_END_SUPPRESS_IPH Py_END_ALLOW_THREADS - if (result < 0) + if (result < 0) { posix_error(); + } return result; } From 76d556e7a33d982f18a935f289e82d205696aadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 11 Aug 2026 17:52:35 +0200 Subject: [PATCH 07/11] gh-155485: Skip `test_pyexpat.test_subparser_inherits_reparse_deferral` when Expat < 2.6.0 (#155554) Assisted-By: Claude Opus 4.6 Co-authored-by: Stan Ulbrych --- Lib/test/test_pyexpat.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 9869f0e88448cf1..baa4f178427d532 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -1045,6 +1045,9 @@ def test_parent_parser_outlives_its_subparsers__chain(self): del parser del subparser + # gh-155485: GetReparseDeferralEnabled always returns False with Expat <2.6.0. + @unittest.skipIf(not expat.ParserCreate().GetReparseDeferralEnabled(), + "requires Python compiled with Expat >= 2.6.0") def test_subparser_inherits_reparse_deferral(self): for enabled in (True, False): parser = expat.ParserCreate() From 4d3075655e8bfddb6f5015b43aade43b1265bd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Tue, 11 Aug 2026 08:52:43 -0700 Subject: [PATCH 08/11] gh-150820: Speed up json.dumps() for small documents (GH-150827) Only create the float formatting helper when the pure Python encoder is used. --- Lib/json/encoder.py | 45 +++++++++---------- ...-06-02-15-45-00.gh-issue-150820.W7tpO7.rst | 1 + 2 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-02-15-45-00.gh-issue-150820.W7tpO7.rst diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py index 718b3254241c565..8768b63a3f80417 100644 --- a/Lib/json/encoder.py +++ b/Lib/json/encoder.py @@ -223,29 +223,6 @@ def iterencode(self, o, _one_shot=False): else: _encoder = encode_basestring - def floatstr(o, allow_nan=self.allow_nan, - _repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY): - # Check for specials. Note that this type of test is processor - # and/or platform-specific, so do tests which don't depend on the - # internals. - - if o != o: - text = 'NaN' - elif o == _inf: - text = 'Infinity' - elif o == _neginf: - text = '-Infinity' - else: - return _repr(o) - - if not allow_nan: - raise ValueError( - "Out of range float values are not JSON compliant: " + - repr(o)) - - return text - - if self.indent is None or isinstance(self.indent, str): indent = self.indent else: @@ -256,6 +233,28 @@ def floatstr(o, allow_nan=self.allow_nan, self.key_separator, self.item_separator, self.sort_keys, self.skipkeys, self.allow_nan) else: + def floatstr(o, allow_nan=self.allow_nan, + _repr=float.__repr__, _inf=INFINITY, _neginf=-INFINITY): + # Check for specials. Note that this type of test is processor + # and/or platform-specific, so do tests which don't depend on + # the internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + _iterencode = _make_iterencode( markers, self.default, _encoder, indent, floatstr, self.key_separator, self.item_separator, self.sort_keys, diff --git a/Misc/NEWS.d/next/Library/2026-06-02-15-45-00.gh-issue-150820.W7tpO7.rst b/Misc/NEWS.d/next/Library/2026-06-02-15-45-00.gh-issue-150820.W7tpO7.rst new file mode 100644 index 000000000000000..ae9858f5294183a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-02-15-45-00.gh-issue-150820.W7tpO7.rst @@ -0,0 +1 @@ +Speed up :func:`json.dumps` for small documents. Patch by Bernát Gábor. From 42a18e14d201f8a99cd1f064b4058cbe81a70ba2 Mon Sep 17 00:00:00 2001 From: Tim Hatch Date: Tue, 11 Aug 2026 08:59:01 -0700 Subject: [PATCH 09/11] gh-68164: Set the "regular file" bit in zipfile's writestr (GH-134232) --- Lib/test/test_zipfile/test_core.py | 6 +++--- Lib/zipfile/__init__.py | 2 +- .../Library/2025-05-19-07-32-51.gh-issue-68164.XhFbJD.rst | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-05-19-07-32-51.gh-issue-68164.XhFbJD.rst diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index e9974d6c05648bb..d0ae7ce787bee32 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -500,7 +500,7 @@ def zip_test_writestr_permissions(self, f, compression): self.make_test_archive(f, compression) with zipfile.ZipFile(f, "r") as zipfp: zinfo = zipfp.getinfo('strfile') - self.assertEqual(zinfo.external_attr, 0o600 << 16) + self.assertEqual(zinfo.external_attr, 0o100600 << 16) zinfo2 = zipfp.getinfo('written-open-w') self.assertEqual(zinfo2.external_attr, 0o600 << 16) @@ -4513,8 +4513,8 @@ def test_for_archive(self): zi = zipfile.ZipInfo(base_filename)._for_archive(zf) self.assertEqual(zi.compress_level, 1) self.assertEqual(zi.compress_type, zipfile.ZIP_STORED) - # ?rw- --- --- - filemode = stat.S_IRUSR | stat.S_IWUSR + # - rw- --- --- + filemode = stat.S_IFREG | stat.S_IRUSR | stat.S_IWUSR # filemode is stored as the highest 16 bits of external_attr self.assertEqual(zi.external_attr >> 16, filemode) self.assertEqual(zi.external_attr & 0xFF, 0) # no MS-DOS flag diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index 764bb9b1e9246f6..dd1f7fb9e802048 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -689,7 +689,7 @@ def _for_archive(self, archive): self.external_attr = 0o40775 << 16 # drwxrwxr-x self.external_attr |= 0x10 # MS-DOS directory flag else: - self.external_attr = 0o600 << 16 # ?rw------- + self.external_attr = 0o100600 << 16 # -rw------- return self def is_dir(self): diff --git a/Misc/NEWS.d/next/Library/2025-05-19-07-32-51.gh-issue-68164.XhFbJD.rst b/Misc/NEWS.d/next/Library/2025-05-19-07-32-51.gh-issue-68164.XhFbJD.rst new file mode 100644 index 000000000000000..21c1ed188c25070 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-19-07-32-51.gh-issue-68164.XhFbJD.rst @@ -0,0 +1,2 @@ +Fix :func:`zipfile.ZipFile.writestr` so it sets the "regular file" bit by +default. From 44e92d4a5922dd8fa61f75834784c2772fdd34ab Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 11 Aug 2026 09:48:19 -0700 Subject: [PATCH 10/11] gh-155363: Fix QSBR slot leak on thread state creation failure (gh-155365) In the free-threaded build, new_threadstate() reserves a QSBR thread-state slot before it can still fail for other reasons, but the failure paths only called free_threadstate(), which does not know about the reservation. Restructure code so failure path doesn't leak. --- Include/internal/pycore_code.h | 4 ++++ ...026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst | 4 ++++ Objects/codeobject.c | 12 +++++++++--- Python/pystate.c | 16 +++++++++------- 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 5b1fddbe15b98b8..32242f89b812e69 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -582,6 +582,10 @@ PyAPI_FUNC(_Py_CODEUNIT *) _PyCode_GetTLBC(PyCodeObject *co); // Returns the reserved index or -1 on error. extern int32_t _Py_ReserveTLBCIndex(PyInterpreterState *interp); +// Release an index returned by _Py_ReserveTLBCIndex() that was never stored +// in a PyThreadState. +extern void _Py_UnreserveTLBCIndex(PyInterpreterState *interp, int32_t index); + // Release the current thread's index into thread-local bytecode arrays extern void _Py_ClearTLBCIndex(_PyThreadStateImpl *tstate); diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst new file mode 100644 index 000000000000000..52200bb9d2fd59f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst @@ -0,0 +1,4 @@ +Fix a leak in the :term:`free-threaded build` when creating a thread state +fails after an internal QSBR slot has been reserved for it. The slot could +never be reclaimed, so the QSBR array grew without bound across repeated +failures. diff --git a/Objects/codeobject.c b/Objects/codeobject.c index d7955cc7390a7ab..58811d63c7e318c 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -3314,14 +3314,20 @@ _Py_ReserveTLBCIndex(PyInterpreterState *interp) } void -_Py_ClearTLBCIndex(_PyThreadStateImpl *tstate) +_Py_UnreserveTLBCIndex(PyInterpreterState *interp, int32_t index) { - PyInterpreterState *interp = ((PyThreadState *)tstate)->interp; if (interp->config.tlbc_enabled) { - _PyIndexPool_FreeIndex(&interp->tlbc_indices, tstate->tlbc_index); + _PyIndexPool_FreeIndex(&interp->tlbc_indices, index); } } +void +_Py_ClearTLBCIndex(_PyThreadStateImpl *tstate) +{ + PyInterpreterState *interp = ((PyThreadState *)tstate)->interp; + _Py_UnreserveTLBCIndex(interp, tstate->tlbc_index); +} + static _PyCodeArray * _PyCodeArray_New(Py_ssize_t size) { diff --git a/Python/pystate.c b/Python/pystate.c index d10b38def32911d..646c157007d4ac1 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1667,21 +1667,23 @@ new_threadstate(PyInterpreterState *interp, int whence) return NULL; } -#ifdef Py_GIL_DISABLED - Py_ssize_t qsbr_idx = _Py_qsbr_reserve(interp); - if (qsbr_idx < 0) { +#ifdef Py_STATS + // The PyStats structure is quite large and is allocated separated from + // tstate. + if (!_PyStats_ThreadInit(interp, tstate)) { free_threadstate(tstate); return NULL; } +#endif +#ifdef Py_GIL_DISABLED int32_t tlbc_idx = _Py_ReserveTLBCIndex(interp); if (tlbc_idx < 0) { free_threadstate(tstate); return NULL; } -#endif -#ifdef Py_STATS - // The PyStats structure is quite large and is allocated separated from tstate. - if (!_PyStats_ThreadInit(interp, tstate)) { + Py_ssize_t qsbr_idx = _Py_qsbr_reserve(interp); + if (qsbr_idx < 0) { + _Py_UnreserveTLBCIndex(interp, tlbc_idx); free_threadstate(tstate); return NULL; } From 1cf7d898287972947f746fd647bd8aa8fc0e5aae Mon Sep 17 00:00:00 2001 From: globalshrug Date: Tue, 11 Aug 2026 10:01:52 -0700 Subject: [PATCH 11/11] gh-82039: Relax cookiejar.py case-sensitive regex for the inconsequential first line of the cookie file (GH-15673) Co-authored-by: Oleg Iarygin Co-authored-by: Serhiy Storchaka --- Lib/http/cookiejar.py | 3 ++- Lib/test/test_http_cookiejar.py | 25 +++++++++++++++++++ Misc/ACKS | 1 + ...3-02-11-09-49-49.gh-issue-82039.caTE7O.rst | 2 ++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2023-02-11-09-49-49.gh-issue-82039.caTE7O.rst diff --git a/Lib/http/cookiejar.py b/Lib/http/cookiejar.py index 13e5b104a81ea2b..302bd3676a8144d 100644 --- a/Lib/http/cookiejar.py +++ b/Lib/http/cookiejar.py @@ -53,7 +53,8 @@ def _debug(*args): HTTPONLY_ATTR = "HTTPOnly" HTTPONLY_PREFIX = "#HttpOnly_" DEFAULT_HTTP_PORT = str(http.client.HTTP_PORT) -NETSCAPE_MAGIC_RGX = re.compile("#( Netscape)? HTTP Cookie File") +NETSCAPE_MAGIC_RGX = re.compile("#( Netscape)? HTTP Cookie File", + re.IGNORECASE | re.ASCII) MISSING_FILENAME_TEXT = ("a filename was not supplied (nor was the CookieJar " "instance initialised with one)") NETSCAPE_HEADER_TEXT = """\ diff --git a/Lib/test/test_http_cookiejar.py b/Lib/test/test_http_cookiejar.py index 04cb440cd4ccf66..7f39b5c772bd10f 100644 --- a/Lib/test/test_http_cookiejar.py +++ b/Lib/test/test_http_cookiejar.py @@ -459,6 +459,31 @@ def test_bad_magic(self): finally: os_helper.unlink(filename) + def test_magic_ignores_case(self): + filename = os_helper.TESTFN + self.addCleanup(os_helper.unlink, filename) + for magic in ("# Netscape HTTP Cookie File", + "# netscape http cookie file", + "# HTTP Cookie File", + "# http cookie file"): + with self.subTest(magic=magic): + with open(filename, "w") as f: + f.write(magic + "\n") + MozillaCookieJar().load(filename) + + def test_magic_is_not_unicode(self): + # Unicode case folding must not be used: 'ſ' (U+017F) and 'K' + # (U+212A) are case-insensitively equal to 's' and 'k' in Unicode. + filename = os_helper.TESTFN + self.addCleanup(os_helper.unlink, filename) + for magic in ("# Netſcape HTTP Cookie File", + "# Netscape HTTP CooKie File"): + with self.subTest(magic=magic): + with open(filename, "w", encoding="utf-8") as f: + f.write(magic + "\n") + self.assertRaises(LoadError, MozillaCookieJar().load, filename) + + class CookieTests(unittest.TestCase): # XXX # Get rid of string comparisons where not actually testing str / repr. diff --git a/Misc/ACKS b/Misc/ACKS index fec00c1b272f4ea..9316e9359499581 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -748,6 +748,7 @@ Peter Harris Jonathan Hartley Travis B. Hartwell Henrik Harutyunyan +Ashley Harvey Shane Harvey Larry Hastings Tim Hatch diff --git a/Misc/NEWS.d/next/Library/2023-02-11-09-49-49.gh-issue-82039.caTE7O.rst b/Misc/NEWS.d/next/Library/2023-02-11-09-49-49.gh-issue-82039.caTE7O.rst new file mode 100644 index 000000000000000..cb75ae83780eaf6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-02-11-09-49-49.gh-issue-82039.caTE7O.rst @@ -0,0 +1,2 @@ +:meth:`http.cookiejar.FileCookieJar.load` now checks the first, format +signature line in a case-insensitive manner. Patch by Ashley Harvey.