Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9a76735

Browse filesBrowse files
committed
Close #19946: use runpy as needed in multiprocessing
- handles main files without a suffix - handles main submodules properly - adds test cases for the various kinds of __main__
1 parent 7cff4cd commit 9a76735
Copy full SHA for 9a76735

File tree

4 files changed

+375
-54
lines changed
Filter options

4 files changed

+375
-54
lines changed

‎Doc/whatsnew/3.4.rst

Copy file name to clipboardExpand all lines: Doc/whatsnew/3.4.rst
+12-3Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -624,13 +624,22 @@ mmap objects can now be weakref'ed.
624624
multiprocessing
625625
---------------
626626

627-
On Unix two new *start methods* have been added for starting processes
628-
using :mod:`multiprocessing`. These make the mixing of processes with
629-
threads more robust. See :issue:`8713`.
627+
On Unix, two new *start methods* (``spawn`` and ``forkserver``) have been
628+
added for starting processes using :mod:`multiprocessing`. These make
629+
the mixing of processes with threads more robust, and the ``spawn``
630+
method matches the semantics that multiprocessing has always used on
631+
Windows. (Contributed by Richard Oudkerk in :issue:`8713`).
630632

631633
Also, except when using the old *fork* start method, child processes
632634
will no longer inherit unneeded handles/file descriptors from their parents.
633635

636+
:mod:`multiprocessing` now relies on :mod:`runpy` (which implements the
637+
``-m`` switch) to initialise ``__main__`` appropriately in child processes
638+
when using the ``spawn`` or ``forkserver`` start methods. This resolves some
639+
edge cases where combining multiprocessing, the ``-m`` command line switch
640+
and explicit relative imports could cause obscure failures in child
641+
processes. (Contributed by Nick Coghlan in :issue:`19946`)
642+
634643

635644
os
636645
--

‎Lib/multiprocessing/spawn.py

Copy file name to clipboardExpand all lines: Lib/multiprocessing/spawn.py
+70-51Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import os
1212
import pickle
1313
import sys
14+
import runpy
15+
import types
1416

1517
from . import get_start_method, set_start_method
1618
from . import process
@@ -157,15 +159,19 @@ def get_preparation_data(name):
157159
start_method=get_start_method(),
158160
)
159161

160-
if sys.platform != 'win32' or (not WINEXE and not WINSERVICE):
161-
main_path = getattr(sys.modules['__main__'], '__file__', None)
162-
if not main_path and sys.argv[0] not in ('', '-c'):
163-
main_path = sys.argv[0]
162+
# Figure out whether to initialise main in the subprocess as a module
163+
# or through direct execution (or to leave it alone entirely)
164+
main_module = sys.modules['__main__']
165+
main_mod_name = getattr(main_module.__spec__, "name", None)
166+
if main_mod_name is not None:
167+
d['init_main_from_name'] = main_mod_name
168+
elif sys.platform != 'win32' or (not WINEXE and not WINSERVICE):
169+
main_path = getattr(main_module, '__file__', None)
164170
if main_path is not None:
165171
if (not os.path.isabs(main_path) and
166172
process.ORIGINAL_DIR is not None):
167173
main_path = os.path.join(process.ORIGINAL_DIR, main_path)
168-
d['main_path'] = os.path.normpath(main_path)
174+
d['init_main_from_path'] = os.path.normpath(main_path)
169175

170176
return d
171177

@@ -206,55 +212,68 @@ def prepare(data):
206212
if 'start_method' in data:
207213
set_start_method(data['start_method'])
208214

