-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
gh-141860: Add on_error= keyword arg to multiprocessing.set_forkserver_preload
#141859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
gpshead
merged 21 commits into
python:main
from
gpshead:claude/forkserver-raise-exceptions-019rrqWeqQGL2zKWava8nshj
Jan 18, 2026
Merged
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
8b7c3ee
Add raise_exceptions parameter to multiprocessing.set_forkserver_preload
gpshead e8836d5
Skip forkserver preload tests on platforms without fork support
gpshead 5ce91ba
Skip all forkserver tests on platforms without fork support
gpshead 75495cc
Refactor set_forkserver_preload to use on_error parameter
gpshead 84c9e5b
Fix unused import and make __notes__ test more robust
gpshead a399218
Fix warn mode to work when warnings are configured as errors
gpshead 9c3ba84
Change __main__ warning message from 'import' to 'preload'
gpshead 70c05d8
Refactor set_forkserver_preload to use _handle_preload helper
gpshead 045be92
Simplify temporary file handling in tests
gpshead 6d4c521
Remove obvious comments and improve import style in tests
gpshead 30c2cf8
Fix warn mode to work when warnings are configured as errors
gpshead bad9691
Add comments explaining exception catching strategy
gpshead 9d8125f
Use double quotes for string values in documentation
gpshead 2f8edb8
Fix warn mode to work when warnings are configured as errors
gpshead 622345d
Add Gregory P. Smith to NEWS entry contributors
gpshead 42e8eb1
Simplify comments and exception note message
gpshead 64ca5a0
Update _send_value docstring to explain pickling requirement
gpshead 5a8bfc6
Merge main into forkserver on_error branch
gpshead 128e7f0
Fix test hang by restoring __main__ state in TestHandlePreload
gpshead 54f9336
Add stderr capture and assertions to forkserver preload tests
gpshead 6ffe214
Refactor preload error handling into _handle_import_error helper
gpshead File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
Lib/test/test_multiprocessing_forkserver/test_preload.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| """Tests for forkserver preload functionality.""" | ||
|
|
||
| import multiprocessing | ||
| import sys | ||
| import tempfile | ||
| import unittest | ||
| from multiprocessing import forkserver | ||
|
|
||
|
|
||
| class TestForkserverPreload(unittest.TestCase): | ||
| """Tests for forkserver preload functionality.""" | ||
|
|
||
| def setUp(self): | ||
| self._saved_warnoptions = sys.warnoptions.copy() | ||
| # Remove warning options that would convert ImportWarning to errors: | ||
| # - 'error' converts all warnings to errors | ||
| # - 'error::ImportWarning' specifically converts ImportWarning | ||
| # Keep other specific options like 'error::BytesWarning' that | ||
| # subprocess's _args_from_interpreter_flags() expects to remove | ||
| sys.warnoptions[:] = [ | ||
| opt for opt in sys.warnoptions | ||
| if opt not in ('error', 'error::ImportWarning') | ||
| ] | ||
| self.ctx = multiprocessing.get_context('forkserver') | ||
| forkserver._forkserver._stop() | ||
|
|
||
| def tearDown(self): | ||
| sys.warnoptions[:] = self._saved_warnoptions | ||
| forkserver._forkserver._stop() | ||
|
|
||
| @staticmethod | ||
| def _send_value(conn, value): | ||
| """Send value through connection. Static method to be picklable as Process target.""" | ||
| conn.send(value) | ||
|
|
||
| def test_preload_on_error_ignore_default(self): | ||
| """Test that invalid modules are silently ignored by default.""" | ||
| self.ctx.set_forkserver_preload(['nonexistent_module_xyz']) | ||
|
|
||
| r, w = self.ctx.Pipe(duplex=False) | ||
| p = self.ctx.Process(target=self._send_value, args=(w, 42)) | ||
| p.start() | ||
| w.close() | ||
| result = r.recv() | ||
| r.close() | ||
| p.join() | ||
|
gpshead marked this conversation as resolved.
|
||
|
|
||
| self.assertEqual(result, 42) | ||
| self.assertEqual(p.exitcode, 0) | ||
|
|
||
| def test_preload_on_error_ignore_explicit(self): | ||
| """Test that invalid modules are silently ignored with on_error='ignore'.""" | ||
| self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='ignore') | ||
|
|
||
| r, w = self.ctx.Pipe(duplex=False) | ||
| p = self.ctx.Process(target=self._send_value, args=(w, 99)) | ||
| p.start() | ||
| w.close() | ||
| result = r.recv() | ||
| r.close() | ||
| p.join() | ||
|
|
||
| self.assertEqual(result, 99) | ||
| self.assertEqual(p.exitcode, 0) | ||
|
|
||
| def test_preload_on_error_warn(self): | ||
| """Test that invalid modules emit warnings with on_error='warn'.""" | ||
| self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='warn') | ||
|
|
||
| r, w = self.ctx.Pipe(duplex=False) | ||
| p = self.ctx.Process(target=self._send_value, args=(w, 123)) | ||
| p.start() | ||
| w.close() | ||
| result = r.recv() | ||
| r.close() | ||
| p.join() | ||
|
|
||
| self.assertEqual(result, 123) | ||
| self.assertEqual(p.exitcode, 0) | ||
|
|
||
| def test_preload_on_error_fail_breaks_context(self): | ||
| """Test that invalid modules with on_error='fail' breaks the forkserver.""" | ||
| self.ctx.set_forkserver_preload(['nonexistent_module_xyz'], on_error='fail') | ||
|
|
||
| r, w = self.ctx.Pipe(duplex=False) | ||
| try: | ||
| p = self.ctx.Process(target=self._send_value, args=(w, 42)) | ||
| with self.assertRaises((EOFError, ConnectionError, BrokenPipeError)) as cm: | ||
| p.start() | ||
| notes = getattr(cm.exception, '__notes__', []) | ||
| self.assertTrue(notes, "Expected exception to have __notes__") | ||
| self.assertIn('Forkserver process may have crashed', notes[0]) | ||
| finally: | ||
| w.close() | ||
| r.close() | ||
|
|
||
| def test_preload_valid_modules_with_on_error_fail(self): | ||
| """Test that valid modules work fine with on_error='fail'.""" | ||
| self.ctx.set_forkserver_preload(['os', 'sys'], on_error='fail') | ||
|
|
||
| r, w = self.ctx.Pipe(duplex=False) | ||
| p = self.ctx.Process(target=self._send_value, args=(w, 'success')) | ||
| p.start() | ||
| w.close() | ||
| result = r.recv() | ||
| r.close() | ||
| p.join() | ||
|
|
||
| self.assertEqual(result, 'success') | ||
| self.assertEqual(p.exitcode, 0) | ||
|
|
||
| def test_preload_invalid_on_error_value(self): | ||
| """Test that invalid on_error values raise ValueError.""" | ||
| with self.assertRaises(ValueError) as cm: | ||
| self.ctx.set_forkserver_preload(['os'], on_error='invalid') | ||
| self.assertIn("on_error must be 'ignore', 'warn', or 'fail'", str(cm.exception)) | ||
|
|
||
|
|
||
| class TestHandlePreload(unittest.TestCase): | ||
| """Unit tests for _handle_preload() function.""" | ||
|
|
||
| def test_handle_preload_main_on_error_fail(self): | ||
| """Test that __main__ import failures raise with on_error='fail'.""" | ||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: | ||
| f.write('raise RuntimeError("test error in __main__")\n') | ||
| f.flush() | ||
| with self.assertRaises(RuntimeError) as cm: | ||
| forkserver._handle_preload(['__main__'], main_path=f.name, on_error='fail') | ||
| self.assertIn("test error in __main__", str(cm.exception)) | ||
|
|
||
| def test_handle_preload_main_on_error_warn(self): | ||
| """Test that __main__ import failures warn with on_error='warn'.""" | ||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: | ||
| f.write('raise ImportError("test import error")\n') | ||
| f.flush() | ||
| with self.assertWarns(ImportWarning) as cm: | ||
| forkserver._handle_preload(['__main__'], main_path=f.name, on_error='warn') | ||
| self.assertIn("Failed to preload __main__", str(cm.warning)) | ||
| self.assertIn("test import error", str(cm.warning)) | ||
|
|
||
| def test_handle_preload_main_on_error_ignore(self): | ||
| """Test that __main__ import failures are ignored with on_error='ignore'.""" | ||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: | ||
| f.write('raise ImportError("test import error")\n') | ||
| f.flush() | ||
| forkserver._handle_preload(['__main__'], main_path=f.name, on_error='ignore') | ||
|
|
||
| def test_handle_preload_main_valid(self): | ||
| """Test that valid __main__ preload works.""" | ||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: | ||
| f.write('test_var = 42\n') | ||
| f.flush() | ||
| forkserver._handle_preload(['__main__'], main_path=f.name, on_error='fail') | ||
|
|
||
| def test_handle_preload_module_on_error_fail(self): | ||
| """Test that module import failures raise with on_error='fail'.""" | ||
| with self.assertRaises(ModuleNotFoundError): | ||
| forkserver._handle_preload(['nonexistent_test_module_xyz'], on_error='fail') | ||
|
|
||
| def test_handle_preload_module_on_error_warn(self): | ||
| """Test that module import failures warn with on_error='warn'.""" | ||
| with self.assertWarns(ImportWarning) as cm: | ||
| forkserver._handle_preload(['nonexistent_test_module_xyz'], on_error='warn') | ||
| self.assertIn("Failed to preload module", str(cm.warning)) | ||
|
|
||
| def test_handle_preload_module_on_error_ignore(self): | ||
| """Test that module import failures are ignored with on_error='ignore'.""" | ||
| forkserver._handle_preload(['nonexistent_test_module_xyz'], on_error='ignore') | ||
|
|
||
| def test_handle_preload_combined(self): | ||
| """Test preloading both __main__ and modules.""" | ||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f: | ||
| f.write('import sys\n') | ||
| f.flush() | ||
| forkserver._handle_preload(['__main__', 'os', 'sys'], main_path=f.name, on_error='fail') | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.