Coverage for coverage / collector.py: 63.950%

229 statements  

« prev     ^ index     » next       coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +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"""Raw data collector for coverage.py.""" 

5 

6from __future__ import annotations 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

7 

8import contextlib 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

9import functools 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

10import os 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

11import sys 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

12from collections.abc import Mapping 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

13from types import FrameType 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

14from typing import Any, Callable, TypeVar, cast 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

15 

16from coverage import env 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

17from coverage.core import Core 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

18from coverage.data import CoverageData 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

19from coverage.debug import short_stack 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

20from coverage.exceptions import ConfigError 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

21from coverage.misc import human_sorted_items, isolate_module 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

22from coverage.plugin import CoveragePlugin 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

23from coverage.types import ( 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

24 TArc, 

25 TCheckIncludeFn, 

26 TFileDisposition, 

27 Tracer, 

28 TShouldStartContextFn, 

29 TShouldTraceFn, 

30 TTraceData, 

31 TTraceFn, 

32 TWarnFn, 

33) 

34 

35os = isolate_module(os) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

36 

37 

38T = TypeVar("T") 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

39 

40 

41class Collector: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

42 """Collects trace data. 

43 

44 Creates a Tracer object for each thread, since they track stack 

45 information. Each Tracer points to the same shared data, contributing 

46 traced data points. 

47 

48 When the Collector is started, it creates a Tracer for the current thread, 

49 and installs a function to create Tracers for each new thread started. 

50 When the Collector is stopped, all active Tracers are stopped. 

51 

52 Threads started while the Collector is stopped will never have Tracers 

53 associated with them. 

54 

55 """ 

56 

57 # The stack of active Collectors. Collectors are added here when started, 

58 # and popped when stopped. Collectors on the stack are paused when not 

59 # the top, and resumed when they become the top again. 

60 _collectors: list[Collector] = [] 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

61 

62 def __init__( 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

63 self, 

64 core: Core, 

65 should_trace: TShouldTraceFn, 

66 check_include: TCheckIncludeFn, 

67 should_start_context: TShouldStartContextFn | None, 

68 file_mapper: Callable[[str], str], 

69 branch: bool, 

70 warn: TWarnFn, 

71 concurrency: list[str], 

72 ) -> None: 

73 """Create a collector. 

74 

75 `should_trace` is a function, taking a file name and a frame, and 

76 returning a `coverage.FileDisposition object`. 

77 

78 `check_include` is a function taking a file name and a frame. It returns 

79 a boolean: True if the file should be traced, False if not. 

80 

81 `should_start_context` is a function taking a frame, and returning a 

82 string. If the frame should be the start of a new context, the string 

83 is the new context. If the frame should not be the start of a new 

84 context, return None. 

85 

86 `file_mapper` is a function taking a filename, and returning a Unicode 

87 filename. The result is the name that will be recorded in the data 

88 file. 

89 

90 If `branch` is true, then branches will be measured. This involves 

91 collecting data on which statements followed each other (arcs). Use 

92 `get_arc_data` to get the arc data. 

93 

94 `warn` is a warning function, taking a single string message argument 

95 and an optional slug argument which will be a string or None, to be 

96 used if a warning needs to be issued. 

97 

98 `concurrency` is a list of strings indicating the concurrency libraries 

99 in use. Valid values are "greenlet", "eventlet", "gevent", or "thread" 

100 (the default). "thread" can be combined with one of the other three. 

101 Other values are ignored. 

102 

103 """ 

104 self.core = core 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

105 self.should_trace = should_trace 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

106 self.check_include = check_include 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

107 self.should_start_context = should_start_context 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

108 self.file_mapper = file_mapper 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

109 self.branch = branch 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

110 self.warn = warn 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

111 assert isinstance(concurrency, list), f"Expected a list: {concurrency!r}" 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

112 

113 self.pid = os.getpid() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

114 

115 self.covdata: CoverageData 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

116 self.threading = None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

117 self.static_context: str | None = None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

118 

119 self.origin = short_stack() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

120 

121 self.concur_id_func = None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

122 

123 do_threading = False 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

124 

