Skip to content

Commit 1931c2a

Browse files
[3.11] gh-90876: Restore the ability to import multiprocessing when sys.executable is None (GH-106464) (#106495)
gh-90876: Restore the ability to import multiprocessing when `sys.executable` is `None` (GH-106464) Prevent `multiprocessing.spawn` from failing to *import* in environments where `sys.executable` is `None`. This regressed in 3.11 with the addition of support for path-like objects in multiprocessing. Adds a test decorator to have tests only run when part of test_multiprocessing_spawn to `_test_multiprocessing.py` so we can start to avoid re-running the same not-global-state specific test in all 3 modes when there is no need. (cherry picked from commit c60df36) Co-authored-by: Gregory P. Smith <[email protected]>
1 parent 80117dd commit 1931c2a

File tree

3 files changed

+83
-8
lines changed

3 files changed

+83
-8
lines changed

Lib/multiprocessing/spawn.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131
WINSERVICE = False
3232
else:
3333
WINEXE = getattr(sys, 'frozen', False)
34-
WINSERVICE = sys.executable.lower().endswith("pythonservice.exe")
34+
WINSERVICE = sys.executable and sys.executable.lower().endswith("pythonservice.exe")
3535

3636
def set_executable(exe):
3737
global _python_exe
38-
if sys.platform == 'win32':
38+
if exe is None:
39+
_python_exe = exe
40+
elif sys.platform == 'win32':
3941
_python_exe = os.fsdecode(exe)
4042
else:
4143
_python_exe = os.fsencode(exe)

Lib/test/_test_multiprocessing.py

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import os
1414
import gc
1515
import errno
16+
import functools
1617
import signal
1718
import array
1819
import socket
@@ -31,6 +32,7 @@
3132
from test.support import hashlib_helper
3233
from test.support import import_helper
3334
from test.support import os_helper
35+
from test.support import script_helper
3436
from test.support import socket_helper
3537
from test.support import threading_helper
3638
from test.support import warnings_helper
@@ -170,6 +172,59 @@ def check_enough_semaphores():
170172
"to run the test (required: %d)." % nsems_min)
171173

172174

175+
def only_run_in_spawn_testsuite(reason):
176+
"""Returns a decorator: raises SkipTest when SM != spawn at test time.
177+
178+
This can be useful to save overall Python test suite execution time.
179+
"spawn" is the universal mode available on all platforms so this limits the
180+
decorated test to only execute within test_multiprocessing_spawn.
181+
182+
This would not be necessary if we refactored our test suite to split things
183+
into other test files when they are not start method specific to be rerun
184+
under all start methods.
185+
"""
186+
187+
def decorator(test_item):
188+
189+
@functools.wraps(test_item)
190+
def spawn_check_wrapper(*args, **kwargs):
191+
if (start_method := multiprocessing.get_start_method()) != "spawn":
192+
raise unittest.SkipTest(f"{start_method=}, not 'spawn'; {reason}")
193+
return test_item(*args, **kwargs)
194+
195+
return spawn_check_wrapper
196+
197+
return decorator
198+
199+
200+
class TestInternalDecorators(unittest.TestCase):
201+
"""Logic within a test suite that could errantly skip tests? Test it!"""
202+
203+
@unittest.skipIf(sys.platform == "win32", "test requires that fork exists.")
204+
def test_only_run_in_spawn_testsuite(self):
205+
if multiprocessing.get_start_method() != "spawn":
206+
raise unittest.SkipTest("only run in test_multiprocessing_spawn.")
207+
208+
try:
209+
@only_run_in_spawn_testsuite("testing this decorator")
210+
def return_four_if_spawn():
211+
return 4
212+
except Exception as err:
213+
self.fail(f"expected decorated `def` not to raise; caught {err}")
214+
215+
orig_start_method = multiprocessing.get_start_method(allow_none=True)
216+
try:
217+
multiprocessing.set_start_method("spawn", force=True)
218+
self.assertEqual(return_four_if_spawn(), 4)
219+
multiprocessing.set_start_method("fork", force=True)
220+
with self.assertRaises(unittest.SkipTest) as ctx:
221+
return_four_if_spawn()
222+
self.assertIn("testing this decorator", str(ctx.exception))
223+
self.assertIn("start_method=", str(ctx.exception))
224+
finally:
225+
multiprocessing.set_start_method(orig_start_method, force=True)
226+
227+
173228
#
174229
# Creates a wrapper for a function which records the time it takes to finish
175230
#
@@ -5775,6 +5830,7 @@ def test_namespace(self):
57755830

57765831

57775832
class TestNamedResource(unittest.TestCase):
5833+
@only_run_in_spawn_testsuite("spawn specific test.")
57785834
def test_global_named_resource_spawn(self):
57795835
#
57805836
# gh-90549: Check that global named resources in main module
@@ -5785,22 +5841,18 @@ def test_global_named_resource_spawn(self):
57855841
with open(testfn, 'w', encoding='utf-8') as f:
57865842
f.write(textwrap.dedent('''\
57875843
import multiprocessing as mp
5788-
57895844
ctx = mp.get_context('spawn')
5790-
57915845
global_resource = ctx.Semaphore()
5792-
57935846
def submain(): pass
5794-
57955847
if __name__ == '__main__':
57965848
p = ctx.Process(target=submain)
57975849
p.start()
57985850
p.join()
57995851
'''))
5800-
rc, out, err = test.support.script_helper.assert_python_ok(testfn)
5852+
rc, out, err = script_helper.assert_python_ok(testfn)
58015853
# on error, err = 'UserWarning: resource_tracker: There appear to
58025854
# be 1 leaked semaphore objects to clean up at shutdown'
5803-
self.assertEqual(err, b'')
5855+
self.assertFalse(err, msg=err.decode('utf-8'))
58045856

58055857

58065858
class MiscTestCase(unittest.TestCase):
@@ -5809,6 +5861,24 @@ def test__all__(self):
58095861
support.check__all__(self, multiprocessing, extra=multiprocessing.__all__,
58105862
not_exported=['SUBDEBUG', 'SUBWARNING'])
58115863

5864+
@only_run_in_spawn_testsuite("avoids redundant testing.")
5865+
def test_spawn_sys_executable_none_allows_import(self):
5866+
# Regression test for a bug introduced in
5867+
# https://github.com/python/cpython/issues/90876 that caused an
5868+
# ImportError in multiprocessing when sys.executable was None.
5869+
# This can be true in embedded environments.
5870+
rc, out, err = script_helper.assert_python_ok(
5871+
"-c",
5872+
"""if 1:
5873+
import sys
5874+
sys.executable = None
5875+
assert "multiprocessing" not in sys.modules, "already imported!"
5876+
import multiprocessing
5877+
import multiprocessing.spawn # This should not fail\n""",
5878+
)
5879+
self.assertEqual(rc, 0)
5880+
self.assertFalse(err, msg=err.decode('utf-8'))
5881+
58125882

58135883
#
58145884
# Mixins
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Prevent :mod:`multiprocessing.spawn` from failing to *import* in environments
2+
where ``sys.executable`` is ``None``. This regressed in 3.11 with the addition
3+
of support for path-like objects in multiprocessing.

0 commit comments

Comments
 (0)