209-
if 'main_path' in data:
210-
import_main_path(data['main_path'])
215+
if 'init_main_from_name' in data:
216+
_fixup_main_from_name(data['init_main_from_name'])
217+
elif 'init_main_from_path' in data:
218+
_fixup_main_from_path(data['init_main_from_path'])
219+
220+
# Multiprocessing module helpers to fix up the main module in
221+
# spawned subprocesses
222+
def _fixup_main_from_name(mod_name):
223+
# __main__.py files for packages, directories, zip archives, etc, run
224+
# their "main only" code unconditionally, so we don't even try to
225+
# populate anything in __main__, nor do we make any changes to
226+
# __main__ attributes
227+
current_main = sys.modules['__main__']
228+
if mod_name == "__main__" or mod_name.endswith(".__main__"):
229+
return
230+
231+
# If this process was forked, __main__ may already be populated
232+
if getattr(current_main.__spec__, "name", None) == mod_name:
233+
return
234+
235+
# Otherwise, __main__ may contain some non-main code where we need to
236+
# support unpickling it properly. We rerun it as __mp_main__ and make
237+
# the normal __main__ an alias to that
238+
old_main_modules.append(current_main)
239+
main_module = types.ModuleType("__mp_main__")
240+
main_content = runpy.run_module(mod_name,
241+
run_name="__mp_main__",
242+
alter_sys=True)
243+
main_module.__dict__.update(main_content)
244+
sys.modules['__main__'] = sys.modules['__mp_main__'] = main_module
245+
246+
247+
def _fixup_main_from_path(main_path):
248+
# If this process was forked, __main__ may already be populated
249+
current_main = sys.modules['__main__']
250+
251+
# Unfortunately, the main ipython launch script historically had no
252+
# "if __name__ == '__main__'" guard, so we work around that
253+
# by treating it like a __main__.py file
254+
# See https://github.com/ipython/ipython/issues/4698
255+
main_name = os.path.splitext(os.path.basename(main_path))[0]
256+
if main_name == 'ipython':
257+
return
258+
259+
# Otherwise, if __file__ already has the setting we expect,
260+
# there's nothing more to do
261+
if getattr(current_main, '__file__', None) == main_path:
262+
return
263+
264+
# If the parent process has sent a path through rather than a module
265+
# name we assume it is an executable script that may contain
266+
# non-main code that needs to be executed
267+
old_main_modules.append(current_main)
268+
main_module = types.ModuleType("__mp_main__")
269+
main_content = runpy.run_path(main_path,
270+
run_name="__mp_main__")
271+
main_module.__dict__.update(main_content)
272+
sys.modules['__main__'] = sys.modules['__mp_main__'] = main_module
211273

212274

213275
def import_main_path(main_path):
214276
'''
215277
Set sys.modules['__main__'] to module at main_path
216278
'''
217-
# XXX (ncoghlan): The following code makes several bogus
218-
# assumptions regarding the relationship between __file__
219-
# and a module's real name. See PEP 302 and issue #10845
220-
if getattr(sys.modules['__main__'], '__file__', None) == main_path:
221-
return
222-
223-
main_name = os.path.splitext(os.path.basename(main_path))[0]
224-
if main_name == '__init__':
225-
main_name = os.path.basename(os.path.dirname(main_path))
226-
227-
if main_name == '__main__':
228-
main_module = sys.modules['__main__']
229-
main_module.__file__ = main_path
230-
elif main_name != 'ipython':
231-
# Main modules not actually called __main__.py may
232-
# contain additional code that should still be executed
233-
import importlib
234-
import types
235-
236-
if main_path is None:
237-
dirs = None
238-
elif os.path.basename(main_path).startswith('__init__.py'):
239-
dirs = [os.path.dirname(os.path.dirname(main_path))]
240-
else:
241-
dirs = [os.path.dirname(main_path)]
242-
243-
assert main_name not in sys.modules, main_name
244-
sys.modules.pop('__mp_main__', None)
245-
# We should not try to load __main__
246-
# since that would execute 'if __name__ == "__main__"'
247-
# clauses, potentially causing a psuedo fork bomb.
248-
main_module = types.ModuleType(main_name)
249-
# XXX Use a target of main_module?
250-
spec = importlib.find_spec(main_name, path=dirs)
251-
if spec is None:
252-
raise ImportError(name=main_name)
253-
methods = importlib._bootstrap._SpecMethods(spec)
254-
methods.init_module_attrs(main_module)
255-
main_module.__name__ = '__mp_main__'
256-
code = spec.loader.get_code(main_name)
257-
exec(code, main_module.__dict__)
258-
259-
old_main_modules.append(sys.modules['__main__'])
260-
sys.modules['__main__'] = sys.modules['__mp_main__'] = main_module
279+
_fixup_main_from_path(main_path)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.