Coverage for coverage / sqldata.py: 100.000%

465 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"""SQLite coverage data.""" 

5 

6from __future__ import annotations 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

7 

8import collections 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

9import datetime 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

10import functools 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

11import glob 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

12import itertools 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

13import os 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

14import random 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

15import socket 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

16import sqlite3 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

17import string 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

18import sys 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

19import textwrap 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

20import threading 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

21import uuid 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

22import zlib 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

23from collections.abc import Collection, Mapping, Sequence 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

24from typing import Any, Callable, cast 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

25 

26from coverage.debug import NoDebugging, auto_repr, file_summary 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

27from coverage.exceptions import CoverageException, DataError 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

28from coverage.misc import file_be_gone, isolate_module 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

29from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

30from coverage.sqlitedb import SqliteDb 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

31from coverage.types import AnyCallable, FilePath, TArc, TDebugCtl, TLineNo, TWarnFn 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

32from coverage.version import __version__ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

33 

34os = isolate_module(os) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

35 

36# If you change the schema: increment the SCHEMA_VERSION and update the 

37# docs in docs/dbschema.rst by running "make cogdoc". 

38 

39SCHEMA_VERSION = 7 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

40 

41# Schema versions: 

42# 1: Released in 5.0a2 

43# 2: Added contexts in 5.0a3. 

44# 3: Replaced line table with line_map table. 

45# 4: Changed line_map.bitmap to line_map.numbits. 

46# 5: Added foreign key declarations. 

47# 6: Key-value in meta. 

48# 7: line_map -> line_bits 

49 

50SCHEMA = """\ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

51CREATE TABLE coverage_schema ( 

52 -- One row, to record the version of the schema in this db. 

53 version integer 

54); 

55 

56CREATE TABLE meta ( 

57 -- Key-value pairs, to record metadata about the data 

58 key text, 

59 value text, 

60 unique (key) 

61 -- Possible keys: 

62 -- 'has_arcs' boolean -- Is this data recording branches? 

63 -- 'sys_argv' text -- The coverage command line that recorded the data. 

64 -- 'version' text -- The version of coverage.py that made the file. 

65 -- 'when' text -- Datetime when the file was created. 

66); 

67 

68CREATE TABLE file ( 

69 -- A row per file measured. 

70 id integer primary key, 

71 path text, 

72 unique (path) 

73); 

74 

75CREATE TABLE context ( 

76 -- A row per context measured. 

77 id integer primary key, 

78 context text, 

79 unique (context) 

80); 

81 

82CREATE TABLE line_bits ( 

83 -- If recording lines, a row per context per file executed. 

84 -- All of the line numbers for that file/context are in one numbits. 

85 file_id integer, -- foreign key to `file`. 

86 context_id integer, -- foreign key to `context`. 

87 numbits blob, -- see the numbits functions in coverage.numbits 

88 foreign key (file_id) references file (id), 

89 foreign key (context_id) references context (id), 

90 unique (file_id, context_id) 

91); 

92 

93CREATE TABLE arc ( 

94 -- If recording branches, a row per context per from/to line transition executed. 

95 file_id integer, -- foreign key to `file`. 

96 context_id integer, -- foreign key to `context`. 

97 fromno integer, -- line number jumped from. 

98 tono integer, -- line number jumped to. 

99 foreign key (file_id) references file (id), 

100 foreign key (context_id) references context (id), 

101 unique (file_id, context_id, fromno, tono) 

102); 

103 

104CREATE TABLE tracer ( 

105 -- A row per file indicating the tracer used for that file. 

106 file_id integer primary key, 

107 tracer text, 

108 foreign key (file_id) references file (id) 

109); 

110""" 

111 

112 

113def _locked(method: AnyCallable) -> AnyCallable: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

114 """A decorator for methods that should hold self._lock.""" 

115 

116 @functools.wraps(method) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

117 def _wrapped(self: CoverageData, *args: Any, **kwargs: Any) -> Any: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

