coverage 7.11.2__cp314-cp314t-musllinux_1_2_riscv64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coverage/__init__.py +40 -0
- coverage/__main__.py +12 -0
- coverage/annotate.py +114 -0
- coverage/bytecode.py +196 -0
- coverage/cmdline.py +1184 -0
- coverage/collector.py +486 -0
- coverage/config.py +731 -0
- coverage/context.py +74 -0
- coverage/control.py +1481 -0
- coverage/core.py +142 -0
- coverage/data.py +227 -0
- coverage/debug.py +669 -0
- coverage/disposition.py +59 -0
- coverage/env.py +135 -0
- coverage/exceptions.py +91 -0
- coverage/execfile.py +329 -0
- coverage/files.py +553 -0
- coverage/html.py +856 -0
- coverage/htmlfiles/coverage_html.js +733 -0
- coverage/htmlfiles/favicon_32.png +0 -0
- coverage/htmlfiles/index.html +164 -0
- coverage/htmlfiles/keybd_closed.png +0 -0
- coverage/htmlfiles/pyfile.html +149 -0
- coverage/htmlfiles/style.css +377 -0
- coverage/htmlfiles/style.scss +824 -0
- coverage/inorout.py +614 -0
- coverage/jsonreport.py +188 -0
- coverage/lcovreport.py +219 -0
- coverage/misc.py +373 -0
- coverage/multiproc.py +120 -0
- coverage/numbits.py +146 -0
- coverage/parser.py +1213 -0
- coverage/patch.py +166 -0
- coverage/phystokens.py +197 -0
- coverage/plugin.py +617 -0
- coverage/plugin_support.py +299 -0
- coverage/py.typed +1 -0
- coverage/python.py +269 -0
- coverage/pytracer.py +369 -0
- coverage/regions.py +127 -0
- coverage/report.py +298 -0
- coverage/report_core.py +117 -0
- coverage/results.py +471 -0
- coverage/sqldata.py +1153 -0
- coverage/sqlitedb.py +239 -0
- coverage/sysmon.py +482 -0
- coverage/templite.py +306 -0
- coverage/tomlconfig.py +210 -0
- coverage/tracer.cpython-314t-riscv64-linux-musl.so +0 -0
- coverage/tracer.pyi +43 -0
- coverage/types.py +206 -0
- coverage/version.py +35 -0
- coverage/xmlreport.py +264 -0
- coverage-7.11.2.dist-info/METADATA +221 -0
- coverage-7.11.2.dist-info/RECORD +59 -0
- coverage-7.11.2.dist-info/WHEEL +5 -0
- coverage-7.11.2.dist-info/entry_points.txt +4 -0
- coverage-7.11.2.dist-info/licenses/LICENSE.txt +177 -0
- coverage-7.11.2.dist-info/top_level.txt +1 -0
coverage/misc.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
+
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
|
3
|
+
|
|
4
|
+
"""Miscellaneous stuff for coverage.py."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import contextlib
|
|
9
|
+
import datetime
|
|
10
|
+
import errno
|
|
11
|
+
import functools
|
|
12
|
+
import hashlib
|
|
13
|
+
import importlib
|
|
14
|
+
import importlib.util
|
|
15
|
+
import inspect
|
|
16
|
+
import os
|
|
17
|
+
import os.path
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import types
|
|
21
|
+
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
22
|
+
from types import ModuleType
|
|
23
|
+
from typing import Any, NoReturn, TypeVar
|
|
24
|
+
|
|
25
|
+
# In 6.0, the exceptions moved from misc.py to exceptions.py. But a number of
|
|
26
|
+
# other packages were importing the exceptions from misc, so import them here.
|
|
27
|
+
# pylint: disable=unused-wildcard-import
|
|
28
|
+
from coverage.exceptions import * # pylint: disable=wildcard-import
|
|
29
|
+
from coverage.exceptions import CoverageException
|
|
30
|
+
from coverage.types import TArc
|
|
31
|
+
|
|
32
|
+
ISOLATED_MODULES: dict[ModuleType, ModuleType] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def isolate_module(mod: ModuleType) -> ModuleType:
|
|
36
|
+
"""Copy a module so that we are isolated from aggressive mocking.
|
|
37
|
+
|
|
38
|
+
If a test suite mocks os.path.exists (for example), and then we need to use
|
|
39
|
+
it during the test, everything will get tangled up if we use their mock.
|
|
40
|
+
Making a copy of the module when we import it will isolate coverage.py from
|
|
41
|
+
those complications.
|
|
42
|
+
"""
|
|
43
|
+
if mod not in ISOLATED_MODULES:
|
|
44
|
+
new_mod = types.ModuleType(mod.__name__)
|
|
45
|
+
ISOLATED_MODULES[mod] = new_mod
|
|
46
|
+
for name in dir(mod):
|
|
47
|
+
value = getattr(mod, name)
|
|
48
|
+
if isinstance(value, types.ModuleType):
|
|
49
|
+
value = isolate_module(value)
|
|
50
|
+
setattr(new_mod, name, value)
|
|
51
|
+
return ISOLATED_MODULES[mod]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
os = isolate_module(os)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SysModuleSaver:
|
|
58
|
+
"""Saves the contents of sys.modules, and removes new modules later."""
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self.old_modules = set(sys.modules)
|
|
62
|
+
|
|
63
|
+
def restore(self) -> None:
|
|
64
|
+
"""Remove any modules imported since this object started."""
|
|
65
|
+
new_modules = set(sys.modules) - self.old_modules
|
|
66
|
+
for m in new_modules:
|
|
67
|
+
del sys.modules[m]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@contextlib.contextmanager
|
|
71
|
+
def sys_modules_saved() -> Iterator[None]:
|
|
72
|
+
"""A context manager to remove any modules imported during a block."""
|
|
73
|
+
saver = SysModuleSaver()
|
|
74
|
+
try:
|
|
75
|
+
yield
|
|
76
|
+
finally:
|
|
77
|
+
saver.restore()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def import_third_party(modname: str) -> tuple[ModuleType, bool]:
|
|
81
|
+
"""Import a third-party module we need, but might not be installed.
|
|
82
|
+
|
|
83
|
+
This also cleans out the module after the import, so that coverage won't
|
|
84
|
+
appear to have imported it. This lets the third party use coverage for
|
|
85
|
+
their own tests.
|
|
86
|
+
|
|
87
|
+
Arguments:
|
|
88
|
+
modname (str): the name of the module to import.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The imported module, and a boolean indicating if the module could be imported.
|
|
92
|
+
|
|
93
|
+
If the boolean is False, the module returned is not the one you want: don't use it.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
with sys_modules_saved():
|
|
97
|
+
try:
|
|
98
|
+
return importlib.import_module(modname), True
|
|
99
|
+
except ImportError:
|
|
100
|
+
return sys, False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def nice_pair(pair: TArc) -> str:
|
|
104
|
+
"""Make a nice string representation of a pair of numbers.
|
|
105
|
+
|
|
106
|
+
If the numbers are equal, just return the number, otherwise return the pair
|
|
107
|
+
with a dash between them, indicating the range.
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
start, end = pair
|
|
111
|
+
if start == end:
|
|
112
|
+
return f"{start}"
|
|
113
|
+
else:
|
|
114
|
+
return f"{start}-{end}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def bool_or_none(b: Any) -> bool | None:
|
|
118
|
+
"""Return bool(b), but preserve None."""
|
|
119
|
+
if b is None:
|
|
120
|
+
return None
|
|
121
|
+
else:
|
|
122
|
+
return bool(b)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def join_regex(regexes: Iterable[str]) -> str:
|
|
126
|
+
"""Combine a series of regex strings into one that matches any of them."""
|
|
127
|
+
regexes = list(regexes)
|
|
128
|
+
if len(regexes) == 1:
|
|
129
|
+
return regexes[0]
|
|
130
|
+
else:
|
|
131
|
+
return "|".join(f"(?:{r})" for r in regexes)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def file_be_gone(path: str) -> None:
|
|
135
|
+
"""Remove a file, and don't get annoyed if it doesn't exist."""
|
|
136
|
+
try:
|
|
137
|
+
os.remove(path)
|
|
138
|
+
except OSError as e:
|
|
139
|
+
if e.errno != errno.ENOENT:
|
|
140
|
+
raise
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def ensure_dir(directory: str) -> None:
|
|
144
|
+
"""Make sure the directory exists.
|
|
145
|
+
|
|
146
|
+
If `directory` is None or empty, do nothing.
|
|
147
|
+
"""
|
|
148
|
+
if directory:
|
|
149
|
+
os.makedirs(directory, exist_ok=True)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def ensure_dir_for_file(path: str) -> None:
|
|
153
|
+
"""Make sure the directory for the path exists."""
|
|
154
|
+
ensure_dir(os.path.dirname(path))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Hasher:
|
|
158
|
+
"""Hashes Python data for fingerprinting."""
|
|
159
|
+
|
|
160
|
+
def __init__(self) -> None:
|
|
161
|
+
self.hash = hashlib.new("sha3_256", usedforsecurity=False)
|
|
162
|
+
|
|
163
|
+
def update(self, v: Any) -> None:
|
|
164
|
+
"""Add `v` to the hash, recursively if needed."""
|
|
165
|
+
self.hash.update(str(type(v)).encode("utf-8"))
|
|
166
|
+
match v:
|
|
167
|
+
case None:
|
|
168
|
+
pass
|
|
169
|
+
case str():
|
|
170
|
+
self.hash.update(v.encode("utf-8"))
|
|
171
|
+
case bytes():
|
|
172
|
+
self.hash.update(v)
|
|
173
|
+
case int() | float():
|
|
174
|
+
self.hash.update(str(v).encode("utf-8"))
|
|
175
|
+
case tuple() | list():
|
|
176
|
+
for e in v:
|
|
177
|
+
self.update(e)
|
|
178
|
+
case dict():
|
|
179
|
+
keys = v.keys()
|
|
180
|
+
for k in sorted(keys):
|
|
181
|
+
self.update(k)
|
|
182
|
+
self.update(v[k])
|
|
183
|
+
case _:
|
|
184
|
+
for k in dir(v):
|
|
185
|
+
if k.startswith("__"):
|
|
186
|
+
continue
|
|
187
|
+
a = getattr(v, k)
|
|
188
|
+
if inspect.isroutine(a):
|
|
189
|
+
continue
|
|
190
|
+
self.update(k)
|
|
191
|
+
self.update(a)
|
|
192
|
+
self.hash.update(b".")
|
|
193
|
+
|
|
194
|
+
def hexdigest(self) -> str:
|
|
195
|
+
"""Retrieve the hex digest of the hash."""
|
|
196
|
+
return self.hash.hexdigest()[:32]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _needs_to_implement(that: Any, func_name: str) -> NoReturn:
|
|
200
|
+
"""Helper to raise NotImplementedError in interface stubs."""
|
|
201
|
+
if hasattr(that, "_coverage_plugin_name"):
|
|
202
|
+
thing = "Plugin"
|
|
203
|
+
name = that._coverage_plugin_name
|
|
204
|
+
else:
|
|
205
|
+
thing = "Class"
|
|
206
|
+
klass = that.__class__
|
|
207
|
+
name = f"{klass.__module__}.{klass.__name__}"
|
|
208
|
+
|
|
209
|
+
raise NotImplementedError(
|
|
210
|
+
f"{thing} {name!r} needs to implement {func_name}()",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class DefaultValue:
|
|
215
|
+
"""A sentinel object to use for unusual default-value needs.
|
|
216
|
+
|
|
217
|
+
Construct with a string that will be used as the repr, for display in help
|
|
218
|
+
and Sphinx output.
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self, display_as: str) -> None:
|
|
223
|
+
self.display_as = display_as
|
|
224
|
+
|
|
225
|
+
def __repr__(self) -> str:
|
|
226
|
+
return self.display_as
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def substitute_variables(text: str, variables: Mapping[str, str]) -> str:
|
|
230
|
+
"""Substitute ``${VAR}`` variables in `text` with their values.
|
|
231
|
+
|
|
232
|
+
Variables in the text can take a number of shell-inspired forms::
|
|
233
|
+
|
|
234
|
+
$VAR
|
|
235
|
+
${VAR}
|
|
236
|
+
${VAR?} strict: an error if VAR isn't defined.
|
|
237
|
+
${VAR-missing} defaulted: "missing" if VAR isn't defined.
|
|
238
|
+
$$ just a dollar sign.
|
|
239
|
+
|
|
240
|
+
`variables` is a dictionary of variable values.
|
|
241
|
+
|
|
242
|
+
Returns the resulting text with values substituted.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
dollar_pattern = r"""(?x) # Use extended regex syntax
|
|
246
|
+
\$ # A dollar sign,
|
|
247
|
+
(?: # then
|
|
248
|
+
(?P<dollar> \$ ) | # a dollar sign, or
|
|
249
|
+
(?P<word1> \w+ ) | # a plain word, or
|
|
250
|
+
\{ # a {-wrapped
|
|
251
|
+
(?P<word2> \w+ ) # word,
|
|
252
|
+
(?: # either
|
|
253
|
+
(?P<strict> \? ) | # with a strict marker
|
|
254
|
+
-(?P<defval> [^}]* ) # or a default value
|
|
255
|
+
)? # maybe.
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
dollar_groups = ("dollar", "word1", "word2")
|
|
261
|
+
|
|
262
|
+
def dollar_replace(match: re.Match[str]) -> str:
|
|
263
|
+
"""Called for each $replacement."""
|
|
264
|
+
# Only one of the dollar_groups will have matched, just get its text.
|
|
265
|
+
word = next(g for g in match.group(*dollar_groups) if g) # pragma: always breaks
|
|
266
|
+
if word == "$":
|
|
267
|
+
return "$"
|
|
268
|
+
elif word in variables:
|
|
269
|
+
return variables[word]
|
|
270
|
+
elif match["strict"]:
|
|
271
|
+
msg = f"Variable {word} is undefined: {text!r}"
|
|
272
|
+
raise CoverageException(msg)
|
|
273
|
+
else:
|
|
274
|
+
return match["defval"]
|
|
275
|
+
|
|
276
|
+
text = re.sub(dollar_pattern, dollar_replace, text)
|
|
277
|
+
return text
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def format_local_datetime(dt: datetime.datetime) -> str:
|
|
281
|
+
"""Return a string with local timezone representing the date."""
|
|
282
|
+
return dt.astimezone().strftime("%Y-%m-%d %H:%M %z")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def import_local_file(modname: str, modfile: str | None = None) -> ModuleType:
|
|
286
|
+
"""Import a local file as a module.
|
|
287
|
+
|
|
288
|
+
Opens a file in the current directory named `modname`.py, imports it
|
|
289
|
+
as `modname`, and returns the module object. `modfile` is the file to
|
|
290
|
+
import if it isn't in the current directory.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
if modfile is None:
|
|
294
|
+
modfile = modname + ".py"
|
|
295
|
+
spec = importlib.util.spec_from_file_location(modname, modfile)
|
|
296
|
+
assert spec is not None
|
|
297
|
+
mod = importlib.util.module_from_spec(spec)
|
|
298
|
+
sys.modules[modname] = mod
|
|
299
|
+
assert spec.loader is not None
|
|
300
|
+
spec.loader.exec_module(mod)
|
|
301
|
+
|
|
302
|
+
return mod
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@functools.cache
|
|
306
|
+
def _human_key(s: str) -> tuple[list[str | int], str]:
|
|
307
|
+
"""Turn a string into a list of string and number chunks.
|
|
308
|
+
|
|
309
|
+
"z23a" -> (["z", 23, "a"], "z23a")
|
|
310
|
+
|
|
311
|
+
The original string is appended as a last value to ensure the
|
|
312
|
+
key is unique enough so that "x1y" and "x001y" can be distinguished.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def tryint(s: str) -> str | int:
|
|
316
|
+
"""If `s` is a number, return an int, else `s` unchanged."""
|
|
317
|
+
try:
|
|
318
|
+
return int(s)
|
|
319
|
+
except ValueError:
|
|
320
|
+
return s
|
|
321
|
+
|
|
322
|
+
return ([tryint(c) for c in re.split(r"(\d+)", s)], s)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def human_sorted(strings: Iterable[str]) -> list[str]:
|
|
326
|
+
"""Sort the given iterable of strings the way that humans expect.
|
|
327
|
+
|
|
328
|
+
Numeric components in the strings are sorted as numbers.
|
|
329
|
+
|
|
330
|
+
Returns the sorted list.
|
|
331
|
+
|
|
332
|
+
"""
|
|
333
|
+
return sorted(strings, key=_human_key)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
SortableItem = TypeVar("SortableItem", bound=Sequence[Any])
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def human_sorted_items(
|
|
340
|
+
items: Iterable[SortableItem],
|
|
341
|
+
reverse: bool = False,
|
|
342
|
+
) -> list[SortableItem]:
|
|
343
|
+
"""Sort (string, ...) items the way humans expect.
|
|
344
|
+
|
|
345
|
+
The elements of `items` can be any tuple/list. They'll be sorted by the
|
|
346
|
+
first element (a string), with ties broken by the remaining elements.
|
|
347
|
+
|
|
348
|
+
Returns the sorted list of items.
|
|
349
|
+
"""
|
|
350
|
+
return sorted(items, key=lambda item: (_human_key(item[0]), *item[1:]), reverse=reverse)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def plural(n: int, thing: str = "", things: str = "") -> str:
|
|
354
|
+
"""Pluralize a word.
|
|
355
|
+
|
|
356
|
+
If n is 1, return thing. Otherwise return things, or thing+s.
|
|
357
|
+
"""
|
|
358
|
+
if n == 1:
|
|
359
|
+
return thing
|
|
360
|
+
else:
|
|
361
|
+
return things or (thing + "s")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def stdout_link(text: str, url: str) -> str:
|
|
365
|
+
"""Format text+url as a clickable link for stdout.
|
|
366
|
+
|
|
367
|
+
If attached to a terminal, use escape sequences. Otherwise, just return
|
|
368
|
+
the text.
|
|
369
|
+
"""
|
|
370
|
+
if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
|
|
371
|
+
return f"\033]8;;{url}\a{text}\033]8;;\a"
|
|
372
|
+
else:
|
|
373
|
+
return text
|
coverage/multiproc.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
+
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
|
3
|
+
|
|
4
|
+
"""Monkey-patching to add multiprocessing support for coverage.py"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import multiprocessing
|
|
9
|
+
import multiprocessing.process
|
|
10
|
+
import os
|
|
11
|
+
import os.path
|
|
12
|
+
import sys
|
|
13
|
+
import traceback
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from coverage.debug import DebugControl
|
|
17
|
+
|
|
18
|
+
# An attribute that will be set on the module to indicate that it has been
|
|
19
|
+
# monkey-patched.
|
|
20
|
+
PATCHED_MARKER = "_coverage$patched"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
OriginalProcess = multiprocessing.process.BaseProcess
|
|
24
|
+
original_bootstrap = OriginalProcess._bootstrap # type: ignore[attr-defined]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProcessWithCoverage(OriginalProcess): # pylint: disable=abstract-method
|
|
28
|
+
"""A replacement for multiprocess.Process that starts coverage."""
|
|
29
|
+
|
|
30
|
+
def _bootstrap(self, *args, **kwargs): # type: ignore[no-untyped-def]
|
|
31
|
+
"""Wrapper around _bootstrap to start coverage."""
|
|
32
|
+
debug: DebugControl | None = None
|
|
33
|
+
try:
|
|
34
|
+
from coverage import Coverage # avoid circular import
|
|
35
|
+
|
|
36
|
+
cov = Coverage(data_suffix=True, auto_data=True)
|
|
37
|
+
cov._warn_preimported_source = False
|
|
38
|
+
cov.start()
|
|
39
|
+
_debug = cov._debug
|
|
40
|
+
assert _debug is not None
|
|
41
|
+
if _debug.should("multiproc"):
|
|
42
|
+
debug = _debug
|
|
43
|
+
if debug:
|
|
44
|
+
debug.write("Calling multiprocessing bootstrap")
|
|
45
|
+
except Exception:
|
|
46
|
+
print("Exception during multiprocessing bootstrap init:", file=sys.stderr)
|
|
47
|
+
traceback.print_exc(file=sys.stderr)
|
|
48
|
+
sys.stderr.flush()
|
|
49
|
+
raise
|
|
50
|
+
try:
|
|
51
|
+
return original_bootstrap(self, *args, **kwargs)
|
|
52
|
+
finally:
|
|
53
|
+
if debug:
|
|
54
|
+
debug.write("Finished multiprocessing bootstrap")
|
|
55
|
+
try:
|
|
56
|
+
cov.stop()
|
|
57
|
+
cov.save()
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
if debug:
|
|
60
|
+
debug.write("Exception during multiprocessing bootstrap cleanup", exc=exc)
|
|
61
|
+
raise
|
|
62
|
+
if debug:
|
|
63
|
+
debug.write("Saved multiprocessing data")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Stowaway:
|
|
67
|
+
"""An object to pickle, so when it is unpickled, it can apply the monkey-patch."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, rcfile: str) -> None:
|
|
70
|
+
self.rcfile = rcfile
|
|
71
|
+
|
|
72
|
+
def __getstate__(self) -> dict[str, str]:
|
|
73
|
+
return {"rcfile": self.rcfile}
|
|
74
|
+
|
|
75
|
+
def __setstate__(self, state: dict[str, str]) -> None:
|
|
76
|
+
patch_multiprocessing(state["rcfile"])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def patch_multiprocessing(rcfile: str) -> None:
|
|
80
|
+
"""Monkey-patch the multiprocessing module.
|
|
81
|
+
|
|
82
|
+
This enables coverage measurement of processes started by multiprocessing.
|
|
83
|
+
This involves aggressive monkey-patching.
|
|
84
|
+
|
|
85
|
+
`rcfile` is the path to the rcfile being used.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
if hasattr(multiprocessing, PATCHED_MARKER):
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap # type: ignore[attr-defined]
|
|
93
|
+
|
|
94
|
+
# Set the value in ProcessWithCoverage that will be pickled into the child
|
|
95
|
+
# process.
|
|
96
|
+
os.environ["COVERAGE_RCFILE"] = os.path.abspath(rcfile)
|
|
97
|
+
|
|
98
|
+
# When spawning processes rather than forking them, we have no state in the
|
|
99
|
+
# new process. We sneak in there with a Stowaway: we stuff one of our own
|
|
100
|
+
# objects into the data that gets pickled and sent to the subprocess. When
|
|
101
|
+
# the Stowaway is unpickled, its __setstate__ method is called, which
|
|
102
|
+
# re-applies the monkey-patch.
|
|
103
|
+
# Windows only spawns, so this is needed to keep Windows working.
|
|
104
|
+
try:
|
|
105
|
+
from multiprocessing import spawn
|
|
106
|
+
|
|
107
|
+
original_get_preparation_data = spawn.get_preparation_data
|
|
108
|
+
except (ImportError, AttributeError):
|
|
109
|
+
pass
|
|
110
|
+
else:
|
|
111
|
+
|
|
112
|
+
def get_preparation_data_with_stowaway(name: str) -> dict[str, Any]:
|
|
113
|
+
"""Get the original preparation data, and also insert our stowaway."""
|
|
114
|
+
d = original_get_preparation_data(name)
|
|
115
|
+
d["stowaway"] = Stowaway(rcfile)
|
|
116
|
+
return d
|
|
117
|
+
|
|
118
|
+
spawn.get_preparation_data = get_preparation_data_with_stowaway
|
|
119
|
+
|
|
120
|
+
setattr(multiprocessing, PATCHED_MARKER, True)
|
coverage/numbits.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
+
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Functions to manipulate packed binary representations of number sets.
|
|
6
|
+
|
|
7
|
+
To save space, coverage stores sets of line numbers in SQLite using a packed
|
|
8
|
+
binary representation called a numbits. A numbits is a set of positive
|
|
9
|
+
integers.
|
|
10
|
+
|
|
11
|
+
A numbits is stored as a blob in the database. The exact meaning of the bytes
|
|
12
|
+
in the blobs should be considered an implementation detail that might change in
|
|
13
|
+
the future. Use these functions to work with those binary blobs of data.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sqlite3
|
|
21
|
+
from collections.abc import Iterable
|
|
22
|
+
from itertools import zip_longest
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def nums_to_numbits(nums: Iterable[int]) -> bytes:
|
|
26
|
+
"""Convert `nums` into a numbits.
|
|
27
|
+
|
|
28
|
+
Arguments:
|
|
29
|
+
nums: a reusable iterable of integers, the line numbers to store.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A binary blob.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
nbytes = max(nums) // 8 + 1
|
|
36
|
+
except ValueError:
|
|
37
|
+
# nums was empty.
|
|
38
|
+
return b""
|
|
39
|
+
b = bytearray(nbytes)
|
|
40
|
+
for num in nums:
|
|
41
|
+
b[num // 8] |= 1 << num % 8
|
|
42
|
+
return bytes(b)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def numbits_to_nums(numbits: bytes) -> list[int]:
|
|
46
|
+
"""Convert a numbits into a list of numbers.
|
|
47
|
+
|
|
48
|
+
Arguments:
|
|
49
|
+
numbits: a binary blob, the packed number set.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
A list of ints.
|
|
53
|
+
|
|
54
|
+
When registered as a SQLite function by :func:`register_sqlite_functions`,
|
|
55
|
+
this returns a string, a JSON-encoded list of ints.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
nums = []
|
|
59
|
+
for byte_i, byte in enumerate(numbits):
|
|
60
|
+
for bit_i in range(8):
|
|
61
|
+
if byte & (1 << bit_i):
|
|
62
|
+
nums.append(byte_i * 8 + bit_i)
|
|
63
|
+
return nums
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def numbits_union(numbits1: bytes, numbits2: bytes) -> bytes:
|
|
67
|
+
"""Compute the union of two numbits.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A new numbits, the union of `numbits1` and `numbits2`.
|
|
71
|
+
"""
|
|
72
|
+
byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0)
|
|
73
|
+
return bytes(b1 | b2 for b1, b2 in byte_pairs)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def numbits_intersection(numbits1: bytes, numbits2: bytes) -> bytes:
|
|
77
|
+
"""Compute the intersection of two numbits.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A new numbits, the intersection `numbits1` and `numbits2`.
|
|
81
|
+
"""
|
|
82
|
+
byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0)
|
|
83
|
+
intersection_bytes = bytes(b1 & b2 for b1, b2 in byte_pairs)
|
|
84
|
+
return intersection_bytes.rstrip(b"\0")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def numbits_any_intersection(numbits1: bytes, numbits2: bytes) -> bool:
|
|
88
|
+
"""Is there any number that appears in both numbits?
|
|
89
|
+
|
|
90
|
+
Determine whether two number sets have a non-empty intersection. This is
|
|
91
|
+
faster than computing the intersection.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A bool, True if there is any number in both `numbits1` and `numbits2`.
|
|
95
|
+
"""
|
|
96
|
+
byte_pairs = zip_longest(numbits1, numbits2, fillvalue=0)
|
|
97
|
+
return any(b1 & b2 for b1, b2 in byte_pairs)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def num_in_numbits(num: int, numbits: bytes) -> bool:
|
|
101
|
+
"""Does the integer `num` appear in `numbits`?
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A bool, True if `num` is a member of `numbits`.
|
|
105
|
+
"""
|
|
106
|
+
nbyte, nbit = divmod(num, 8)
|
|
107
|
+
if nbyte >= len(numbits):
|
|
108
|
+
return False
|
|
109
|
+
return bool(numbits[nbyte] & (1 << nbit))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def register_sqlite_functions(connection: sqlite3.Connection) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Define numbits functions in a SQLite connection.
|
|
115
|
+
|
|
116
|
+
This defines these functions for use in SQLite statements:
|
|
117
|
+
|
|
118
|
+
* :func:`numbits_union`
|
|
119
|
+
* :func:`numbits_intersection`
|
|
120
|
+
* :func:`numbits_any_intersection`
|
|
121
|
+
* :func:`num_in_numbits`
|
|
122
|
+
* :func:`numbits_to_nums`
|
|
123
|
+
|
|
124
|
+
`connection` is a :class:`sqlite3.Connection <python:sqlite3.Connection>`
|
|
125
|
+
object. After creating the connection, pass it to this function to
|
|
126
|
+
register the numbits functions. Then you can use numbits functions in your
|
|
127
|
+
queries::
|
|
128
|
+
|
|
129
|
+
import sqlite3
|
|
130
|
+
from coverage.numbits import register_sqlite_functions
|
|
131
|
+
|
|
132
|
+
conn = sqlite3.connect("example.db")
|
|
133
|
+
register_sqlite_functions(conn)
|
|
134
|
+
c = conn.cursor()
|
|
135
|
+
# Kind of a nonsense query:
|
|
136
|
+
# Find all the files and contexts that executed line 47 in any file:
|
|
137
|
+
c.execute(
|
|
138
|
+
"select file_id, context_id from line_bits where num_in_numbits(?, numbits)",
|
|
139
|
+
(47,)
|
|
140
|
+
)
|
|
141
|
+
"""
|
|
142
|
+
connection.create_function("numbits_union", 2, numbits_union)
|
|
143
|
+
connection.create_function("numbits_intersection", 2, numbits_intersection)
|
|
144
|
+
connection.create_function("numbits_any_intersection", 2, numbits_any_intersection)
|
|
145
|
+
connection.create_function("num_in_numbits", 2, num_in_numbits)
|
|
146
|
+
connection.create_function("numbits_to_nums", 1, lambda b: json.dumps(numbits_to_nums(b)))
|