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

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 

3 

4"""Monkey-patching to add multiprocessing support for coverage.py""" 

5 

6from __future__ import annotations 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

7 

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

15 

16from coverage.debug import DebugControl 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

17 

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

21 

22 

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

25 

26 

27class ProcessWithCoverage(OriginalProcess): # pylint: disable=abstract-method 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

28 """A replacement for multiprocess.Process that starts coverage.""" 

29 

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

35 

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") 

64 

65 

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.""" 

68 

69 def __init__(self, rcfile: str) -> None: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

70 self.rcfile = rcfile 

71 

72 def __getstate__(self) -> dict[str, str]: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

73 return {"rcfile": self.rcfile} 

74 

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

77 

78 

79def patch_multiprocessing(rcfile: str) -> None: 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

80 """Monkey-patch the multiprocessing module. 

81 

82 This enables coverage measurement of processes started by multiprocessing. 

83 This involves aggressive monkey-patching. 

84 

85 `rcfile` is the path to the rcfile being used. 

86 

87 """ 

88 

89 if hasattr(multiprocessing, PATCHED_MARKER): 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

90 return 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

91 

92 OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap # type: ignore[attr-defined] 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

93 

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

97 

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

106 

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: 

111 

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 

117 

118 spawn.get_preparation_data = get_preparation_data_with_stowaway 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs

119 

120 setattr(multiprocessing, PATCHED_MARKER, True) 1Ea4tFbGc5uHdIe6vJfKg7wLhMi8xNjOkW9y%PlXQmY!z'RnZSo0#A(Tp1Uq2$B)Vr3CDs