From 298d5440eb83f2dfd5651bac86d1592ec358d54c Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:47:21 -0800 Subject: [PATCH 1/2] gh-143706: Fix sys.argv not set during multiprocessing forkserver `__main__` preload (#143717) The forkserver was not passing sys.argv to its main() function, causing sys.argv to be empty during `__main__` module import in child processes. This fixes a non-obvious regression inadvertently introduced by the gh-126631 main preloading fix. --- Lib/multiprocessing/forkserver.py | 6 ++++- Lib/test/_test_multiprocessing.py | 20 +++++++++++++++++ Lib/test/mp_preload_sysargv.py | 22 +++++++++++++++++++ ...01-12-07-17-38.gh-issue-143706.sysArgv.rst | 5 +++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 Lib/test/mp_preload_sysargv.py create mode 100644 Misc/NEWS.d/next/Library/2026-01-12-07-17-38.gh-issue-143706.sysArgv.rst diff --git a/Lib/multiprocessing/forkserver.py b/Lib/multiprocessing/forkserver.py index cc8947c5e04fb1..15c455a598dc27 100644 --- a/Lib/multiprocessing/forkserver.py +++ b/Lib/multiprocessing/forkserver.py @@ -152,6 +152,8 @@ def ensure_running(self): main_kws['sys_path'] = data['sys_path'] if 'init_main_from_path' in data: main_kws['main_path'] = data['init_main_from_path'] + if 'sys_argv' in data: + main_kws['sys_argv'] = data['sys_argv'] with socket.socket(socket.AF_UNIX) as listener: address = connection.arbitrary_address('AF_UNIX') @@ -197,7 +199,7 @@ def ensure_running(self): # def main(listener_fd, alive_r, preload, main_path=None, sys_path=None, - *, authkey_r=None): + *, sys_argv=None, authkey_r=None): """Run forkserver.""" if authkey_r is not None: try: @@ -209,6 +211,8 @@ def main(listener_fd, alive_r, preload, main_path=None, sys_path=None, authkey = b'' if preload: + if sys_argv is not None: + sys.argv[:] = sys_argv if sys_path is not None: sys.path[:] = sys_path if '__main__' in preload and main_path is not None: diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 844539104e3a3e..cc07062eee6f98 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -7086,6 +7086,26 @@ def test_preload_main(self): out = out.decode().split("\n") self.assertEqual(out, ['__main__', '__mp_main__', 'f', 'f', '']) + def test_preload_main_sys_argv(self): + # gh-143706: Check that sys.argv is set before __main__ is pre-loaded + if multiprocessing.get_start_method() != "forkserver": + self.skipTest("forkserver specific test") + + name = os.path.join(os.path.dirname(__file__), 'mp_preload_sysargv.py') + _, out, err = test.support.script_helper.assert_python_ok( + name, 'foo', 'bar') + self.assertEqual(err, b'') + + out = out.decode().split("\n") + expected_argv = "['foo', 'bar']" + self.assertEqual(out, [ + f"module:{expected_argv}", + f"fun:{expected_argv}", + f"module:{expected_argv}", + f"fun:{expected_argv}", + '', + ]) + # # Mixins # diff --git a/Lib/test/mp_preload_sysargv.py b/Lib/test/mp_preload_sysargv.py new file mode 100644 index 00000000000000..5ad38cda889f55 --- /dev/null +++ b/Lib/test/mp_preload_sysargv.py @@ -0,0 +1,22 @@ +# gh-143706: Test that sys.argv is correctly set during main module import +# when using forkserver with __main__ preloading. + +import multiprocessing +import sys + +# This will be printed during module import - sys.argv should be correct here +print(f"module:{sys.argv[1:]}") + +def fun(): + # This will be printed when the function is called + print(f"fun:{sys.argv[1:]}") + +if __name__ == "__main__": + ctx = multiprocessing.get_context("forkserver") + ctx.set_forkserver_preload(['__main__']) + + fun() + + p = ctx.Process(target=fun) + p.start() + p.join() diff --git a/Misc/NEWS.d/next/Library/2026-01-12-07-17-38.gh-issue-143706.sysArgv.rst b/Misc/NEWS.d/next/Library/2026-01-12-07-17-38.gh-issue-143706.sysArgv.rst new file mode 100644 index 00000000000000..5bdefbb7913c14 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-12-07-17-38.gh-issue-143706.sysArgv.rst @@ -0,0 +1,5 @@ +Fix :mod:`multiprocessing` forkserver so that :data:`sys.argv` is correctly +set before ``__main__`` is preloaded. Previously, :data:`sys.argv` was empty +during main module import in forkserver child processes. This fixes a +regression introduced in 3.13.8 and 3.14.1. Root caused by Aaron Wieczorek, +test provided by Thomas Watson, thanks! From a6bc60da02ea37f33d5abe5e7028fb0876110b76 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Mon, 12 Jan 2026 17:55:02 -0600 Subject: [PATCH 2/2] Update random combinatoric recipes and add tests (gh-143762) --- Doc/library/random.rst | 91 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/Doc/library/random.rst b/Doc/library/random.rst index 6bddf575a809a1..4c37a69079dcd6 100644 --- a/Doc/library/random.rst +++ b/Doc/library/random.rst @@ -634,11 +634,12 @@ from the combinatoric iterators in the :mod:`itertools` module or the :pypi:`more-itertools` project: .. testcode:: + import random - def random_product(*args, repeat=1): - "Random selection from itertools.product(*args, **kwds)" - pools = [tuple(pool) for pool in args] * repeat + def random_product(*iterables, repeat=1): + "Random selection from itertools.product(*iterables, repeat=repeat)" + pools = tuple(map(tuple, iterables)) * repeat return tuple(map(random.choice, pools)) def random_permutation(iterable, r=None): @@ -663,15 +664,89 @@ or the :pypi:`more-itertools` project: return tuple(pool[i] for i in indices) def random_derangement(iterable): - "Choose a permutation where no element is in its original position." + "Choose a permutation where no element stays in its original position." seq = tuple(iterable) if len(seq) < 2: - raise ValueError('derangements require at least two values') - perm = list(seq) + if not seq: + return () + raise IndexError('No derangments to choose from') + perm = list(range(len(seq))) + start = tuple(perm) while True: random.shuffle(perm) - if all(p != q for p, q in zip(seq, perm)): - return tuple(perm) + if all(p != q for p, q in zip(start, perm)): + return tuple([seq[i] for i in perm]) + +.. doctest:: + :hide: + + >>> import random + + + >>> random.seed(8675309) + >>> random_product('ABCDEFG', repeat=5) + ('D', 'B', 'E', 'F', 'E') + + + >>> random.seed(8675309) + >>> random_permutation('ABCDEFG') + ('D', 'B', 'E', 'C', 'G', 'A', 'F') + >>> random_permutation('ABCDEFG', 5) + ('A', 'G', 'D', 'C', 'B') + + + >>> random.seed(8675309) + >>> random_combination('ABCDEFG', 7) + ('A', 'B', 'C', 'D', 'E', 'F', 'G') + >>> random_combination('ABCDEFG', 6) + ('A', 'B', 'C', 'D', 'F', 'G') + >>> random_combination('ABCDEFG', 5) + ('A', 'B', 'C', 'E', 'F') + >>> random_combination('ABCDEFG', 4) + ('B', 'C', 'D', 'G') + >>> random_combination('ABCDEFG', 3) + ('B', 'E', 'G') + >>> random_combination('ABCDEFG', 2) + ('E', 'G') + >>> random_combination('ABCDEFG', 1) + ('C',) + >>> random_combination('ABCDEFG', 0) + () + + + >>> random.seed(8675309) + >>> random_combination_with_replacement('ABCDEFG', 7) + ('B', 'C', 'D', 'E', 'E', 'E', 'G') + >>> random_combination_with_replacement('ABCDEFG', 3) + ('A', 'B', 'E') + >>> random_combination_with_replacement('ABCDEFG', 2) + ('A', 'G') + >>> random_combination_with_replacement('ABCDEFG', 1) + ('E',) + >>> random_combination_with_replacement('ABCDEFG', 0) + () + + + >>> random.seed(8675309) + >>> random_derangement('') + () + >>> random_derangement('A') + Traceback (most recent call last): + ... + IndexError: No derangments to choose from + >>> random_derangement('AB') + ('B', 'A') + >>> random_derangement('ABC') + ('C', 'A', 'B') + >>> random_derangement('ABCD') + ('B', 'A', 'D', 'C') + >>> random_derangement('ABCDE') + ('B', 'C', 'A', 'E', 'D') + >>> # Identical inputs treated as distinct + >>> identical = 20 + >>> random_derangement((10, identical, 30, identical)) + (20, 30, 10, 20) + The default :func:`.random` returns multiples of 2⁻⁵³ in the range *0.0 ≤ x < 1.0*. All such numbers are evenly spaced and are exactly