Coverage for coverage / multiproc.py: 73.684%
66 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""Monkey-patching to add multiprocessing support for coverage.py"""
6from __future__ import annotations 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
8import multiprocessing 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
9import multiprocessing.process 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
10import os 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
11import os.path 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
12import sys 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
13import traceback 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
14from typing import Any 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
16from coverage.debug import DebugControl 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
18# An attribute that will be set on the module to indicate that it has been
19# monkey-patched.
20PATCHED_MARKER = "_coverage$patched" 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
23OriginalProcess = multiprocessing.process.BaseProcess 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
24original_bootstrap = OriginalProcess._bootstrap # type: ignore[attr-defined] 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
27class ProcessWithCoverage(OriginalProcess): # pylint: disable=abstract-method 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
28 """A replacement for multiprocess.Process that starts coverage."""
30 def _bootstrap(self, *args, **kwargs): # type: ignore[no-untyped-def] 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
31 """Wrapper around _bootstrap to start coverage."""
32 debug: DebugControl | None = None 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
33 try: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
34 from coverage import Coverage # avoid circular import 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
36 cov = Coverage(data_suffix=True, auto_data=True) 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
37 cov._warn_preimported_source = False 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
38 cov.start() 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
39 _debug = cov._debug 1atbcudevfgwhixjkylmznoApqBrCDs
40 assert _debug is not None 1atbcudevfgwhixjkylmznoApqBrCDs
41 if _debug.should("multiproc"): 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true1atbcudevfgwhixjkylmznoApqBrCDs
42 debug = _debug
43 if debug: 43 ↛ 44line 43 didn't jump to line 44 because the condition on line 43 was never true1atbcudevfgwhixjkylmznoApqBrCDs
44 debug.write("Calling multiprocessing bootstrap")
45 except Exception: 1EaFbGcHdIeJfKgLhMiNjOkWPlXQmYRnZSo0Tp1Uq2Vr3s
46 print("Exception during multiprocessing bootstrap init:", file=sys.stderr) 1EaFbGcHdIeJfKgLhMiNjOkWPlXQmYRnZSo0Tp1Uq2Vr3s
47 traceback.print_exc(file=sys.stderr) 1EaFbGcHdIeJfKgLhMiNjOkWPlXQmYRnZSo0Tp1Uq2Vr3s
48 sys.stderr.flush() 1EaFbGcHdIeJfKgLhMiNjOkWPlXQmYRnZSo0Tp1Uq2Vr3s
49 raise 1EaFbGcHdIeJfKgLhMiNjOkWPlXQmYRnZSo0Tp1Uq2Vr3s
50 try: 1atbcudevfgwhixjkylmznoApqBrCDs
51 return original_bootstrap(self, *args, **kwargs) 1atbcudevfgwhixjkylmznoApqBrCDs
52 finally:
53 if debug: 53 ↛ 54line 53 didn't jump to line 54 because the condition on line 53 was never true1atbcudevfgwhixjkylmznoApqBrCDs
54 debug.write("Finished multiprocessing bootstrap")
55 try: 1atbcudevfgwhixjkylmznoApqBrCDs
56 cov.stop() 1atbcudevfgwhixjkylmznoApqBrCDs
57 cov.save() 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOk9yPlQm!zRnSo#ATpUq$BVrCDs
58 except Exception as exc:
59 if debug:
60 debug.write("Exception during multiprocessing bootstrap cleanup", exc=exc)
61 raise
62 if debug: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOk9yPlQm!zRnSo#ATpUq$BVrCDs
63 debug.write("Saved multiprocessing data")
66class Stowaway: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
67 """An object to pickle, so when it is unpickled, it can apply the monkey-patch."""
69 def __init__(self, rcfile: str) -> None: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
70 self.rcfile = rcfile
72 def __getstate__(self) -> dict[str, str]: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
73 return {"rcfile": self.rcfile}
75 def __setstate__(self, state: dict[str, str]) -> None: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
76 patch_multiprocessing(state["rcfile"]) 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
79def patch_multiprocessing(rcfile: str) -> None: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
80 """Monkey-patch the multiprocessing module.
82 This enables coverage measurement of processes started by multiprocessing.
83 This involves aggressive monkey-patching.
85 `rcfile` is the path to the rcfile being used.
87 """
89 if hasattr(multiprocessing, PATCHED_MARKER): 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
90 return 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
92 OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap # type: ignore[attr-defined] 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
94 # Set the value in ProcessWithCoverage that will be pickled into the child
95 # process.
96 os.environ["COVERAGE_RCFILE"] = os.path.abspath(rcfile) 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
98 # When spawning processes rather than forking them, we have no state in the
99 # new process. We sneak in there with a Stowaway: we stuff one of our own
100 # objects into the data that gets pickled and sent to the subprocess. When
101 # the Stowaway is unpickled, its __setstate__ method is called, which
102 # re-applies the monkey-patch.
103 # Windows only spawns, so this is needed to keep Windows working.
104 try: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
105 from multiprocessing import spawn 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
107 original_get_preparation_data = spawn.get_preparation_data 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
108 except (ImportError, AttributeError):
109 pass
110 else:
112 def get_preparation_data_with_stowaway(name: str) -> dict[str, Any]: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
113 """Get the original preparation data, and also insert our stowaway."""
114 d = original_get_preparation_data(name)
115 d["stowaway"] = Stowaway(rcfile)
116 return d
118 spawn.get_preparation_data = get_preparation_data_with_stowaway 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs
120 setattr(multiprocessing, PATCHED_MARKER, True) 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs