Skip to content

Commit 1198fea

Browse files
committed
refactor: split up apply_patches
Some of the nested functions in the old code were to avoid warnings about closure cells assigned in a loop. We knew the loop wasn't a problem, but this way, it's definitely not a problem, and we can see each patch individually more easily.
1 parent 817542f commit 1198fea

File tree

1 file changed

+80
-71
lines changed

1 file changed

+80
-71
lines changed

coverage/patch.py

Lines changed: 80 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import os
1111
import site
1212
from pathlib import Path
13-
from typing import TYPE_CHECKING, Any, Callable, NoReturn
13+
from typing import TYPE_CHECKING, Any, NoReturn
1414

1515
from coverage import env
1616
from coverage.exceptions import ConfigError, CoverageException
@@ -29,87 +29,96 @@ def apply_patches(
2929
make_pth_file: bool = True,
3030
) -> None:
3131
"""Apply invasive patches requested by `[run] patch=`."""
32-
3332
for patch in sorted(set(config.patch)):
3433
if patch == "_exit":
35-
if debug.should("patch"):
36-
debug.write("Patching _exit")
37-
38-
def make_exit_patch(
39-
old_exit: Callable[[int], NoReturn],
40-
) -> Callable[[int], NoReturn]:
41-
def coverage_os_exit_patch(status: int) -> NoReturn:
42-
with contextlib.suppress(Exception):
43-
if debug.should("patch"):
44-
debug.write("Using _exit patch")
45-
with contextlib.suppress(Exception):
46-
cov.save()
47-
old_exit(status)
48-
49-
return coverage_os_exit_patch
50-
51-
os._exit = make_exit_patch(os._exit) # type: ignore[assignment]
34+
_patch__exit(cov, debug)
5235

5336
elif patch == "execv":
54-
if env.WINDOWS:
55-
raise CoverageException("patch=execv isn't supported yet on Windows.")
56-
57-
if debug.should("patch"):
58-
debug.write("Patching execv")
59-
60-
def make_execv_patch(fname: str, old_execv: Any) -> Any:
61-
def coverage_execv_patch(*args: Any, **kwargs: Any) -> Any:
62-
with contextlib.suppress(Exception):
63-
if debug.should("patch"):
64-
debug.write(f"Using execv patch for {fname}")
65-
with contextlib.suppress(Exception):
66-
cov.save()
67-
68-
if fname.endswith("e"):
69-
# Assume the `env` argument is passed positionally.
70-
new_env = args[-1]
71-
# Pass our environment variable in the new environment.
72-
new_env["COVERAGE_PROCESS_START"] = config.config_file
73-
if env.TESTING:
74-
# The subprocesses need to use the same core as the main process.
75-
new_env["COVERAGE_CORE"] = os.getenv("COVERAGE_CORE")
76-
77-
# When testing locally, we need to honor the pyc file location
78-
# or they get written to the .tox directories and pollute the
79-
# next run with a different core.
80-
if (cache_prefix := os.getenv("PYTHONPYCACHEPREFIX")) is not None:
81-
new_env["PYTHONPYCACHEPREFIX"] = cache_prefix
82-
83-
# Without this, it fails on PyPy and Ubuntu.
84-
new_env["PATH"] = os.getenv("PATH")
85-
old_execv(*args, **kwargs)
86-
87-
return coverage_execv_patch
88-
89-
# All the exec* and spawn* functions eventually call execv or execve.
90-
os.execv = make_execv_patch("execv", os.execv)
91-
os.execve = make_execv_patch("execve", os.execve)
37+
_patch_execv(cov, config, debug)
9238

9339
elif patch == "subprocess":
94-
if debug.should("patch"):
95-
debug.write("Patching subprocess")
96-
97-
if make_pth_file:
98-
pth_files = create_pth_files()
99-
def make_deleter(pth_files: list[Path]) -> Callable[[], None]:
100-
def delete_pth_files() -> None:
101-
for p in pth_files:
102-
p.unlink(missing_ok=True)
103-
return delete_pth_files
104-
atexit.register(make_deleter(pth_files))
105-
assert config.config_file is not None
106-
os.environ["COVERAGE_PROCESS_START"] = config.config_file
107-
os.environ["COVERAGE_PROCESS_DATAFILE"] = os.path.abspath(config.data_file)
40+
_patch_subprocess(config, debug, make_pth_file)
10841

