Coverage for coverage / patch.py: 69.231%

87 statements  

« prev     ^ index     » next       coverage.py v7.12.1a0.dev1, created at 2025-11-29 20:34 +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"""Invasive patches for coverage.py.""" 

5 

6from __future__ import annotations 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

7 

8import atexit 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

9import contextlib 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

10import os 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

11import site 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

12from pathlib import Path 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

13from typing import TYPE_CHECKING, Any, NoReturn 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

14 

15from coverage import env 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

16from coverage.debug import NoDebugging, DevNullDebug 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

17from coverage.exceptions import ConfigError, CoverageException 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

18 

19if TYPE_CHECKING: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

20 from coverage import Coverage 

21 from coverage.config import CoverageConfig 

22 from coverage.types import TDebugCtl 

23 

24 

25def apply_patches( 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

26 cov: Coverage, 

27 config: CoverageConfig, 

28 debug: TDebugCtl, 

29 *, 

30 make_pth_file: bool = True, 

31) -> None: 

32 """Apply invasive patches requested by `[run] patch=`.""" 

33 debug = debug if debug.should("patch") else DevNullDebug() 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

34 for patch in sorted(set(config.patch)): 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

35 match patch: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$

36 case "_exit": 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$

37 _patch__exit(cov, debug) 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$

38 

39 case "execv": 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$

40 _patch_execv(cov, config, debug) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

41 

42 case "fork": 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$

43 _patch_fork(debug) 1S

44 

45 case "subprocess": 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$

46 _patch_subprocess(config, debug, make_pth_file) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

47 

48 case _: 1abcdUVefghWXijklYZmnop01qrst23uvwxy45zABCD67EFGHI89JKLMN!#ST$

49 raise ConfigError(f"Unknown patch {patch!r}") 1abcdUVefghWXijklYZmnop01qrst23uvwxy45zABCD67EFGHI89JKLMN!#ST$

50 

51 

52def _patch__exit(cov: Coverage, debug: TDebugCtl) -> None: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

53 """Patch os._exit.""" 

54 debug.write("Patching _exit") 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

55 

56 old_exit = os._exit 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

57 

58 def coverage_os_exit_patch(status: int) -> NoReturn: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

59 with contextlib.suppress(Exception): 

60 debug.write(f"Using _exit patch with {cov = }") 

61 with contextlib.suppress(Exception): 

62 cov.save() 

63 old_exit(status) 

64 

65 os._exit = coverage_os_exit_patch 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

66 

67 

68def _patch_execv(cov: Coverage, config: CoverageConfig, debug: TDebugCtl) -> None: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

69 """Patch the execv family of functions.""" 

70 if env.WINDOWS: 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

71 raise CoverageException("patch=execv isn't supported yet on Windows.") 

72 

73 debug.write("Patching execv") 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

74 

75 def make_execv_patch(fname: str, old_execv: Any) -> Any: 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

76 def coverage_execv_patch(*args: Any, **kwargs: Any) -> Any: 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

77 with contextlib.suppress(Exception): 

78 debug.write(f"Using execv patch for {fname} with {cov = }") 

79 with contextlib.suppress(Exception): 

80 cov.save() 

81 

82 if fname.endswith("e"): 

83 # Assume the `env` argument is passed positionally. 

84 new_env = args[-1] 

85 # Pass our configuration in the new environment. 

86 new_env["COVERAGE_PROCESS_CONFIG"] = config.serialize() 

87 if env.TESTING: 

88 # The subprocesses need to use the same core as the main process. 

89 new_env["COVERAGE_CORE"] = os.getenv("COVERAGE_CORE") 

90 

91 # When testing locally, we need to honor the pyc file location 

92 # or they get written to the .tox directories and pollute the 

93 # next run with a different core. 

94 if (cache_prefix := os.getenv("PYTHONPYCACHEPREFIX")) is not None: 

95 new_env["PYTHONPYCACHEPREFIX"] = cache_prefix 

96 

97 # Without this, it fails on PyPy and Ubuntu. 

98 new_env["PATH"] = os.getenv("PATH") 

99 old_execv(*args, **kwargs) 

100 

101 return coverage_execv_patch 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

102 

103 # All the exec* and spawn* functions eventually call execv or execve. 

104 os.execv = make_execv_patch("execv", os.execv) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

105 os.execve = make_execv_patch("execve", os.execve) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

106 

107 

108def _patch_fork(debug: TDebugCtl) -> None: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

109 """Ensure Coverage is properly reset after a fork.""" 

110 from coverage.control import _after_fork_in_child 

111 

112 if env.WINDOWS: 

113 raise CoverageException("patch=fork isn't supported yet on Windows.") 

114 

115 debug.write("Patching fork") 

116 os.register_at_fork(after_in_child=_after_fork_in_child) 

117 

118 

119def _patch_subprocess(config: CoverageConfig, debug: TDebugCtl, make_pth_file: bool) -> None: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

120 """Write .pth files and set environment vars to measure subprocesses.""" 

121 debug.write("Patching subprocess") 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

122 

123 if make_pth_file: 123 ↛ 132line 123 didn't jump to line 132 because the condition on line 123 was always true1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

124 pth_files = create_pth_files(debug) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

125 

126 def delete_pth_files() -> None: 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

127 for p in pth_files: 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMN

128 debug.write(f"Deleting subprocess .pth file: {str(p)!r}") 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMN

129 p.unlink(missing_ok=True) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMN

130 

131 atexit.register(delete_pth_files) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

132 assert config.config_file is not None 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

133 os.environ["COVERAGE_PROCESS_CONFIG"] = config.serialize() 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST

134 

135 

136# Writing .pth files is not obvious. On Windows, getsitepackages() returns two 

137# directories. A .pth file in the first will be run, but coverage isn't 

138# importable yet. We write into all the places we can, but with defensive 

139# import code. 

140 

141PTH_CODE = """\ 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

142try: 

143 import coverage 

144except: 

145 pass 

146else: 

147 coverage.process_startup() 

148""" 

149 

150PTH_TEXT = f"import sys; exec({PTH_CODE!r})\n" 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

151 

152 

153def create_pth_files(debug: TDebugCtl = NoDebugging()) -> list[Path]: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

154 """Create .pth files for measuring subprocesses.""" 

155 pth_files = [] 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

156 for pth_dir in site.getsitepackages(): 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

157 pth_file = Path(pth_dir) / f"subcover_{os.getpid()}.pth" 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

158 try: 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

159 if debug.should("patch"): 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

160 debug.write(f"Writing subprocess .pth file: {str(pth_file)!r}") 

161 pth_file.write_text(PTH_TEXT, encoding="utf-8") 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

162 except OSError as oserr: # pragma: cant happen 

163 if debug.should("patch"): 

164 debug.write(f"Couldn't write subprocess .pth file: {oserr}") 

165 continue 

166 else: 

167 pth_files.append(pth_file) 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

168 if debug.should("patch"): 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$

169 debug.write(f"Subprocess .pth files created: {', '.join(map(str, pth_files)) or '-none-'}") 

170 return pth_files 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$