118 if self._debug.should("lock"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

119 self._debug.write(f"Locking {self._lock!r} for {method.__name__}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

120 with self._lock: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

121 if self._debug.should("lock"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

122 self._debug.write(f"Locked {self._lock!r} for {method.__name__}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

123 return method(self, *args, **kwargs) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

124 

125 return _wrapped 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

126 

127 

128class NumbitsUnionAgg: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

129 """SQLite aggregate function for computing union of numbits.""" 

130 

131 def __init__(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

132 self.result = b"" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

133 

134 def step(self, value: bytes) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

135 """Process one value in the aggregation.""" 

136 self.result = numbits_union(self.result, value) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

137 

138 def finalize(self) -> bytes: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

139 """Return the final aggregated result.""" 

140 return self.result 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

141 

142 

143class CoverageData: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

144 """Manages collected coverage data, including file storage. 

145 

146 This class is the public supported API to the data that coverage.py 

147 collects during program execution. It includes information about what code 

148 was executed. It does not include information from the analysis phase, to 

149 determine what lines could have been executed, or what lines were not 

150 executed. 

151 

152 .. note:: 

153 

154 The data file is currently a SQLite database file, with a 

155 :ref:`documented schema <dbschema>`. The schema is subject to change 

156 though, so be careful about querying it directly. Use this API if you 

157 can to isolate yourself from changes. 

158 

159 There are a number of kinds of data that can be collected: 

160 

161 * **lines**: the line numbers of source lines that were executed. 

162 These are always available. 

163 

164 * **arcs**: pairs of source and destination line numbers for transitions 

165 between source lines. These are only available if branch coverage was 

166 used. 

167 

168 * **file tracer names**: the module names of the file tracer plugins that 

169 handled each file in the data. 

170 

171 Lines, arcs, and file tracer names are stored for each source file. File 

172 names in this API are case-sensitive, even on platforms with 

173 case-insensitive file systems. 

174 

175 A data file either stores lines, or arcs, but not both. 

176 

177 A data file is associated with the data when the :class:`CoverageData` 

178 is created, using the parameters `basename`, `suffix`, and `no_disk`. The 

179 base name can be queried with :meth:`base_filename`, and the actual file 

180 name being used is available from :meth:`data_filename`. 

181 

182 To read an existing coverage.py data file, use :meth:`read`. You can then 

183 access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, 

184 or :meth:`file_tracer`. 

185 

186 The :meth:`has_arcs` method indicates whether arc data is available. You 

187 can get a set of the files in the data with :meth:`measured_files`. As 

188 with most Python containers, you can determine if there is any data at all 

189 by using this object as a boolean value. 

190 

191 The contexts for each line in a file can be read with 

192 :meth:`contexts_by_lineno`. 

193 

194 To limit querying to certain contexts, use :meth:`set_query_context` or 

195 :meth:`set_query_contexts`. These will narrow the focus of subsequent 

196 :meth:`lines`, :meth:`arcs`, and :meth:`contexts_by_lineno` calls. The set 

197 of all measured context names can be retrieved with 

198 :meth:`measured_contexts`. 

199 

200 Most data files will be created by coverage.py itself, but you can use 

201 methods here to create data files if you like. The :meth:`add_lines`, 

202 :meth:`add_arcs`, and :meth:`add_file_tracers` methods add data, in ways 

203 that are convenient for coverage.py. 

204 

205 To record data for contexts, use :meth:`set_context` to set a context to 

206 be used for subsequent :meth:`add_lines` and :meth:`add_arcs` calls. 

207 

208 To add a source file without any measured data, use :meth:`touch_file`, 

209 or :meth:`touch_files` for a list of such files. 

210 

211 Write the data to its file with :meth:`write`. 

212 

213 You can clear the data in memory with :meth:`erase`. Data for specific 

214 files can be removed from the database with :meth:`purge_files`. 

215 

216 Two data collections can be combined by using :meth:`update` on one 

217 :class:`CoverageData`, passing it the other. 

218 

219 Data in a :class:`CoverageData` can be serialized and deserialized with 

220 :meth:`dumps` and :meth:`loads`. 

221 

222 The methods used during the coverage.py collection phase 

223 (:meth:`add_lines`, :meth:`add_arcs`, :meth:`set_context`, and 

224 :meth:`add_file_tracers`) are thread-safe. Other methods may not be. 

225 

226 """ 

227 

228 def __init__( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(

229 self, 

230 basename: FilePath | None = None, 

231 suffix: str | bool | None = None, 

232 no_disk: bool = False, 

233 warn: TWarnFn | None = None, 

234 debug: TDebugCtl | None = None, 

235 ) -> None: 

236 """Create a :class:`CoverageData` object to hold coverage-measured data. 

237 

238 Arguments: 

239 basename (str): the base name of the data file, defaulting to 

240 ".coverage". This can be a path to a file in another directory. 

241 suffix (str or bool): has the same meaning as the `data_suffix` 

242 argument to :class:`coverage.Coverage`. 

243 no_disk (bool): if True, keep all data in memory, and don't 

244 write any disk file. 

245 warn: a warning callback function, accepting a warning message 

246 argument. 

247 debug: a `DebugControl` object (optional) 

248 

249 """ 

250 self._no_disk = no_disk 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

251 self._basename = os.path.abspath(basename or ".coverage") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

252 self._suffix = suffix 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

253 self._warn = warn 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

254 self._debug = debug or NoDebugging() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

255 

256 self._choose_filename() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

257 # Maps filenames to row ids. 

258 self._file_map: dict[str, int] = {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

259 # Maps thread ids to SqliteDb objects. 

260 self._dbs: dict[int, SqliteDb] = {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

261 self._pid = os.getpid() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

262 # Synchronize the operations used during collection. 

263 self._lock = threading.RLock() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

264 

265 # Are we in sync with the data file? 

266 self._have_used = False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

267 

268 self._has_lines = False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

269 self._has_arcs = False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

270 

271 self._current_context: str | None = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

272 self._current_context_id: int | None = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

273 self._query_context_ids: list[int] | None = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

274 

275 __repr__ = auto_repr 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

276 

277 def _debug_dataio(self, msg: str, filename: str) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

278 """A helper for debug messages which are all similar.""" 

279 if self._debug.should("dataio"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

280 self._debug.write(f"{msg} {filename!r} ({file_summary(filename)})") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

281 

282 def _choose_filename(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

283 """Set self._filename based on inited attributes.""" 

284 if self._no_disk: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

285 self._filename = f"file:coverage-{uuid.uuid4()}?mode=memory&cache=shared" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

286 else: 

287 self._filename = self._basename 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

288 suffix = filename_suffix(self._suffix) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

289 if suffix: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

290 self._filename += f".{suffix}" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

291 

292 def _reset(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

293 """Reset our attributes.""" 

294 if not self._no_disk: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

295 self.close() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

296 self._file_map = {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

297 self._have_used = False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

298 self._current_context_id = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

299 

300 def close(self, force: bool = False) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

301 """Really close all the database objects.""" 

302 if self._debug.should("dataio"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

303 self._debug.write(f"Closing dbs, force={force}: {self._dbs}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

304 for db in self._dbs.values(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

305 db.close(force=force) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

306 self._dbs = {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

307 

308 def _open_db(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

309 """Open an existing db file, and read its metadata.""" 

310 self._debug_dataio("Opening data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

311 self._dbs[threading.get_ident()] = SqliteDb(self._filename, self._debug, self._no_disk) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

312 self._read_db() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

313 

314 def _read_db(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

315 """Read the metadata from a database so that we are ready to use it.""" 

316 with self._dbs[threading.get_ident()] as db: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

317 try: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

318 row = db.execute_one("select version from coverage_schema") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

319 assert row is not None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

320 except Exception as exc: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

321 if "no such table: coverage_schema" in str(exc): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

322 self._init_db(db) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

323 else: 

324 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

325 "Data file {!r} doesn't seem to be a coverage data file: {}".format( 

326 self._filename, 

327 exc, 

328 ), 

329 ) from exc 

330 else: 

331 schema_version = row[0] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

332 if schema_version != SCHEMA_VERSION: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

333 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

334 "Couldn't use data file {!r}: wrong schema: {} instead of {}".format( 

335 self._filename, 

336 schema_version, 

337 SCHEMA_VERSION, 

338 ), 

339 ) 

340 

341 row = db.execute_one("select value from meta where key = 'has_arcs'") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

342 if row is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

343 self._has_arcs = bool(int(row[0])) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

344 self._has_lines = not self._has_arcs 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

345 

346 with db.execute("select id, path from file") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

347 for file_id, path in cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

348 self._file_map[path] = file_id 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

349 

350 def _init_db(self, db: SqliteDb) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

351 """Write the initial contents of the database.""" 

352 self._debug_dataio("Initing data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

353 db.executescript(SCHEMA) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

354 db.execute_void("INSERT INTO coverage_schema (version) VALUES (?)", (SCHEMA_VERSION,)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

355 

356 # When writing metadata, avoid information that will needlessly change 

357 # the hash of the data file, unless we're debugging processes. 

358 meta_data = [ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

359 ("version", __version__), 

360 ] 

361 if self._debug.should("process"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

362 meta_data.extend( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nF$Q1oG7pH#R2qI6rJ8S3TU4

363 [ 

364 ("sys_argv", str(getattr(sys, "argv", None))), 

365 ("when", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")), 

366 ] 

367 ) 

368 db.executemany_void("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)", meta_data) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

369 

370 def _connect(self) -> SqliteDb: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

371 """Get the SqliteDb object to use.""" 

372 if threading.get_ident() not in self._dbs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

373 self._open_db() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

374 return self._dbs[threading.get_ident()] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

375 

376 def __bool__(self) -> bool: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

377 if threading.get_ident() not in self._dbs and not os.path.exists(self._filename): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

378 return False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

379 try: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

380 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

381 with con.execute("SELECT * FROM file LIMIT 1") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

382 return bool(list(cur)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

383 except CoverageException: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

384 return False 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

385 

386 def dumps(self) -> bytes: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

387 """Serialize the current data to a byte string. 

388 

389 The format of the serialized data is not documented. It is only 

390 suitable for use with :meth:`loads` in the same version of 

391 coverage.py. 

392 

393 Note that this serialization is not what gets stored in coverage data 

394 files. This method is meant to produce bytes that can be transmitted 

395 elsewhere and then deserialized with :meth:`loads`. 

396 

397 Returns: 

398 A byte string of serialized data. 

399 

400 .. versionadded:: 5.0 

401 

402 """ 

403 self._debug_dataio("Dumping data from data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

404 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

405 script = con.dump() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

406 return b"z" + zlib.compress(script.encode("utf-8")) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

407 

408 def loads(self, data: bytes) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

409 """Deserialize data from :meth:`dumps`. 

410 

411 Use with a newly-created empty :class:`CoverageData` object. It's 

412 undefined what happens if the object already has data in it. 

413 

414 Note that this is not for reading data from a coverage data file. It 

415 is only for use on data you produced with :meth:`dumps`. 

416 

417 Arguments: 

418 data: A byte string of serialized data produced by :meth:`dumps`. 

419 

420 .. versionadded:: 5.0 

421 

422 """ 

423 self._debug_dataio("Loading data into data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

424 if data[:1] != b"z": 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

425 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

426 f"Unrecognized serialization: {data[:40]!r} (head of {len(data)} bytes)", 

427 ) 

428 script = zlib.decompress(data[1:]).decode("utf-8") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

429 self._dbs[threading.get_ident()] = db = SqliteDb(self._filename, self._debug, self._no_disk) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

430 with db: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

431 db.executescript(script) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

432 self._read_db() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

433 self._have_used = True 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

434 

435 def _file_id(self, filename: str, add: bool = False) -> int | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

436 """Get the file id for `filename`. 

437 

438 If filename is not in the database yet, add it if `add` is True. 

439 If `add` is not True, return None. 

440 """ 

441 if filename not in self._file_map: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

442 if add: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

443 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

444 self._file_map[filename] = con.execute_for_rowid( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

445 "INSERT OR REPLACE INTO file (path) VALUES (?)", 

446 (filename,), 

447 ) 

448 return self._file_map.get(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

449 

450 def _context_id(self, context: str) -> int | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

451 """Get the id for a context.""" 

452 assert context is not None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

453 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

454 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

455 row = con.execute_one("SELECT id FROM context WHERE context = ?", (context,)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

456 if row is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

457 return cast(int, row[0]) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

458 else: 

459 return None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

460 

461 @_locked 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

462 def set_context(self, context: str | None) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

463 """Set the current context for future :meth:`add_lines` etc. 

464 

465 `context` is a str, the name of the context to use for the next data 

466 additions. The context persists until the next :meth:`set_context`. 

467 

468 .. versionadded:: 5.0 

469 

470 """ 

471 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

472 self._debug.write(f"Setting coverage context: {context!r}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

473 self._current_context = context 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

474 self._current_context_id = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

475 

476 def _set_context_id(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

477 """Use the _current_context to set _current_context_id.""" 

478 context = self._current_context or "" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

479 context_id = self._context_id(context) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

480 if context_id is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

481 self._current_context_id = context_id 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

482 else: 

483 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

484 self._current_context_id = con.execute_for_rowid( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

485 "INSERT INTO context (context) VALUES (?)", 

486 (context,), 

487 ) 

488 

489 def base_filename(self) -> str: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

490 """The base filename for storing data. 

491 

492 .. versionadded:: 5.0 

493 

494 """ 

495 return self._basename 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

496 

497 def data_filename(self) -> str: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

498 """Where is the data stored? 

499 

500 .. versionadded:: 5.0 

501 

502 """ 

503 return self._filename 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

504 

505 @_locked 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

506 def add_lines(self, line_data: Mapping[str, Collection[TLineNo]]) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

507 """Add measured line data. 

508 

509 `line_data` is a dictionary mapping file names to iterables of ints:: 

510 

511 { filename: { line1, line2, ... }, ...} 

512 

513 """ 

514 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

515 self._debug.write( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

516 "Adding lines: %d files, %d lines total" 

517 % ( 

518 len(line_data), 

519 sum(len(lines) for lines in line_data.values()), 

520 ) 

521 ) 

522 if self._debug.should("dataop2"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qI6rJS3TU4

523 for filename, linenos in sorted(line_data.items()): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qI6rJS3TU4

524 self._debug.write(f" {filename}: {linenos}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qI6rJS3TU4

525 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

526 self._choose_lines_or_arcs(lines=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

527 if not line_data: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

528 return 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0)mE9nFQ1%oG7pHR2'qI6rJS3(TU4

529 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

530 self._set_context_id() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

531 for filename, linenos in line_data.items(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

532 line_bits = nums_to_numbits(linenos) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

533 file_id = self._file_id(filename, add=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

534 query = "SELECT numbits FROM line_bits WHERE file_id = ? AND context_id = ?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

535 with con.execute(query, (file_id, self._current_context_id)) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

536 existing = list(cur) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

537 if existing: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

538 line_bits = numbits_union(line_bits, existing[0][0]) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

539 

540 con.execute_void( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

541 """ 

542 INSERT OR REPLACE INTO line_bits 

543 (file_id, context_id, numbits) VALUES (?, ?, ?) 

544 """, 

545 (file_id, self._current_context_id, line_bits), 

546 ) 

547 

548 @_locked 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

549 def add_arcs(self, arc_data: Mapping[str, Collection[TArc]]) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

550 """Add measured arc data. 

551 

552 `arc_data` is a dictionary mapping file names to iterables of pairs of 

553 ints:: 

554 

555 { filename: { (l1,l2), (l1,l2), ... }, ...} 

556 

557 """ 

558 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

559 self._debug.write( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

560 "Adding arcs: %d files, %d arcs total" 

561 % ( 

562 len(arc_data), 

563 sum(len(arcs) for arcs in arc_data.values()), 

564 ) 

565 ) 

566 if self._debug.should("dataop2"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

567 for filename, arcs in sorted(arc_data.items()): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

568 self._debug.write(f" {filename}: {arcs}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

569 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

570 self._choose_lines_or_arcs(arcs=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

571 if not arc_data: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

572 return 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClD!P0mEnF$Q1%oGpHR2'qIrJ8S3(TU4

573 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

574 self._set_context_id() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

575 for filename, arcs in arc_data.items(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

576 if not arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

577 continue 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

578 file_id = self._file_id(filename, add=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

579 data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

580 con.executemany_void( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

581 """ 

582 INSERT OR IGNORE INTO arc 

583 (file_id, context_id, fromno, tono) VALUES (?, ?, ?, ?) 

584 """, 

585 data, 

586 ) 

587 

588 def _choose_lines_or_arcs(self, lines: bool = False, arcs: bool = False) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

589 """Force the data file to choose between lines and arcs.""" 

590 assert lines or arcs 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

591 assert not (lines and arcs) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

592 if lines and self._has_arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

593 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

594 self._debug.write("Error: Can't add line measurements to existing branch data") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

595 raise DataError("Can't add line measurements to existing branch data") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

596 if arcs and self._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

597 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

598 self._debug.write("Error: Can't add branch measurements to existing line data") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

599 raise DataError("Can't add branch measurements to existing line data") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

600 if not self._has_arcs and not self._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

601 self._has_lines = lines 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

602 self._has_arcs = arcs 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

603 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

604 con.execute_void( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

605 "INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)", 

606 ("has_arcs", str(int(arcs))), 

607 ) 

608 

609 @_locked 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

610 def add_file_tracers(self, file_tracers: Mapping[str, str]) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

611 """Add per-file plugin information. 

612 

613 `file_tracers` is { filename: plugin_name, ... } 

614 

615 """ 

616 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

617 self._debug.write(f"Adding file tracers: {len(file_tracers)} files") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

618 if not file_tracers: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

619 return 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

620 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

621 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

622 for filename, plugin_name in file_tracers.items(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

623 file_id = self._file_id(filename, add=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

624 existing_plugin = self.file_tracer(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

625 if existing_plugin: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

626 if existing_plugin != plugin_name: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

627 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

628 "Conflicting file tracer name for '{}': {!r} vs {!r}".format( 

629 filename, 

630 existing_plugin, 

631 plugin_name, 

632 ), 

633 ) 

634 elif plugin_name: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

635 con.execute_void( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

636 "INSERT INTO TRACER (file_id, tracer) VALUES (?, ?)", 

637 (file_id, plugin_name), 

638 ) 

639 

640 def touch_file(self, filename: str, plugin_name: str = "") -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

641 """Ensure that `filename` appears in the data, empty if needed. 

642 

643 `plugin_name` is the name of the plugin responsible for this file. 

644 It is used to associate the right filereporter, etc. 

645 """ 

646 self.touch_files([filename], plugin_name) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

647 

648 def touch_files(self, filenames: Collection[str], plugin_name: str | None = None) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

649 """Ensure that `filenames` appear in the data, empty if needed. 

650 

651 `plugin_name` is the name of the plugin responsible for these files. 

652 It is used to associate the right filereporter, etc. 

653 """ 

654 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

655 self._debug.write(f"Touching {filenames!r}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qIrJS3TU4

656 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

657 with self._connect(): # Use this to get one transaction. 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

658 if not self._has_arcs and not self._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

659 raise DataError("Can't touch files in an empty CoverageData") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4

660 

661 for filename in filenames: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

662 self._file_id(filename, add=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

663 if plugin_name: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

664 # Set the tracer for this file 

665 self.add_file_tracers({filename: plugin_name}) 1abKcdLefMghNijOklPmnQopRqrS

666 

667 def purge_files(self, filenames: Collection[str]) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

668 """Purge any existing coverage data for the given `filenames`. 

669 

670 .. versionadded:: 7.2 

671 

672 """ 

673 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

674 self._debug.write(f"Purging data for {filenames!r}") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

675 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

676 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

677 if self._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

678 sql = "DELETE FROM line_bits WHERE file_id=?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

679 elif self._has_arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

680 sql = "DELETE FROM arc WHERE file_id=?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

681 else: 

682 raise DataError("Can't purge files in an empty CoverageData") 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

683 

684 for filename in filenames: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

685 file_id = self._file_id(filename, add=False) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

686 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

687 continue 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

688 con.execute_void(sql, (file_id,)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

689 

690 def update( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(

691 self, 

692 other_data: CoverageData, 

693 map_path: Callable[[str], str] | None = None, 

694 ) -> None: 

695 """Update this data with data from another :class:`CoverageData`. 

696 

697 If `map_path` is provided, it's a function that re-map paths to match 

698 the local machine's. Note: `map_path` is None only when called 

699 directly from the test suite. 

700 

701 """ 

702 if self._debug.should("dataop"): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

703 self._debug.write( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qIrJS3TU4

704 "Updating with data from {!r}".format( 

705 getattr(other_data, "_filename", "???"), 

706 ) 

707 ) 

708 if self._has_lines and other_data._has_arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

709 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4

710 "Can't combine branch coverage data with statement data", slug="cant-combine" 

711 ) 

712 if self._has_arcs and other_data._has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

713 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4

714 "Can't combine statement coverage data with branch data", slug="cant-combine" 

715 ) 

716 

717 map_path = map_path or (lambda p: p) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

718 

719 # Force the database we're writing to to exist before we start nesting contexts. 

720 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

721 other_data.read() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

722 

723 # Ensure other_data has a properly initialized database 

724 with other_data._connect(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

725 pass 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

726 

727 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

728 assert con.con is not None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

729 con.con.isolation_level = "IMMEDIATE" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

730 

731 # Register functions for SQLite 

732 con.con.create_function("numbits_union", 2, numbits_union) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

733 con.con.create_function("map_path", 1, map_path) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

734 con.con.create_aggregate( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

735 "numbits_union_agg", 

736 1, 

737 NumbitsUnionAgg, # type: ignore[arg-type] 

738 ) 

739 

740 # Attach the other database 

741 con.execute_void("ATTACH DATABASE ? AS other_db", (other_data.data_filename(),)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

742 

743 # Create temporary table with mapped file paths to avoid repeated map_path() calls 

744 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

745 CREATE TEMP TABLE other_file_mapped AS 

746 SELECT 

747 other_file.id as other_file_id, 

748 map_path(other_file.path) as mapped_path 

749 FROM other_db.file AS other_file 

750 """) 

751 

752 # Check for tracer conflicts before proceeding 

753 with con.execute(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

754 SELECT other_file_mapped.mapped_path, 

755 COALESCE(main.tracer.tracer, ''), 

756 COALESCE(other_db.tracer.tracer, '') 

757 FROM main.file 

758 LEFT JOIN main.tracer ON main.file.id = main.tracer.file_id 

759 INNER JOIN other_file_mapped ON main.file.path = other_file_mapped.mapped_path 

760 LEFT JOIN other_db.tracer ON other_file_mapped.other_file_id = other_db.tracer.file_id 

761 WHERE COALESCE(main.tracer.tracer, '') != COALESCE(other_db.tracer.tracer, '') 

762 """) as cur: 

763 conflicts = list(cur) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

764 if conflicts: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

765 path, this_tracer, other_tracer = conflicts[0] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4

766 raise DataError( 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oGpHR2qIrJS3TU4

767 "Conflicting file tracer name for '{}': {!r} vs {!r}".format( 

768 path, 

769 this_tracer, 

770 other_tracer, 

771 ), 

772 ) 

773 

774 # Insert missing files from other_db (with map_path applied) 

775 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

776 INSERT OR IGNORE INTO main.file (path) 

777 SELECT DISTINCT mapped_path FROM other_file_mapped 

778 """) 

779 

780 # Insert missing contexts from other_db 

781 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

782 INSERT OR IGNORE INTO main.context (context) 

783 SELECT context FROM other_db.context 

784 """) 

785 

786 # Update file_map with any new files 

787 with con.execute("SELECT id, path FROM file") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

788 self._file_map.update({path: id for id, path in cur}) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

789 

790 with con.execute(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

791 SELECT 

792 EXISTS(SELECT 1 FROM other_db.arc), 

793 EXISTS(SELECT 1 FROM other_db.line_bits) 

794 """) as cur: 

795 has_arcs, has_lines = cur.fetchone() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

796 

797 # Handle arcs if present in other_db 

798 if has_arcs: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

799 self._choose_lines_or_arcs(arcs=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

800 

801 # Create context mapping table for faster lookups 

802 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

803 CREATE TEMP TABLE context_mapping AS 

804 SELECT 

805 other_context.id as other_id, 

806 main_context.id as main_id 

807 FROM other_db.context AS other_context 

808 INNER JOIN main.context AS main_context ON other_context.context = main_context.context 

809 """) 

810 

811 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

812 INSERT OR IGNORE INTO main.arc (file_id, context_id, fromno, tono) 

813 SELECT 

814 main_file.id, 

815 context_mapping.main_id, 

816 other_arc.fromno, 

817 other_arc.tono 

818 FROM other_db.arc AS other_arc 

819 INNER JOIN other_file_mapped ON other_arc.file_id = other_file_mapped.other_file_id 

820 INNER JOIN context_mapping ON other_arc.context_id = context_mapping.other_id 

821 INNER JOIN main.file AS main_file ON other_file_mapped.mapped_path = main_file.path 

822 """) 

823 

824 # Handle line_bits if present in other_db 

825 if has_lines: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

826 self._choose_lines_or_arcs(lines=True) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

827 

828 # Handle line_bits by aggregating other_db data by mapped target, 

829 # then inserting/updating 

830 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

831 INSERT OR REPLACE INTO main.line_bits (file_id, context_id, numbits) 

832 SELECT 

833 main_file.id, 

834 main_context.id, 

835 numbits_union( 

836 COALESCE(( 

837 SELECT numbits FROM main.line_bits 

838 WHERE file_id = main_file.id AND context_id = main_context.id 

839 ), X''), 

840 aggregated.combined_numbits 

841 ) 

842 FROM ( 

843 SELECT 

844 other_file_mapped.mapped_path, 

845 other_context.context, 

846 numbits_union_agg(other_line_bits.numbits) as combined_numbits 

847 FROM other_db.line_bits AS other_line_bits 

848 INNER JOIN other_file_mapped ON other_line_bits.file_id = other_file_mapped.other_file_id 

849 INNER JOIN other_db.context AS other_context ON other_line_bits.context_id = other_context.id 

850 GROUP BY other_file_mapped.mapped_path, other_context.context 

851 ) AS aggregated 

852 INNER JOIN main.file AS main_file ON aggregated.mapped_path = main_file.path 

853 INNER JOIN main.context AS main_context ON aggregated.context = main_context.context 

854 """) 

855 

856 # Insert tracers from other_db (avoiding conflicts we already checked) 

857 con.execute_void(""" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

858 INSERT OR IGNORE INTO main.tracer (file_id, tracer) 

859 SELECT 

860 main_file.id, 

861 other_tracer.tracer 

862 FROM other_db.tracer AS other_tracer 

863 INNER JOIN other_file_mapped ON other_tracer.file_id = other_file_mapped.other_file_id 

864 INNER JOIN main.file AS main_file ON other_file_mapped.mapped_path = main_file.path 

865 """) 

866 

867 if not self._no_disk: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

868 # Update all internal cache data. 

869 self._reset() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

870 self.read() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

871 

872 def erase(self, parallel: bool = False) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

873 """Erase the data in this object. 

874 

875 If `parallel` is true, then also deletes data files created from the 

876 basename by parallel-mode. 

877 

878 """ 

879 self._reset() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

880 if self._no_disk: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

881 return 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

882 self._debug_dataio("Erasing data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

883 file_be_gone(self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

884 if parallel: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

885 data_dir, local = os.path.split(self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

886 local_abs_path = os.path.join(os.path.abspath(data_dir), local) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

887 pattern = glob.escape(local_abs_path) + ".*" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

888 for filename in glob.glob(pattern): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

889 self._debug_dataio("Erasing parallel data file", filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

890 file_be_gone(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

891 

892 def read(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

893 """Start using an existing data file.""" 

894 if os.path.exists(self._filename): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

895 with self._connect(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

896 self._have_used = True 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

897 

898 def write(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

899 """Ensure the data is written to the data file.""" 

900 self._debug_dataio("Writing (no-op) data file", self._filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

901 

902 def _start_using(self) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

903 """Call this before using the database at all.""" 

904 if self._pid != os.getpid(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

905 # Looks like we forked! Have to start a new data file. 

906 self._reset() 1asbtcudvewfxgyhziAjBkC5lD!mE9nF$oG7pH#qI6rJ8TU

907 self._choose_filename() 1asbtcudvewfxgyhziAjBkC5lD!mE9nF$oG7pH#qI6rJ8TU

908 self._pid = os.getpid() 1asbtcudvewfxgyhziAjBkC5lD!mE9nF$oG7pH#qI6rJ8TU

909 if not self._have_used: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

910 self.erase() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

911 self._have_used = True 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

912 

913 def has_arcs(self) -> bool: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

914 """Does the database have arcs (True) or lines (False).""" 

915 return bool(self._has_arcs) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

916 

917 def measured_files(self) -> set[str]: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

918 """A set of all files that have been measured. 

919 

920 Note that a file may be mentioned as measured even though no lines or 

921 arcs for that file are present in the data. 

922 

923 """ 

924 return set(self._file_map) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

925 

926 def measured_contexts(self) -> set[str]: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

927 """A set of all contexts that have been measured. 

928 

929 .. versionadded:: 5.0 

930 

931 """ 

932 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

933 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

934 with con.execute("SELECT DISTINCT(context) FROM context") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

935 contexts = {row[0] for row in cur} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

936 return contexts 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

937 

938 def file_tracer(self, filename: str) -> str | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

939 """Get the plugin name of the file tracer for a file. 

940 

941 Returns the name of the plugin that handles this file. If the file was 

942 measured, but didn't use a plugin, then "" is returned. If the file 

943 was not measured, then None is returned. 

944 

945 """ 

946 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

947 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

948 file_id = self._file_id(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

949 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

950 return None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4

951 row = con.execute_one("SELECT tracer FROM tracer WHERE file_id = ?", (file_id,)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

952 if row is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

953 return row[0] or "" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4

954 return "" # File was measured, but no tracer associated. 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

955 

956 def set_query_context(self, context: str) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

957 """Set a context for subsequent querying. 

958 

959 The next :meth:`lines`, :meth:`arcs`, or :meth:`contexts_by_lineno` 

960 calls will be limited to only one context. `context` is a string which 

961 must match a context exactly. If it does not, no exception is raised, 

962 but queries will return no data. 

963 

964 .. versionadded:: 5.0 

965 

966 """ 

967 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

968 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

969 with con.execute("SELECT id FROM context WHERE context = ?", (context,)) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

970 self._query_context_ids = [row[0] for row in cur.fetchall()] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

971 

972 def set_query_contexts(self, contexts: Sequence[str] | None) -> None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

973 """Set a number of contexts for subsequent querying. 

974 

975 The next :meth:`lines`, :meth:`arcs`, or :meth:`contexts_by_lineno` 

976 calls will be limited to the specified contexts. `contexts` is a list 

977 of Python regular expressions. Contexts will be matched using 

978 :func:`re.search <python:re.search>`. Data will be included in query 

979 results if they are part of any of the contexts matched. 

980 

981 .. versionadded:: 5.0 

982 

983 """ 

984 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

985 if contexts: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

986 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qI6rJ8S3TU4

987 context_clause = " or ".join(["context REGEXP ?"] * len(contexts)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qI6rJ8S3TU4

988 with con.execute("SELECT id FROM context WHERE " + context_clause, contexts) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qI6rJ8S3TU4

989 self._query_context_ids = [row[0] for row in cur.fetchall()] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mEnFQ1oG7pHR2qI6rJ8S3TU4

990 else: 

991 self._query_context_ids = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

992 

993 def lines(self, filename: str) -> list[TLineNo] | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

994 """Get the list of lines executed for a source file. 

995 

996 If the file was not measured, returns None. A file might be measured, 

997 and have no lines executed, in which case an empty list is returned. 

998 

999 If the file was executed, returns a list of integers, the line numbers 

1000 executed in the file. The list is in no particular order. 

1001 

1002 """ 

1003 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1004 if self.has_arcs(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1005 arcs = self.arcs(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1006 if arcs is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1007 all_lines = itertools.chain.from_iterable(arcs) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1008 return list({l for l in all_lines if l > 0}) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1009 

1010 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1011 file_id = self._file_id(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1012 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1013 return None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

1014 else: 

1015 query = "SELECT numbits FROM line_bits WHERE file_id = ?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1016 data = [file_id] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1017 if self._query_context_ids is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1018 ids_array = ", ".join("?" * len(self._query_context_ids)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mE9nFQ1oG7pHR2qI6rJS3TU4

1019 query += " AND context_id IN (" + ids_array + ")" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mE9nFQ1oG7pHR2qI6rJS3TU4

1020 data += self._query_context_ids 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lDP0mE9nFQ1oG7pHR2qI6rJS3TU4

1021 with con.execute(query, data) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1022 bitmaps = list(cur) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1023 nums = set() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1024 for row in bitmaps: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1025 nums.update(numbits_to_nums(row[0])) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1026 return list(nums) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1027 

1028 def arcs(self, filename: str) -> list[TArc] | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1029 """Get the list of arcs executed for a file. 

1030 

1031 If the file was not measured, returns None. A file might be measured, 

1032 and have no arcs executed, in which case an empty list is returned. 

1033 

1034 If the file was executed, returns a list of 2-tuples of integers. Each 

1035 pair is a starting line number and an ending line number for a 

1036 transition from one line to another. The list is in no particular 

1037 order. 

1038 

1039 Negative numbers have special meaning. If the starting line number is 

1040 -N, it represents an entry to the code object that starts at line N. 

1041 If the ending ling number is -N, it's an exit from the code object that 

1042 starts at line N. 

1043 

1044 """ 

1045 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1046 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1047 file_id = self._file_id(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1048 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1049 return None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

1050 else: 

1051 query = "SELECT DISTINCT fromno, tono FROM arc WHERE file_id = ?" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1052 data = [file_id] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1053 if self._query_context_ids is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1054 ids_array = ", ".join("?" * len(self._query_context_ids)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4

1055 query += " AND context_id IN (" + ids_array + ")" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4

1056 data += self._query_context_ids 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0mE9nFQ1oG7pH#R2qI6rJ8S3TU4

1057 with con.execute(query, data) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1058 return list(cur) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1059 

1060 def contexts_by_lineno(self, filename: str) -> dict[TLineNo, list[str]]: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1061 """Get the contexts for each line in a file. 

1062 

1063 Returns: 

1064 A dict mapping line numbers to a list of context names. 

1065 

1066 .. versionadded:: 5.0 

1067 

1068 """ 

1069 self._start_using() 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1070 with self._connect() as con: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1071 file_id = self._file_id(filename) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1072 if file_id is None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1073 return {} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1074 

1075 lineno_contexts_map = collections.defaultdict(set) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1076 if self.has_arcs(): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1077 query = """ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1078 SELECT arc.fromno, arc.tono, context.context 

1079 FROM arc, context 

1080 WHERE arc.file_id = ? AND arc.context_id = context.id 

1081 """ 

1082 data = [file_id] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1083 if self._query_context_ids is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1084 ids_array = ", ".join("?" * len(self._query_context_ids)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1085 query += " AND arc.context_id IN (" + ids_array + ")" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1086 data += self._query_context_ids 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1087 with con.execute(query, data) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1088 for fromno, tono, context in cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1089 if fromno > 0: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1090 lineno_contexts_map[fromno].add(context) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1091 if tono > 0: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1092 lineno_contexts_map[tono].add(context) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1093 else: 

1094 query = """ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1095 SELECT l.numbits, c.context FROM line_bits l, context c 

1096 WHERE l.context_id = c.id 

1097 AND file_id = ? 

1098 """ 

1099 data = [file_id] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1100 if self._query_context_ids is not None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1101 ids_array = ", ".join("?" * len(self._query_context_ids)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1102 query += " AND l.context_id IN (" + ids_array + ")" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1103 data += self._query_context_ids 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1104 with con.execute(query, data) as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1105 for numbits, context in cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1106 for lineno in numbits_to_nums(numbits): 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1107 lineno_contexts_map[lineno].add(context) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1108 

1109 return {lineno: list(contexts) for lineno, contexts in lineno_contexts_map.items()} 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1110 

1111 @classmethod 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1112 def sys_info(cls) -> list[tuple[str, Any]]: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1113 """Our information for `Coverage.sys_info`. 

1114 

1115 Returns a list of (key, value) pairs. 

1116 

1117 """ 

1118 with SqliteDb(":memory:", debug=NoDebugging()) as db: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1119 with db.execute("PRAGMA temp_store") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1120 temp_store = [row[0] for row in cur] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1121 with db.execute("PRAGMA compile_options") as cur: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1122 copts = [row[0] for row in cur] 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1123 copts = textwrap.wrap(", ".join(copts), width=75) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1124 

1125 return [ 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1126 ("sqlite3_sqlite_version", sqlite3.sqlite_version), 

1127 ("sqlite3_temp_store", temp_store), 

1128 ("sqlite3_compile_options", copts), 

1129 ] 

1130 

1131 

1132def filename_suffix(suffix: str | bool | None) -> str | None: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1133 """Compute a filename suffix for a data file. 

1134 

1135 If `suffix` is a string or None, simply return it. If `suffix` is True, 

1136 then build a suffix incorporating the hostname, process id, and a random 

1137 number. 

1138 

1139 Returns a string or None. 

1140 

1141 """ 

1142 if suffix is True: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1143 # If data_suffix was a simple true value, then make a suffix with 

1144 # plenty of distinguishing information. We do this here in 

1145 # `save()` at the last minute so that the pid will be correct even 

1146 # if the process forks. 

1147 die = random.Random(os.urandom(8)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1148 letters = string.ascii_uppercase + string.ascii_lowercase 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1149 rolls = "".join(die.choice(letters) for _ in range(6)) 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1150 suffix = f"{socket.gethostname()}.{os.getpid()}.X{rolls}x" 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1151 elif suffix is False: 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4

1152 suffix = None 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkClDP0mEnFQ1oGpHR2qIrJS3TU4

1153 return suffix 1asbtKVcudvLWewfxMXgyhzNYiAjBOZkC5lD!P0)mE9nF$Q1%oG7pH#R2'qI6rJ8S3(TU4