10942
else:
11043
raise ConfigError(f"Unknown patch {patch!r}")
11144

11245

46+
def _patch__exit(cov: Coverage, debug: TDebugCtl) -> None:
47+
"""Patch os._exit."""
48+
if debug.should("patch"):
49+
debug.write("Patching _exit")
50+
51+
old_exit = os._exit
52+
53+
def coverage_os_exit_patch(status: int) -> NoReturn:
54+
with contextlib.suppress(Exception):
55+
if debug.should("patch"):
56+
debug.write("Using _exit patch")
57+
with contextlib.suppress(Exception):
58+
cov.save()
59+
old_exit(status)
60+
61+
os._exit = coverage_os_exit_patch
62+
63+
64+
def _patch_execv(cov: Coverage, config: CoverageConfig, debug: TDebugCtl) -> None:
65+
"""Patch the execv family of functions."""
66+
if env.WINDOWS:
67+
raise CoverageException("patch=execv isn't supported yet on Windows.")
68+
69+
if debug.should("patch"):
70+
debug.write("Patching execv")
71+
72+
def make_execv_patch(fname: str, old_execv: Any) -> Any:
73+
def coverage_execv_patch(*args: Any, **kwargs: Any) -> Any:
74+
with contextlib.suppress(Exception):
75+
if debug.should("patch"):
76+
debug.write(f"Using execv patch for {fname}")
77+
with contextlib.suppress(Exception):
78+
cov.save()
79+
80+
if fname.endswith("e"):
81+
# Assume the `env` argument is passed positionally.
82+
new_env = args[-1]
83+
# Pass our environment variable in the new environment.
84+
new_env["COVERAGE_PROCESS_START"] = config.config_file
85+
if env.TESTING:
86+
# The subprocesses need to use the same core as the main process.
87+
new_env["COVERAGE_CORE"] = os.getenv("COVERAGE_CORE")
88+
89+
# When testing locally, we need to honor the pyc file location
90+
# or they get written to the .tox directories and pollute the
91+
# next run with a different core.
92+
if (cache_prefix := os.getenv("PYTHONPYCACHEPREFIX")) is not None:
93+
new_env["PYTHONPYCACHEPREFIX"] = cache_prefix
94+
95+
# Without this, it fails on PyPy and Ubuntu.
96+
new_env["PATH"] = os.getenv("PATH")
97+
old_execv(*args, **kwargs)
98+
99+
return coverage_execv_patch
100+
101+
# All the exec* and spawn* functions eventually call execv or execve.
102+
os.execv = make_execv_patch("execv", os.execv)
103+
os.execve = make_execv_patch("execve", os.execve)
104+
105+
106+
def _patch_subprocess(config: CoverageConfig, debug: TDebugCtl, make_pth_file: bool) -> None:
107+
"""Write .pth files and set environment vars to measure subprocesses."""
108+
if debug.should("patch"):
109+
debug.write("Patching subprocess")
110+
111+
if make_pth_file:
112+
pth_files = create_pth_files()
113+
def delete_pth_files() -> None:
114+
for p in pth_files:
115+
p.unlink(missing_ok=True)
116+
atexit.register(delete_pth_files)
117+
assert config.config_file is not None
118+
os.environ["COVERAGE_PROCESS_START"] = config.config_file
119+
os.environ["COVERAGE_PROCESS_DATAFILE"] = os.path.abspath(config.data_file)
120+
121+
113122
# Writing .pth files is not obvious. On Windows, getsitepackages() returns two
114123
# directories. A .pth file in the first will be run, but coverage isn't
115124
# importable yet. We write into all the places we can, but with defensive

0 commit comments

Comments
 (0)