Skip to content

Commit c60df36

Browse files
authored
gh-90876: Restore the ability to import multiprocessing when sys.executable is None (#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.
1 parent 76fac7b commit c60df36

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
@@ -171,6 +173,59 @@ def check_enough_semaphores():
171173
"to run the test (required: %d)." % nsems_min)
172174

173175

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

58165871

58175872
class TestNamedResource(unittest.TestCase):
5873+
@only_run_in_spawn_testsuite("spawn specific test.")
58185874
def test_global_named_resource_spawn(self):
58195875
#
58205876
# gh-90549: Check that global named resources in main module
@@ -5825,22 +5881,18 @@ def test_global_named_resource_spawn(self):
58255881
with open(testfn, 'w', encoding='utf-8') as f:
58265882
f.write(textwrap.dedent('''\
58275883
import multiprocessing as mp
5828-
58295884
ctx = mp.get_context('spawn')
5830-
58315885
global_resource = ctx.Semaphore()
5832-
58335886
def submain(): pass
5834-
58355887
if __name__ == '__main__':
58365888
p = ctx.Process(target=submain)
58375889
p.start()
58385890
p.join()
58395891
'''))
5840-
rc, out, err = test.support.script_helper.assert_python_ok(testfn)
5892+
rc, out, err = script_helper.assert_python_ok(testfn)
58415893
# on error, err = 'UserWarning: resource_tracker: There appear to
58425894
# be 1 leaked semaphore objects to clean up at shutdown'
5843-
self.assertEqual(err, b'')
5895+
self.assertFalse(err, msg=err.decode('utf-8'))
58445896

58455897

58465898
class MiscTestCase(unittest.TestCase):
@@ -5849,6 +5901,24 @@ def test__all__(self):
58495901
support.check__all__(self, multiprocessing, extra=multiprocessing.__all__,
58505902
not_exported=['SUBDEBUG', 'SUBWARNING'])
58515903

5904+
@only_run_in_spawn_testsuite("avoids redundant testing.")
5905+
def test_spawn_sys_executable_none_allows_import(self):
5906+
# Regression test for a bug introduced in
5907+
# https://github.com/python/cpython/issues/90876 that caused an
5908+
# ImportError in multiprocessing when sys.executable was None.
5909+
# This can be true in embedded environments.
5910+
rc, out, err = script_helper.assert_python_ok(
5911+
"-c",
5912+
"""if 1:
5913+
import sys
5914+
sys.executable = None
5915+
assert "multiprocessing" not in sys.modules, "already imported!"
5916+
import multiprocessing
5917+
import multiprocessing.spawn # This should not fail\n""",
5918+
)
5919+
self.assertEqual(rc, 0)
5920+
self.assertFalse(err, msg=err.decode('utf-8'))
5921+
58525922

58535923
#
58545924
# 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)