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
« 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
4"""Invasive patches for coverage.py."""
6from __future__ import annotations 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$
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$
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$
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
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$
39 case "execv": 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$
40 _patch_execv(cov, config, debug) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST
42 case "fork": 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$
43 _patch_fork(debug) 1S
45 case "subprocess": 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45zAPBCD67EFQGHI89JKRLMN!#ST$
46 _patch_subprocess(config, debug, make_pth_file) 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST
48 case _: 1abcdUVefghWXijklYZmnop01qrst23uvwxy45zABCD67EFGHI89JKLMN!#ST$
49 raise ConfigError(f"Unknown patch {patch!r}") 1abcdUVefghWXijklYZmnop01qrst23uvwxy45zABCD67EFGHI89JKLMN!#ST$
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$
56 old_exit = os._exit 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$
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)
65 os._exit = coverage_os_exit_patch 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$
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.")
73 debug.write("Patching execv") 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST
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()
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")
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
97 # Without this, it fails on PyPy and Ubuntu.
98 new_env["PATH"] = os.getenv("PATH")
99 old_execv(*args, **kwargs)
101 return coverage_execv_patch 1abcdefghijklmnopqrstuvOwxyzAPBCDEFQGHIJKRLMNST
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
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
112 if env.WINDOWS:
113 raise CoverageException("patch=fork isn't supported yet on Windows.")
115 debug.write("Patching fork")
116 os.register_at_fork(after_in_child=_after_fork_in_child)
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
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
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
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
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.
141PTH_CODE = """\ 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$
142try:
143 import coverage
144except:
145 pass
146else:
147 coverage.process_startup()
148"""
150PTH_TEXT = f"import sys; exec({PTH_CODE!r})\n" 1abcdUVefghWXijklYZmnop01qrst23uvOwxy45%zAPBCD67'EFQGHI89(JKRLMN!#)ST$
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$