125 tried = "nothing" # to satisfy pylint 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

126 try: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

127 if "greenlet" in concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

128 tried = "greenlet" 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

129 import greenlet 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

130 

131 self.concur_id_func = greenlet.getcurrent 1dBeCfDgEhFiGjHkIlJmKnLoMaN5bO6cP7QRS

132 elif "eventlet" in concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

133 tried = "eventlet" 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

134 import eventlet.greenthread 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

135 

136 self.concur_id_func = eventlet.greenthread.getcurrent 1dBeCfDgEhFiGjHkIlJmKnLoMaN5bO6cP7

137 elif "gevent" in concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

138 tried = "gevent" 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

139 import gevent 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

140 

141 self.concur_id_func = gevent.getcurrent 1dBeCfDgEhFiGjHkIlJmKnLoMaN5bO6cP7

142 

143 if "thread" in concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

144 do_threading = True 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpWqXrYsZt0u1v2w3x4QRS

145 except ImportError as ex: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

146 msg = f"Couldn't trace with concurrency={tried}, the module isn't installed." 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

147 raise ConfigError(msg) from ex 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

148 

149 if self.concur_id_func and not hasattr(core.tracer_class, "concur_id_func"): 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

150 raise ConfigError( 1BCDEFGHIJKLMN5O6P7QRS

151 "Can't support concurrency={} with {}, only threads are supported.".format( 

152 tried, 

153 self.tracer_name(), 

154 ), 

155 ) 

156 

157 if do_threading or not concurrency: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

158 # It's important to import threading only if we need it. If 

159 # it's imported early, and the program being measured uses 

160 # gevent, then gevent's monkey-patching won't work properly. 

161 import threading 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

162 

163 self.threading = threading 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

164 

165 self.reset() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

166 

167 def __repr__(self) -> str: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

168 return f"<Collector at {id(self):#x}: {self.tracer_name()}>" 

169 

170 def use_data(self, covdata: CoverageData, context: str | None) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

171 """Use `covdata` for recording data.""" 

172 self.covdata = covdata 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

173 self.static_context = context 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

174 self.covdata.set_context(self.static_context) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

175 

176 def tracer_name(self) -> str: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

177 """Return the class name of the tracer we're using.""" 

178 return self.core.tracer_class.__name__ 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

179 

