1010import os
1111import site
1212from pathlib import Path
13- from typing import TYPE_CHECKING , Any , Callable , NoReturn
13+ from typing import TYPE_CHECKING , Any , NoReturn
1414
1515from coverage import env
1616from 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