10
10
import os
11
11
import site
12
12
from pathlib import Path
13
- from typing import TYPE_CHECKING , Any , Callable , NoReturn
13
+ from typing import TYPE_CHECKING , Any , NoReturn
14
14
15
15
from coverage import env
16
16
from coverage .exceptions import ConfigError , CoverageException
@@ -29,87 +29,96 @@ def apply_patches(
29
29
make_pth_file : bool = True ,
30
30
) -> None :
31
31
"""Apply invasive patches requested by `[run] patch=`."""
32
-
33
32
for patch in sorted (set (config .patch )):
34
33
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 )
52
35
53
36
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 )
92
38
93
39
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 )
108
41
109
42
else :
110
43
raise ConfigError (f"Unknown patch { patch !r} " )
111
44
112
45
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
+
113
122
# Writing .pth files is not obvious. On Windows, getsitepackages() returns two
114
123
# directories. A .pth file in the first will be run, but coverage isn't
115
124
# importable yet. We write into all the places we can, but with defensive
0 commit comments