180 def _clear_data(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

181 """Clear out existing data, but stay ready for more collection.""" 

182 # We used to use self.data.clear(), but that would remove filename 

183 # keys and data values that were still in use higher up the stack 

184 # when we are called as part of switch_context. 

185 with self.data_lock or contextlib.nullcontext(): 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

186 for d in self.data.values(): 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

187 d.clear() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

188 

189 for tracer in self.tracers: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

190 tracer.reset_activity() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

191 

192 def reset(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

193 """Clear collected data, and prepare to collect more.""" 

194 self.data_lock = self.threading.Lock() if self.threading else None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

195 

196 # The trace data we are collecting. 

197 self.data: TTraceData = {} 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

198 

199 # A dictionary mapping file names to file tracer plugin names that will 

200 # handle them. 

201 self.file_tracers: dict[str, str] = {} 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

202 

203 self.disabled_plugins: set[str] = set() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

204 

205 # The .should_trace_cache attribute is a cache from file names to 

206 # coverage.FileDisposition objects, or None. When a file is first 

207 # considered for tracing, a FileDisposition is obtained from 

208 # Coverage.should_trace. Its .trace attribute indicates whether the 

209 # file should be traced or not. If it should be, a plugin with dynamic 

210 # file names can decide not to trace it based on the dynamic file name 

211 # being excluded by the inclusion rules, in which case the 

212 # FileDisposition will be replaced by None in the cache. 

213 if env.PYPY: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

214 import __pypy__ # pylint: disable=import-error 1QRS

215 

216 # Alex Gaynor said: 

217 # should_trace_cache is a strictly growing key: once a key is in 

218 # it, it never changes. Further, the keys used to access it are 

219 # generally constant, given sufficient context. That is to say, at 

220 # any given point _trace() is called, pypy is able to know the key. 

221 # This is because the key is determined by the physical source code 

222 # line, and that's invariant with the call site. 

223 # 

224 # This property of a dict with immutable keys, combined with 

225 # call-site-constant keys is a match for PyPy's module dict, 

226 # which is optimized for such workloads. 

227 # 

228 # This gives a 20% benefit on the workload described at 

229 # https://bitbucket.org/pypy/pypy/issue/1871/10x-slower-than-cpython-under-coverage 

230 self.should_trace_cache = __pypy__.newdict("module") 1QRS

231 else: 

232 self.should_trace_cache = {} 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)

233 

234 # Our active Tracers. 

235 self.tracers: list[Tracer] = [] 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

236 

237 self._clear_data() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

238 

239 def lock_data(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

240 """Lock self.data_lock, for use by the C tracer.""" 

241 if self.data_lock is not None: 

242 self.data_lock.acquire() 

243 

244 def unlock_data(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

245 """Unlock self.data_lock, for use by the C tracer.""" 

246 if self.data_lock is not None: 

247 self.data_lock.release() 

248 

249 def _start_tracer(self) -> TTraceFn | None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

250 """Start a new Tracer object, and store it in self.tracers.""" 

251 tracer = self.core.tracer_class(**self.core.tracer_kwargs) 

252 tracer.data = self.data 

253 tracer.lock_data = self.lock_data 

254 tracer.unlock_data = self.unlock_data 

255 tracer.trace_arcs = self.branch 

256 tracer.should_trace = self.should_trace 

257 tracer.should_trace_cache = self.should_trace_cache 

258 tracer.warn = self.warn 

259 

260 if hasattr(tracer, "concur_id_func"): 

261 tracer.concur_id_func = self.concur_id_func 

262 if hasattr(tracer, "file_tracers"): 

263 tracer.file_tracers = self.file_tracers 

264 if hasattr(tracer, "threading"): 

265 tracer.threading = self.threading 

266 if hasattr(tracer, "check_include"): 

267 tracer.check_include = self.check_include 

268 if hasattr(tracer, "should_start_context"): 

269 tracer.should_start_context = self.should_start_context 

270 if hasattr(tracer, "switch_context"): 

271 tracer.switch_context = self.switch_context 

272 if hasattr(tracer, "disable_plugin"): 

273 tracer.disable_plugin = self.disable_plugin 

274 

275 fn = tracer.start() 

276 self.tracers.append(tracer) 

277 

278 return fn 

279 

280 # The trace function has to be set individually on each thread before 

281 # execution begins. Ironically, the only support the threading module has 

282 # for running code before the thread main is the tracing function. So we 

283 # install this as a trace function, and the first time it's called, it does 

284 # the real trace installation. 

285 # 

286 # New in 3.12: threading.settrace_all_threads: https://github.com/python/cpython/pull/96681 

287 

288 def _installation_trace(self, frame: FrameType, event: str, arg: Any) -> TTraceFn | None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

289 """Called on new threads, installs the real tracer.""" 

290 # Remove ourselves as the trace function. 

291 sys.settrace(None) 

292 # Install the real tracer. 

293 fn: TTraceFn | None = self._start_tracer() 

294 # Invoke the real trace function with the current event, to be sure 

295 # not to lose an event. 

296 if fn: 

297 fn = fn(frame, event, arg) 

298 # Return the new trace function to continue tracing in this scope. 

299 return fn 

300 

301 def start(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

302 """Start collecting trace information.""" 

303 # We may be a new collector in a forked process. The old process' 

304 # collectors will be in self._collectors, but they won't be usable. 

305 # Find them and discard them. 

306 keep_collectors = [] 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

307 for c in self._collectors: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

308 if c.pid == self.pid: 308 ↛ 311line 308 didn't jump to line 311 because the condition on line 308 was always true1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

309 keep_collectors.append(c) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

310 else: 

311 c.post_fork() 

312 self._collectors[:] = keep_collectors 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

313 

314 if self._collectors: 314 ↛ 317line 314 didn't jump to line 317 because the condition on line 314 was always true1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

315 self._collectors[-1].pause() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

316 

317 self.tracers = [] 

318 

319 try: 

320 # Install the tracer on this thread. 

321 self._start_tracer() 1QRS

322 except: 

323 if self._collectors: 

324 self._collectors[-1].resume() 

325 raise 

326 

327 # If _start_tracer succeeded, then we add ourselves to the global 

328 # stack of collectors. 

329 self._collectors.append(self) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

330 

331 # Install our installation tracer in threading, to jump-start other 

332 # threads. 

333 if self.core.systrace and self.threading: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

334 self.threading.settrace(self._installation_trace) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

335 

336 def stop(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

337 """Stop collecting trace information.""" 

338 assert self._collectors 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

339 if self._collectors[-1] is not self: 339 ↛ 340line 339 didn't jump to line 340 because the condition on line 339 was never true1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

340 print("self._collectors:") 

341 for c in self._collectors: 

342 print(f" {c!r}\n{c.origin}") 

343 assert self._collectors[-1] is self, ( 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

344 f"Expected current collector to be {self!r}, but it's {self._collectors[-1]!r}" 

345 ) 

346 

347 self.pause() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

348 

349 # Remove this Collector from the stack, and resume the one underneath (if any). 

350 self._collectors.pop() 

351 if self._collectors: 

352 self._collectors[-1].resume() 

353 

354 def pause(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

355 """Pause tracing, but be prepared to `resume`.""" 

356 for tracer in self.tracers: 356 ↛ 363line 356 didn't jump to line 363 because the loop on line 356 didn't complete1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

357 tracer.stop() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

358 stats = tracer.get_stats() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpWqXrYsZt0u1v2w3x4QRS

359 if stats: 359 ↛ 360line 359 didn't jump to line 360 because the condition on line 359 was never true1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaNbOcPpWqXrYsZt0u1v2w3x4QRS

360 print(f"\nCoverage.py {tracer.__class__.__name__} stats:") 

361 for k, v in human_sorted_items(stats.items()): 

362 print(f"{k:>20}: {v}") 

363 if self.threading: 

364 self.threading.settrace(None) 

365 

366 def resume(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

367 """Resume tracing after a `pause`.""" 

368 for tracer in self.tracers: 1defghijklmnoyzAabcpqrstuvwx

369 tracer.start() 1defghijklmnoyzAabcpqrstuvwx

370 if self.core.systrace: 370 ↛ exitline 370 didn't return from function 'resume' because the condition on line 370 was always true1defghijklmnoyzAabcpqrstuvwx

371 if self.threading: 371 ↛ 374line 371 didn't jump to line 374 because the condition on line 371 was always true1defghijklmnoyzAabcpqrstuvwx

372 self.threading.settrace(self._installation_trace) 1defghijklmnoyzAabcpqrstuvwx

373 else: 

374 self._start_tracer() 

375 

376 def post_fork(self) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

377 """After a fork, tracers might need to adjust.""" 

378 for tracer in self.tracers: 

379 if hasattr(tracer, "post_fork"): 

380 tracer.post_fork() 

381 

382 def _activity(self) -> bool: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

383 """Has any activity been traced? 

384 

385 Returns a boolean, True if any trace function was invoked. 

386 

387 """ 

388 return any(tracer.activity() for tracer in self.tracers) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

389 

390 def switch_context(self, new_context: str | None) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

391 """Switch to a new dynamic context.""" 

392 context: str | None 

393 self.flush_data() 

394 if self.static_context: 

395 context = self.static_context 

396 if new_context: 

397 context += "|" + new_context 

398 else: 

399 context = new_context 

400 self.covdata.set_context(context) 

401 

402 def disable_plugin(self, disposition: TFileDisposition) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

403 """Disable the plugin mentioned in `disposition`.""" 

404 file_tracer = disposition.file_tracer 

405 assert file_tracer is not None 

406 plugin = file_tracer._coverage_plugin 

407 plugin_name = plugin._coverage_plugin_name 

408 self.warn(f"Disabling plug-in {plugin_name!r} due to previous exception") 

409 plugin._coverage_enabled = False 

410 disposition.trace = False 

411 

412 @functools.cache # pylint: disable=method-cache-max-size-none 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

413 def cached_mapped_file(self, filename: str) -> str: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

414 """A locally cached version of file names mapped through file_mapper.""" 

415 return self.file_mapper(filename) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

416 

417 def mapped_file_dict(self, d: Mapping[str, T]) -> dict[str, T]: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

418 """Return a dict like d, but with keys modified by file_mapper.""" 

419 # The call to list(items()) ensures that the GIL protects the dictionary 

420 # iterator against concurrent modifications by tracers running 

421 # in other threads. We try three times in case of concurrent 

422 # access, hoping to get a clean copy. 

423 runtime_err = None 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

424 for _ in range(3): # pragma: part covered 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

425 try: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

426 items = list(d.items()) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

427 except RuntimeError as ex: # pragma: cant happen 

428 runtime_err = ex 

429 else: 

430 break 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

431 else: # pragma: cant happen 

432 assert isinstance(runtime_err, Exception) 

433 raise runtime_err 

434 

435 return {self.cached_mapped_file(k): v for k, v in items if v} 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

436 

437 def plugin_was_disabled(self, plugin: CoveragePlugin) -> None: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

438 """Record that `plugin` was disabled during the run.""" 

439 self.disabled_plugins.add(plugin._coverage_plugin_name) 1defghijklmnoyzAabcpqrstuvwx

440 

441 def flush_data(self) -> bool: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

442 """Save the collected data to our associated `CoverageData`. 

443 

444 Data may have also been saved along the way. This forces the 

445 last of the data to be saved. 

446 

447 Returns True if there was data to save, False if not. 

448 """ 

449 if not self._activity(): 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

450 return False 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

451 

452 if self.branch: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

453 if self.core.packed_arcs: 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

454 # Unpack the line number pairs packed into integers. See 

455 # tracer.c:CTracer_record_pair for the C code that creates 

456 # these packed ints. 

457 arc_data: dict[str, list[TArc]] = {} 1defghijklmnoyzAabcpqrstuvwx

458 packed_data = cast(dict[str, set[int]], self.data) 1defghijklmnoyzAabcpqrstuvwx

459 

460 # The list() here and in the inner loop are to get a clean copy 

461 # even as tracers are continuing to add data. 

462 for fname, packeds in list(packed_data.items()): 1defghijklmnoyzAabcpqrstuvwx

463 tuples = [] 1defghijklmnoyzAabcpqrstuvwx

464 for packed in list(packeds): 1defghijklmnoyzAabcpqrstuvwx

465 l1 = packed & 0xFFFFF 1defghijklmnoyzAabcpqrstuvwx

466 l2 = (packed & (0xFFFFF << 20)) >> 20 1defghijklmnoyzAabcpqrstuvwx

467 if packed & (1 << 40): 1defghijklmnoyzAabcpqrstuvwx

468 l1 *= -1 1defghijklmnoyzAabcpqrstuvwx

469 if packed & (1 << 41): 1defghijklmnoyzAabcpqrstuvwx

470 l2 *= -1 1defghijklmnoyzAabcpqrstuvwx

471 tuples.append((l1, l2)) 1defghijklmnoyzAabcpqrstuvwx

472 arc_data[fname] = tuples 1defghijklmnoyzAabcpqrstuvwx

473 else: 

474 arc_data = cast(dict[str, list[TArc]], self.data) 1BCDEFGHIJKLMTUVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

475 self.covdata.add_arcs(self.mapped_file_dict(arc_data)) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

476 else: 

477 line_data = cast(dict[str, set[int]], self.data) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

478 self.covdata.add_lines(self.mapped_file_dict(line_data)) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

479 

480 file_tracers = { 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

481 k: v for k, v in self.file_tracers.items() if v not in self.disabled_plugins 

482 } 

483 self.covdata.add_file_tracers(self.mapped_file_dict(file_tracers)) 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

484 

485 self._clear_data() 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS

486 return True 1dBeCfDgEhFiGjHkIlJmKnLoMyTzUAVaN5bO6cP7pW8qX9rY!sZ#t0$u1%v2'w3(x4)QRS