coverage 7.11.3__cp314-cp314-musllinux_1_2_x86_64.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.
Potentially problematic release.
This version of coverage might be problematic. Click here for more details.
- 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 +139 -0
- coverage/data.py +227 -0
- coverage/debug.py +669 -0
- coverage/disposition.py +59 -0
- coverage/env.py +135 -0
- coverage/exceptions.py +85 -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-314-x86_64-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.3.dist-info/METADATA +221 -0
- coverage-7.11.3.dist-info/RECORD +59 -0
- coverage-7.11.3.dist-info/WHEEL +5 -0
- coverage-7.11.3.dist-info/entry_points.txt +4 -0
- coverage-7.11.3.dist-info/licenses/LICENSE.txt +177 -0
- coverage-7.11.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
"""Support for plugins."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import os.path
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Iterable, Iterator
|
|
12
|
+
from types import FrameType
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
from coverage.exceptions import PluginError
|
|
16
|
+
from coverage.misc import isolate_module
|
|
17
|
+
from coverage.plugin import CoveragePlugin, FileReporter, FileTracer
|
|
18
|
+
from coverage.types import TArc, TConfigurable, TDebugCtl, TLineNo, TPluginConfig, TSourceTokenLines
|
|
19
|
+
|
|
20
|
+
os = isolate_module(os)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Plugins:
|
|
24
|
+
"""The currently loaded collection of coverage.py plugins."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, debug: TDebugCtl | None = None) -> None:
|
|
27
|
+
self.order: list[CoveragePlugin] = []
|
|
28
|
+
self.names: dict[str, CoveragePlugin] = {}
|
|
29
|
+
self.file_tracers: list[CoveragePlugin] = []
|
|
30
|
+
self.configurers: list[CoveragePlugin] = []
|
|
31
|
+
self.context_switchers: list[CoveragePlugin] = []
|
|
32
|
+
|
|
33
|
+
self.current_module: str | None = None
|
|
34
|
+
self.debug = debug
|
|
35
|
+
|
|
36
|
+
def load_from_config(
|
|
37
|
+
self,
|
|
38
|
+
modules: Iterable[str],
|
|
39
|
+
config: TPluginConfig,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Load plugin modules, and read their settings from configuration."""
|
|
42
|
+
|
|
43
|
+
for module in modules:
|
|
44
|
+
self.current_module = module
|
|
45
|
+
__import__(module)
|
|
46
|
+
mod = sys.modules[module]
|
|
47
|
+
|
|
48
|
+
coverage_init = getattr(mod, "coverage_init", None)
|
|
49
|
+
if not coverage_init:
|
|
50
|
+
raise PluginError(
|
|
51
|
+
f"Plugin module {module!r} didn't define a coverage_init function",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
options = config.get_plugin_options(module)
|
|
55
|
+
coverage_init(self, options)
|
|
56
|
+
|
|
57
|
+
self.current_module = None
|
|
58
|
+
|
|
59
|
+
def load_from_callables(
|
|
60
|
+
self,
|
|
61
|
+
plugin_inits: Iterable[TCoverageInit],
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Load plugins from callables provided."""
|
|
64
|
+
for fn in plugin_inits:
|
|
65
|
+
fn(self)
|
|
66
|
+
|
|
67
|
+
def add_file_tracer(self, plugin: CoveragePlugin) -> None:
|
|
68
|
+
"""Add a file tracer plugin.
|
|
69
|
+
|
|
70
|
+
`plugin` is an instance of a third-party plugin class. It must
|
|
71
|
+
implement the :meth:`CoveragePlugin.file_tracer` method.
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
self._add_plugin(plugin, self.file_tracers)
|
|
75
|
+
|
|
76
|
+
def add_configurer(self, plugin: CoveragePlugin) -> None:
|
|
77
|
+
"""Add a configuring plugin.
|
|
78
|
+
|
|
79
|
+
`plugin` is an instance of a third-party plugin class. It must
|
|
80
|
+
implement the :meth:`CoveragePlugin.configure` method.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
self._add_plugin(plugin, self.configurers)
|
|
84
|
+
|
|
85
|
+
def add_dynamic_context(self, plugin: CoveragePlugin) -> None:
|
|
86
|
+
"""Add a dynamic context plugin.
|
|
87
|
+
|
|
88
|
+
`plugin` is an instance of a third-party plugin class. It must
|
|
89
|
+
implement the :meth:`CoveragePlugin.dynamic_context` method.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
self._add_plugin(plugin, self.context_switchers)
|
|
93
|
+
|
|
94
|
+
def add_noop(self, plugin: CoveragePlugin) -> None:
|
|
95
|
+
"""Add a plugin that does nothing.
|
|
96
|
+
|
|
97
|
+
This is only useful for testing the plugin support.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
self._add_plugin(plugin, None)
|
|
101
|
+
|
|
102
|
+
def _add_plugin(
|
|
103
|
+
self,
|
|
104
|
+
plugin: CoveragePlugin,
|
|
105
|
+
specialized: list[CoveragePlugin] | None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Add a plugin object.
|
|
108
|
+
|
|
109
|
+
`plugin` is a :class:`CoveragePlugin` instance to add. `specialized`
|
|
110
|
+
is a list to append the plugin to.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
plugin_name = f"{self.current_module}.{plugin.__class__.__name__}"
|
|
114
|
+
if self.debug and self.debug.should("plugin"):
|
|
115
|
+
self.debug.write(f"Loaded plugin {self.current_module!r}: {plugin!r}")
|
|
116
|
+
labelled = LabelledDebug(f"plugin {self.current_module!r}", self.debug)
|
|
117
|
+
plugin = DebugPluginWrapper(plugin, labelled)
|
|
118
|
+
|
|
119
|
+
plugin._coverage_plugin_name = plugin_name
|
|
120
|
+
plugin._coverage_enabled = True
|
|
121
|
+
self.order.append(plugin)
|
|
122
|
+
self.names[plugin_name] = plugin
|
|
123
|
+
if specialized is not None:
|
|
124
|
+
specialized.append(plugin)
|
|
125
|
+
|
|
126
|
+
def __bool__(self) -> bool:
|
|
127
|
+
return bool(self.order)
|
|
128
|
+
|
|
129
|
+
def __iter__(self) -> Iterator[CoveragePlugin]:
|
|
130
|
+
return iter(self.order)
|
|
131
|
+
|
|
132
|
+
def get(self, plugin_name: str) -> CoveragePlugin:
|
|
133
|
+
"""Return a plugin by name."""
|
|
134
|
+
return self.names[plugin_name]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
TCoverageInit = Callable[[Plugins], None]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class LabelledDebug:
|
|
141
|
+
"""A Debug writer, but with labels for prepending to the messages."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, label: str, debug: TDebugCtl, prev_labels: Iterable[str] = ()):
|
|
144
|
+
self.labels = list(prev_labels) + [label]
|
|
145
|
+
self.debug = debug
|
|
146
|
+
|
|
147
|
+
def add_label(self, label: str) -> LabelledDebug:
|
|
148
|
+
"""Add a label to the writer, and return a new `LabelledDebug`."""
|
|
149
|
+
return LabelledDebug(label, self.debug, self.labels)
|
|
150
|
+
|
|
151
|
+
def message_prefix(self) -> str:
|
|
152
|
+
"""The prefix to use on messages, combining the labels."""
|
|
153
|
+
prefixes = self.labels + [""]
|
|
154
|
+
return ":\n".join(" " * i + label for i, label in enumerate(prefixes))
|
|
155
|
+
|
|
156
|
+
def write(self, message: str) -> None:
|
|
157
|
+
"""Write `message`, but with the labels prepended."""
|
|
158
|
+
self.debug.write(f"{self.message_prefix()}{message}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class DebugPluginWrapper(CoveragePlugin):
|
|
162
|
+
"""Wrap a plugin, and use debug to report on what it's doing."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, plugin: CoveragePlugin, debug: LabelledDebug) -> None:
|
|
165
|
+
super().__init__()
|
|
166
|
+
self.plugin = plugin
|
|
167
|
+
self.debug = debug
|
|
168
|
+
|
|
169
|
+
def file_tracer(self, filename: str) -> FileTracer | None:
|
|
170
|
+
tracer = self.plugin.file_tracer(filename)
|
|
171
|
+
self.debug.write(f"file_tracer({filename!r}) --> {tracer!r}")
|
|
172
|
+
if tracer:
|
|
173
|
+
debug = self.debug.add_label(f"file {filename!r}")
|
|
174
|
+
tracer = DebugFileTracerWrapper(tracer, debug)
|
|
175
|
+
return tracer
|
|
176
|
+
|
|
177
|
+
def file_reporter(self, filename: str) -> FileReporter | str:
|
|
178
|
+
reporter = self.plugin.file_reporter(filename)
|
|
179
|
+
assert isinstance(reporter, FileReporter)
|
|
180
|
+
self.debug.write(f"file_reporter({filename!r}) --> {reporter!r}")
|
|
181
|
+
if reporter:
|
|
182
|
+
debug = self.debug.add_label(f"file {filename!r}")
|
|
183
|
+
reporter = DebugFileReporterWrapper(filename, reporter, debug)
|
|
184
|
+
return reporter
|
|
185
|
+
|
|
186
|
+
def dynamic_context(self, frame: FrameType) -> str | None:
|
|
187
|
+
context = self.plugin.dynamic_context(frame)
|
|
188
|
+
self.debug.write(f"dynamic_context({frame!r}) --> {context!r}")
|
|
189
|
+
return context
|
|
190
|
+
|
|
191
|
+
def find_executable_files(self, src_dir: str) -> Iterable[str]:
|
|
192
|
+
executable_files = self.plugin.find_executable_files(src_dir)
|
|
193
|
+
self.debug.write(f"find_executable_files({src_dir!r}) --> {executable_files!r}")
|
|
194
|
+
return executable_files
|
|
195
|
+
|
|
196
|
+
def configure(self, config: TConfigurable) -> None:
|
|
197
|
+
self.debug.write(f"configure({config!r})")
|
|
198
|
+
self.plugin.configure(config)
|
|
199
|
+
|
|
200
|
+
def sys_info(self) -> Iterable[tuple[str, Any]]:
|
|
201
|
+
return self.plugin.sys_info()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class DebugFileTracerWrapper(FileTracer):
|
|
205
|
+
"""A debugging `FileTracer`."""
|
|
206
|
+
|
|
207
|
+
def __init__(self, tracer: FileTracer, debug: LabelledDebug) -> None:
|
|
208
|
+
self.tracer = tracer
|
|
209
|
+
self.debug = debug
|
|
210
|
+
|
|
211
|
+
def _show_frame(self, frame: FrameType) -> str:
|
|
212
|
+
"""A short string identifying a frame, for debug messages."""
|
|
213
|
+
filename = os.path.basename(frame.f_code.co_filename)
|
|
214
|
+
return f"{filename}@{frame.f_lineno}"
|
|
215
|
+
|
|
216
|
+
def source_filename(self) -> str:
|
|
217
|
+
sfilename = self.tracer.source_filename()
|
|
218
|
+
self.debug.write(f"source_filename() --> {sfilename!r}")
|
|
219
|
+
return sfilename
|
|
220
|
+
|
|
221
|
+
def has_dynamic_source_filename(self) -> bool:
|
|
222
|
+
has = self.tracer.has_dynamic_source_filename()
|
|
223
|
+
self.debug.write(f"has_dynamic_source_filename() --> {has!r}")
|
|
224
|
+
return has
|
|
225
|
+
|
|
226
|
+
def dynamic_source_filename(self, filename: str, frame: FrameType) -> str | None:
|
|
227
|
+
dyn = self.tracer.dynamic_source_filename(filename, frame)
|
|
228
|
+
self.debug.write(
|
|
229
|
+
"dynamic_source_filename({!r}, {}) --> {!r}".format(
|
|
230
|
+
filename,
|
|
231
|
+
self._show_frame(frame),
|
|
232
|
+
dyn,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
return dyn
|
|
236
|
+
|
|
237
|
+
def line_number_range(self, frame: FrameType) -> tuple[TLineNo, TLineNo]:
|
|
238
|
+
pair = self.tracer.line_number_range(frame)
|
|
239
|
+
self.debug.write(f"line_number_range({self._show_frame(frame)}) --> {pair!r}")
|
|
240
|
+
return pair
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class DebugFileReporterWrapper(FileReporter):
|
|
244
|
+
"""A debugging `FileReporter`."""
|
|
245
|
+
|
|
246
|
+
def __init__(self, filename: str, reporter: FileReporter, debug: LabelledDebug) -> None:
|
|
247
|
+
super().__init__(filename)
|
|
248
|
+
self.reporter = reporter
|
|
249
|
+
self.debug = debug
|
|
250
|
+
|
|
251
|
+
def relative_filename(self) -> str:
|
|
252
|
+
ret = self.reporter.relative_filename()
|
|
253
|
+
self.debug.write(f"relative_filename() --> {ret!r}")
|
|
254
|
+
return ret
|
|
255
|
+
|
|
256
|
+
def lines(self) -> set[TLineNo]:
|
|
257
|
+
ret = self.reporter.lines()
|
|
258
|
+
self.debug.write(f"lines() --> {ret!r}")
|
|
259
|
+
return ret
|
|
260
|
+
|
|
261
|
+
def excluded_lines(self) -> set[TLineNo]:
|
|
262
|
+
ret = self.reporter.excluded_lines()
|
|
263
|
+
self.debug.write(f"excluded_lines() --> {ret!r}")
|
|
264
|
+
return ret
|
|
265
|
+
|
|
266
|
+
def translate_lines(self, lines: Iterable[TLineNo]) -> set[TLineNo]:
|
|
267
|
+
ret = self.reporter.translate_lines(lines)
|
|
268
|
+
self.debug.write(f"translate_lines({lines!r}) --> {ret!r}")
|
|
269
|
+
return ret
|
|
270
|
+
|
|
271
|
+
def translate_arcs(self, arcs: Iterable[TArc]) -> set[TArc]:
|
|
272
|
+
ret = self.reporter.translate_arcs(arcs)
|
|
273
|
+
self.debug.write(f"translate_arcs({arcs!r}) --> {ret!r}")
|
|
274
|
+
return ret
|
|
275
|
+
|
|
276
|
+
def no_branch_lines(self) -> set[TLineNo]:
|
|
277
|
+
ret = self.reporter.no_branch_lines()
|
|
278
|
+
self.debug.write(f"no_branch_lines() --> {ret!r}")
|
|
279
|
+
return ret
|
|
280
|
+
|
|
281
|
+
def exit_counts(self) -> dict[TLineNo, int]:
|
|
282
|
+
ret = self.reporter.exit_counts()
|
|
283
|
+
self.debug.write(f"exit_counts() --> {ret!r}")
|
|
284
|
+
return ret
|
|
285
|
+
|
|
286
|
+
def arcs(self) -> set[TArc]:
|
|
287
|
+
ret = self.reporter.arcs()
|
|
288
|
+
self.debug.write(f"arcs() --> {ret!r}")
|
|
289
|
+
return ret
|
|
290
|
+
|
|
291
|
+
def source(self) -> str:
|
|
292
|
+
ret = self.reporter.source()
|
|
293
|
+
self.debug.write(f"source() --> {len(ret)} chars")
|
|
294
|
+
return ret
|
|
295
|
+
|
|
296
|
+
def source_token_lines(self) -> TSourceTokenLines:
|
|
297
|
+
ret = list(self.reporter.source_token_lines())
|
|
298
|
+
self.debug.write(f"source_token_lines() --> {len(ret)} tokens")
|
|
299
|
+
return ret
|
coverage/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561 to indicate that this package has type hints.
|
coverage/python.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
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
|
+
"""Python source expertise for coverage.py"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os.path
|
|
9
|
+
import types
|
|
10
|
+
import zipimport
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from coverage import env
|
|
15
|
+
from coverage.exceptions import CoverageException, NoSource
|
|
16
|
+
from coverage.files import canonical_filename, relative_filename, zip_location
|
|
17
|
+
from coverage.misc import isolate_module, join_regex
|
|
18
|
+
from coverage.parser import PythonParser
|
|
19
|
+
from coverage.phystokens import source_encoding, source_token_lines
|
|
20
|
+
from coverage.plugin import CodeRegion, FileReporter
|
|
21
|
+
from coverage.regions import code_regions
|
|
22
|
+
from coverage.types import TArc, TLineNo, TMorf, TSourceTokenLines
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from coverage import Coverage
|
|
26
|
+
|
|
27
|
+
os = isolate_module(os)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_python_source(filename: str) -> bytes:
|
|
31
|
+
"""Read the Python source text from `filename`.
|
|
32
|
+
|
|
33
|
+
Returns bytes.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
with open(filename, "rb") as f:
|
|
37
|
+
source = f.read()
|
|
38
|
+
|
|
39
|
+
return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_python_source(filename: str) -> str:
|
|
43
|
+
"""Return the source code, as unicode."""
|
|
44
|
+
base, ext = os.path.splitext(filename)
|
|
45
|
+
if ext == ".py" and env.WINDOWS:
|
|
46
|
+
exts = [".py", ".pyw"]
|
|
47
|
+
else:
|
|
48
|
+
exts = [ext]
|
|
49
|
+
|
|
50
|
+
source_bytes: bytes | None
|
|
51
|
+
for ext in exts:
|
|
52
|
+
try_filename = base + ext
|
|
53
|
+
if os.path.exists(try_filename):
|
|
54
|
+
# A regular text file: open it.
|
|
55
|
+
source_bytes = read_python_source(try_filename)
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
# Maybe it's in a zip file?
|
|
59
|
+
source_bytes = get_zip_bytes(try_filename)
|
|
60
|
+
if source_bytes is not None:
|
|
61
|
+
break
|
|
62
|
+
else:
|
|
63
|
+
# Couldn't find source.
|
|
64
|
+
raise NoSource(f"No source for code: '{filename}'.", slug="no-source")
|
|
65
|
+
|
|
66
|
+
# Replace \f because of http://bugs.python.org/issue19035
|
|
67
|
+
source_bytes = source_bytes.replace(b"\f", b" ")
|
|
68
|
+
source = source_bytes.decode(source_encoding(source_bytes), "replace")
|
|
69
|
+
|
|
70
|
+
# Python code should always end with a line with a newline.
|
|
71
|
+
if source and source[-1] != "\n":
|
|
72
|
+
source += "\n"
|
|
73
|
+
|
|
74
|
+
return source
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_zip_bytes(filename: str) -> bytes | None:
|
|
78
|
+
"""Get data from `filename` if it is a zip file path.
|
|
79
|
+
|
|
80
|
+
Returns the bytestring data read from the zip file, or None if no zip file
|
|
81
|
+
could be found or `filename` isn't in it. The data returned will be
|
|
82
|
+
an empty string if the file is empty.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
zipfile_inner = zip_location(filename)
|
|
86
|
+
if zipfile_inner is not None:
|
|
87
|
+
zipfile, inner = zipfile_inner
|
|
88
|
+
try:
|
|
89
|
+
zi = zipimport.zipimporter(zipfile)
|
|
90
|
+
except zipimport.ZipImportError:
|
|
91
|
+
return None
|
|
92
|
+
try:
|
|
93
|
+
data = zi.get_data(inner)
|
|
94
|
+
except OSError:
|
|
95
|
+
return None
|
|
96
|
+
return data
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def source_for_file(filename: str) -> str:
|
|
101
|
+
"""Return the source filename for `filename`.
|
|
102
|
+
|
|
103
|
+
Given a file name being traced, return the best guess as to the source
|
|
104
|
+
file to attribute it to.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
if filename.endswith(".py"):
|
|
108
|
+
# .py files are themselves source files.
|
|
109
|
+
return filename
|
|
110
|
+
|
|
111
|
+
elif filename.endswith((".pyc", ".pyo")):
|
|
112
|
+
# Bytecode files probably have source files near them.
|
|
113
|
+
py_filename = filename[:-1]
|
|
114
|
+
if os.path.exists(py_filename):
|
|
115
|
+
# Found a .py file, use that.
|
|
116
|
+
return py_filename
|
|
117
|
+
if env.WINDOWS:
|
|
118
|
+
# On Windows, it could be a .pyw file.
|
|
119
|
+
pyw_filename = py_filename + "w"
|
|
120
|
+
if os.path.exists(pyw_filename):
|
|
121
|
+
return pyw_filename
|
|
122
|
+
# Didn't find source, but it's probably the .py file we want.
|
|
123
|
+
return py_filename
|
|
124
|
+
|
|
125
|
+
# No idea, just use the file name as-is.
|
|
126
|
+
return filename
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def source_for_morf(morf: TMorf) -> str:
|
|
130
|
+
"""Get the source filename for the module-or-file `morf`."""
|
|
131
|
+
if hasattr(morf, "__file__") and morf.__file__:
|
|
132
|
+
filename = morf.__file__
|
|
133
|
+
elif isinstance(morf, types.ModuleType):
|
|
134
|
+
# A module should have had .__file__, otherwise we can't use it.
|
|
135
|
+
# This could be a PEP-420 namespace package.
|
|
136
|
+
raise CoverageException(f"Module {morf} has no file")
|
|
137
|
+
else:
|
|
138
|
+
filename = morf
|
|
139
|
+
|
|
140
|
+
filename = source_for_file(filename)
|
|
141
|
+
return filename
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class PythonFileReporter(FileReporter):
|
|
145
|
+
"""Report support for a Python file."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, morf: TMorf, coverage: Coverage | None = None) -> None:
|
|
148
|
+
self.coverage = coverage
|
|
149
|
+
|
|
150
|
+
filename = source_for_morf(morf)
|
|
151
|
+
|
|
152
|
+
fname = filename
|
|
153
|
+
canonicalize = True
|
|
154
|
+
if self.coverage is not None:
|
|
155
|
+
if self.coverage.config.relative_files:
|
|
156
|
+
canonicalize = False
|
|
157
|
+
if canonicalize:
|
|
158
|
+
fname = canonical_filename(filename)
|
|
159
|
+
super().__init__(fname)
|
|
160
|
+
|
|
161
|
+
if hasattr(morf, "__name__"):
|
|
162
|
+
name = morf.__name__.replace(".", os.sep)
|
|
163
|
+
if os.path.basename(filename).startswith("__init__."):
|
|
164
|
+
name += os.sep + "__init__"
|
|
165
|
+
name += ".py"
|
|
166
|
+
else:
|
|
167
|
+
name = relative_filename(filename)
|
|
168
|
+
self.relname = name
|
|
169
|
+
|
|
170
|
+
self._source: str | None = None
|
|
171
|
+
self._parser: PythonParser | None = None
|
|
172
|
+
self._excluded = None
|
|
173
|
+
|
|
174
|
+
def __repr__(self) -> str:
|
|
175
|
+
return f"<PythonFileReporter {self.filename!r}>"
|
|
176
|
+
|
|
177
|
+
def relative_filename(self) -> str:
|
|
178
|
+
return self.relname
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def parser(self) -> PythonParser:
|
|
182
|
+
"""Lazily create a :class:`PythonParser`."""
|
|
183
|
+
assert self.coverage is not None
|
|
184
|
+
if self._parser is None:
|
|
185
|
+
self._parser = PythonParser(
|
|
186
|
+
filename=self.filename,
|
|
187
|
+
exclude=self.coverage._exclude_regex("exclude"),
|
|
188
|
+
)
|
|
189
|
+
self._parser.parse_source()
|
|
190
|
+
return self._parser
|
|
191
|
+
|
|
192
|
+
def lines(self) -> set[TLineNo]:
|
|
193
|
+
"""Return the line numbers of statements in the file."""
|
|
194
|
+
return self.parser.statements
|
|
195
|
+
|
|
196
|
+
def multiline_map(self) -> dict[TLineNo, TLineNo]:
|
|
197
|
+
"""A map of line numbers to first-line in a multi-line statement."""
|
|
198
|
+
return self.parser.multiline_map
|
|
199
|
+
|
|
200
|
+
def excluded_lines(self) -> set[TLineNo]:
|
|
201
|
+
"""Return the line numbers of statements in the file."""
|
|
202
|
+
return self.parser.excluded
|
|
203
|
+
|
|
204
|
+
def translate_lines(self, lines: Iterable[TLineNo]) -> set[TLineNo]:
|
|
205
|
+
return self.parser.translate_lines(lines)
|
|
206
|
+
|
|
207
|
+
def translate_arcs(self, arcs: Iterable[TArc]) -> set[TArc]:
|
|
208
|
+
return self.parser.translate_arcs(arcs)
|
|
209
|
+
|
|
210
|
+
def no_branch_lines(self) -> set[TLineNo]:
|
|
211
|
+
assert self.coverage is not None
|
|
212
|
+
no_branch = self.parser.lines_matching(
|
|
213
|
+
join_regex(self.coverage.config.partial_list + self.coverage.config.partial_always_list)
|
|
214
|
+
)
|
|
215
|
+
return no_branch
|
|
216
|
+
|
|
217
|
+
def arcs(self) -> set[TArc]:
|
|
218
|
+
return self.parser.arcs()
|
|
219
|
+
|
|
220
|
+
def exit_counts(self) -> dict[TLineNo, int]:
|
|
221
|
+
return self.parser.exit_counts()
|
|
222
|
+
|
|
223
|
+
def missing_arc_description(
|
|
224
|
+
self,
|
|
225
|
+
start: TLineNo,
|
|
226
|
+
end: TLineNo,
|
|
227
|
+
executed_arcs: Iterable[TArc] | None = None,
|
|
228
|
+
) -> str:
|
|
229
|
+
return self.parser.missing_arc_description(start, end)
|
|
230
|
+
|
|
231
|
+
def arc_description(self, start: TLineNo, end: TLineNo) -> str:
|
|
232
|
+
return self.parser.arc_description(start, end)
|
|
233
|
+
|
|
234
|
+
def source(self) -> str:
|
|
235
|
+
if self._source is None:
|
|
236
|
+
self._source = get_python_source(self.filename)
|
|
237
|
+
return self._source
|
|
238
|
+
|
|
239
|
+
def should_be_python(self) -> bool:
|
|
240
|
+
"""Does it seem like this file should contain Python?
|
|
241
|
+
|
|
242
|
+
This is used to decide if a file reported as part of the execution of
|
|
243
|
+
a program was really likely to have contained Python in the first
|
|
244
|
+
place.
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
# Get the file extension.
|
|
248
|
+
_, ext = os.path.splitext(self.filename)
|
|
249
|
+
|
|
250
|
+
# Anything named *.py* should be Python.
|
|
251
|
+
if ext.startswith(".py"):
|
|
252
|
+
return True
|
|
253
|
+
# A file with no extension should be Python.
|
|
254
|
+
if not ext:
|
|
255
|
+
return True
|
|
256
|
+
# Everything else is probably not Python.
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
def source_token_lines(self) -> TSourceTokenLines:
|
|
260
|
+
return source_token_lines(self.source())
|
|
261
|
+
|
|
262
|
+
def code_regions(self) -> Iterable[CodeRegion]:
|
|
263
|
+
return code_regions(self.source())
|
|
264
|
+
|
|
265
|
+
def code_region_kinds(self) -> Iterable[tuple[str, str]]:
|
|
266
|
+
return [
|
|
267
|
+
("function", "functions"),
|
|
268
|
+
("class", "classes"),
|
|
269
|
+
]
|