coverage 7.6.7__cp311-cp311-win_amd64.whl → 7.11.1__cp311-cp311-win_amd64.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.
Files changed (54) hide show
  1. coverage/__init__.py +2 -0
  2. coverage/__main__.py +2 -0
  3. coverage/annotate.py +1 -2
  4. coverage/bytecode.py +177 -3
  5. coverage/cmdline.py +329 -154
  6. coverage/collector.py +31 -42
  7. coverage/config.py +166 -62
  8. coverage/context.py +4 -5
  9. coverage/control.py +164 -85
  10. coverage/core.py +70 -33
  11. coverage/data.py +3 -4
  12. coverage/debug.py +112 -56
  13. coverage/disposition.py +1 -0
  14. coverage/env.py +65 -55
  15. coverage/exceptions.py +35 -7
  16. coverage/execfile.py +18 -13
  17. coverage/files.py +23 -18
  18. coverage/html.py +134 -88
  19. coverage/htmlfiles/style.css +42 -2
  20. coverage/htmlfiles/style.scss +65 -1
  21. coverage/inorout.py +61 -44
  22. coverage/jsonreport.py +17 -8
  23. coverage/lcovreport.py +16 -20
  24. coverage/misc.py +50 -46
  25. coverage/multiproc.py +12 -7
  26. coverage/numbits.py +3 -4
  27. coverage/parser.py +193 -269
  28. coverage/patch.py +166 -0
  29. coverage/phystokens.py +24 -25
  30. coverage/plugin.py +13 -13
  31. coverage/plugin_support.py +36 -35
  32. coverage/python.py +9 -13
  33. coverage/pytracer.py +40 -33
  34. coverage/regions.py +2 -1
  35. coverage/report.py +59 -43
  36. coverage/report_core.py +6 -9
  37. coverage/results.py +118 -66
  38. coverage/sqldata.py +260 -210
  39. coverage/sqlitedb.py +33 -25
  40. coverage/sysmon.py +195 -157
  41. coverage/templite.py +6 -6
  42. coverage/tomlconfig.py +12 -12
  43. coverage/tracer.cp311-win_amd64.pyd +0 -0
  44. coverage/tracer.pyi +2 -0
  45. coverage/types.py +25 -22
  46. coverage/version.py +3 -18
  47. coverage/xmlreport.py +16 -13
  48. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/METADATA +40 -18
  49. coverage-7.11.1.dist-info/RECORD +59 -0
  50. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/WHEEL +1 -1
  51. coverage-7.6.7.dist-info/RECORD +0 -58
  52. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/entry_points.txt +0 -0
  53. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info/licenses}/LICENSE.txt +0 -0
  54. {coverage-7.6.7.dist-info → coverage-7.11.1.dist-info}/top_level.txt +0 -0
coverage/patch.py ADDED
@@ -0,0 +1,166 @@
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
+ """Invasive patches for coverage.py."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import atexit
9
+ import contextlib
10
+ import os
11
+ import site
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any, NoReturn
14
+
15
+ from coverage import env
16
+ from coverage.debug import NoDebugging, DevNullDebug
17
+ from coverage.exceptions import ConfigError, CoverageException
18
+
19
+ if TYPE_CHECKING:
20
+ from coverage import Coverage
21
+ from coverage.config import CoverageConfig
22
+ from coverage.types import TDebugCtl
23
+
24
+
25
+ def apply_patches(
26
+ cov: Coverage,
27
+ config: CoverageConfig,
28
+ debug: TDebugCtl,
29
+ *,
30
+ make_pth_file: bool = True,
31
+ ) -> None:
32
+ """Apply invasive patches requested by `[run] patch=`."""
33
+ debug = debug if debug.should("patch") else DevNullDebug()
34
+ for patch in sorted(set(config.patch)):
35
+ match patch:
36
+ case "_exit":
37
+ _patch__exit(cov, debug)
38
+
39
+ case "execv":
40
+ _patch_execv(cov, config, debug)
41
+
42
+ case "fork":
43
+ _patch_fork(debug)
44
+
45
+ case "subprocess":
46
+ _patch_subprocess(config, debug, make_pth_file)
47
+
48
+ case _:
49
+ raise ConfigError(f"Unknown patch {patch!r}")
50
+
51
+
52
+ def _patch__exit(cov: Coverage, debug: TDebugCtl) -> None:
53
+ """Patch os._exit."""
54
+ debug.write("Patching _exit")
55
+
56
+ old_exit = os._exit
57
+
58
+ def coverage_os_exit_patch(status: int) -> NoReturn:
59
+ with contextlib.suppress(Exception):
60
+ debug.write(f"Using _exit patch with {cov = }")
61
+ with contextlib.suppress(Exception):
62
+ cov.save()
63
+ old_exit(status)
64
+
65
+ os._exit = coverage_os_exit_patch
66
+
67
+
68
+ def _patch_execv(cov: Coverage, config: CoverageConfig, debug: TDebugCtl) -> None:
69
+ """Patch the execv family of functions."""
70
+ if env.WINDOWS:
71
+ raise CoverageException("patch=execv isn't supported yet on Windows.")
72
+
73
+ debug.write("Patching execv")
74
+
75
+ def make_execv_patch(fname: str, old_execv: Any) -> Any:
76
+ def coverage_execv_patch(*args: Any, **kwargs: Any) -> Any:
77
+ with contextlib.suppress(Exception):
78
+ debug.write(f"Using execv patch for {fname} with {cov = }")
79
+ with contextlib.suppress(Exception):
80
+ cov.save()
81
+
82
+ if fname.endswith("e"):
83
+ # Assume the `env` argument is passed positionally.
84
+ new_env = args[-1]
85
+ # Pass our configuration in the new environment.
86
+ new_env["COVERAGE_PROCESS_CONFIG"] = config.serialize()
87
+ if env.TESTING:
88
+ # The subprocesses need to use the same core as the main process.
89
+ new_env["COVERAGE_CORE"] = os.getenv("COVERAGE_CORE")
90
+
91
+ # When testing locally, we need to honor the pyc file location
92
+ # or they get written to the .tox directories and pollute the
93
+ # next run with a different core.
94
+ if (cache_prefix := os.getenv("PYTHONPYCACHEPREFIX")) is not None:
95
+ new_env["PYTHONPYCACHEPREFIX"] = cache_prefix
96
+
97
+ # Without this, it fails on PyPy and Ubuntu.
98
+ new_env["PATH"] = os.getenv("PATH")
99
+ old_execv(*args, **kwargs)
100
+
101
+ return coverage_execv_patch
102
+
103
+ # All the exec* and spawn* functions eventually call execv or execve.
104
+ os.execv = make_execv_patch("execv", os.execv)
105
+ os.execve = make_execv_patch("execve", os.execve)
106
+
107
+
108
+ def _patch_fork(debug: TDebugCtl) -> None:
109
+ """Ensure Coverage is properly reset after a fork."""
110
+ from coverage.control import _after_fork_in_child
111
+
112
+ if env.WINDOWS:
113
+ raise CoverageException("patch=fork isn't supported yet on Windows.")
114
+
115
+ debug.write("Patching fork")
116
+ os.register_at_fork(after_in_child=_after_fork_in_child)
117
+
118
+
119
+ def _patch_subprocess(config: CoverageConfig, debug: TDebugCtl, make_pth_file: bool) -> None:
120
+ """Write .pth files and set environment vars to measure subprocesses."""
121
+ debug.write("Patching subprocess")
122
+
123
+ if make_pth_file:
124
+ pth_files = create_pth_files(debug)
125
+
126
+ def delete_pth_files() -> None:
127
+ for p in pth_files:
128
+ debug.write(f"Deleting subprocess .pth file: {str(p)!r}")
129
+ p.unlink(missing_ok=True)
130
+
131
+ atexit.register(delete_pth_files)
132
+ assert config.config_file is not None
133
+ os.environ["COVERAGE_PROCESS_CONFIG"] = config.serialize()
134
+
135
+
136
+ # Writing .pth files is not obvious. On Windows, getsitepackages() returns two
137
+ # directories. A .pth file in the first will be run, but coverage isn't
138
+ # importable yet. We write into all the places we can, but with defensive
139
+ # import code.
140
+
141
+ PTH_CODE = """\
142
+ try:
143
+ import coverage
144
+ except:
145
+ pass
146
+ else:
147
+ coverage.process_startup()
148
+ """
149
+
150
+ PTH_TEXT = f"import sys; exec({PTH_CODE!r})\n"
151
+
152
+
153
+ def create_pth_files(debug: TDebugCtl = NoDebugging()) -> list[Path]:
154
+ """Create .pth files for measuring subprocesses."""
155
+ pth_files = []
156
+ for pth_dir in site.getsitepackages():
157
+ pth_file = Path(pth_dir) / f"subcover_{os.getpid()}.pth"
158
+ try:
159
+ if debug.should("patch"):
160
+ debug.write(f"Writing subprocess .pth file: {str(pth_file)!r}")
161
+ pth_file.write_text(PTH_TEXT, encoding="utf-8")
162
+ except OSError: # pragma: cant happen
163
+ continue
164
+ else:
165
+ pth_files.append(pth_file)
166
+ return pth_files
coverage/phystokens.py CHANGED
@@ -12,13 +12,11 @@ import re
12
12
  import sys
13
13
  import token
14
14
  import tokenize
15
-
16
15
  from collections.abc import Iterable
17
16
 
18
17
  from coverage import env
19
18
  from coverage.types import TLineNo, TSourceTokenLines
20
19
 
21
-
22
20
  TokenInfos = Iterable[tokenize.TokenInfo]
23
21
 
24
22
 
@@ -57,8 +55,10 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos:
57
55
  if last_ttext.endswith("\\"):
58
56
  inject_backslash = False
59
57
  elif ttype == token.STRING:
60
- if (last_line.endswith("\\\n") and # pylint: disable=simplifiable-if-statement
61
- last_line.rstrip(" \\\n").endswith(last_ttext)):
58
+ if ( # pylint: disable=simplifiable-if-statement
59
+ last_line.endswith("\\\n")
60
+ and last_line.rstrip(" \\\n").endswith(last_ttext)
61
+ ):
62
62
  # Deal with special cases like such code::
63
63
  #
64
64
  # a = ["aaa",\ # there may be zero or more blanks between "," and "\".
@@ -70,15 +70,17 @@ def _phys_tokens(toks: TokenInfos) -> TokenInfos:
70
70
  # It's a multi-line string and the first line ends with
71
71
  # a backslash, so we don't need to inject another.
72
72
  inject_backslash = False
73
- elif sys.version_info >= (3, 12) and ttype == token.FSTRING_MIDDLE:
73
+ elif env.PYBEHAVIOR.fstring_syntax and ttype == token.FSTRING_MIDDLE:
74
74
  inject_backslash = False
75
75
  if inject_backslash:
76
76
  # Figure out what column the backslash is in.
77
77
  ccol = len(last_line.split("\n")[-2]) - 1
78
78
  # Yield the token, with a fake token type.
79
79
  yield tokenize.TokenInfo(
80
- 99999, "\\\n",
81
- (slineno, ccol), (slineno, ccol+2),
80
+ 99999,
81
+ "\\\n",
82
+ (slineno, ccol),
83
+ (slineno, ccol + 2),
82
84
  last_line,
83
85
  )
84
86
  last_line = ltext
@@ -93,7 +95,7 @@ def find_soft_key_lines(source: str) -> set[TLineNo]:
93
95
  soft_key_lines: set[TLineNo] = set()
94
96
 
95
97
  for node in ast.walk(ast.parse(source)):
96
- if sys.version_info >= (3, 10) and isinstance(node, ast.Match):
98
+ if isinstance(node, ast.Match):
97
99
  soft_key_lines.add(node.lineno)
98
100
  for case in node.cases:
99
101
  soft_key_lines.add(case.pattern.lineno)
@@ -126,10 +128,7 @@ def source_token_lines(source: str) -> TSourceTokenLines:
126
128
  source = source.expandtabs(8).replace("\r\n", "\n")
127
129
  tokgen = generate_tokens(source)
128
130
 
129
- if env.PYBEHAVIOR.soft_keywords:
130
- soft_key_lines = find_soft_key_lines(source)
131
- else:
132
- soft_key_lines = set()
131
+ soft_key_lines = find_soft_key_lines(source)
133
132
 
134
133
  for ttype, ttext, (sline, scol), (_, ecol), _ in _phys_tokens(tokgen):
135
134
  mark_start = True
@@ -144,6 +143,9 @@ def source_token_lines(source: str) -> TSourceTokenLines:
144
143
  elif ttype in ws_tokens:
145
144
  mark_end = False
146
145
  else:
146
+ if env.PYBEHAVIOR.fstring_syntax and ttype == token.FSTRING_MIDDLE:
147
+ part = part.replace("{", "{{").replace("}", "}}")
148
+ ecol = scol + len(part)
147
149
  if mark_start and scol > col:
148
150
  line.append(("ws", " " * (scol - col)))
149
151
  mark_start = False
@@ -152,19 +154,16 @@ def source_token_lines(source: str) -> TSourceTokenLines:
152
154
  if keyword.iskeyword(ttext):
153
155
  # Hard keywords are always keywords.
154
156
  tok_class = "key"
155
- elif sys.version_info >= (3, 10): # PYVERSIONS
156
- # Need the version_info check to keep mypy from borking
157
- # on issoftkeyword here.
158
- if env.PYBEHAVIOR.soft_keywords and keyword.issoftkeyword(ttext):
159
- # Soft keywords appear at the start of their line.
160
- if len(line) == 0:
161
- is_start_of_line = True
162
- elif (len(line) == 1) and line[0][0] == "ws":
163
- is_start_of_line = True
164
- else:
165
- is_start_of_line = False
166
- if is_start_of_line and sline in soft_key_lines:
167
- tok_class = "key"
157
+ elif keyword.issoftkeyword(ttext):
158
+ # Soft keywords appear at the start of their line.
159
+ if len(line) == 0:
160
+ is_start_of_line = True
161
+ elif (len(line) == 1) and line[0][0] == "ws":
162
+ is_start_of_line = True
163
+ else:
164
+ is_start_of_line = False
165
+ if is_start_of_line and sline in soft_key_lines:
166
+ tok_class = "key"
168
167
  line.append((tok_class, part))
169
168
  mark_end = True
170
169
  scol = 0
coverage/plugin.py CHANGED
@@ -116,10 +116,9 @@ from __future__ import annotations
116
116
 
117
117
  import dataclasses
118
118
  import functools
119
-
119
+ from collections.abc import Iterable
120
120
  from types import FrameType
121
121
  from typing import Any
122
- from collections.abc import Iterable
123
122
 
124
123
  from coverage import files
125
124
  from coverage.misc import _needs_to_implement
@@ -132,7 +131,7 @@ class CoveragePlugin:
132
131
  _coverage_plugin_name: str
133
132
  _coverage_enabled: bool
134
133
 
135
- def file_tracer(self, filename: str) -> FileTracer | None: # pylint: disable=unused-argument
134
+ def file_tracer(self, filename: str) -> FileTracer | None: # pylint: disable=unused-argument
136
135
  """Get a :class:`FileTracer` object for a file.
137
136
 
138
137
  Plug-in type: file tracer.
@@ -174,8 +173,8 @@ class CoveragePlugin:
174
173
 
175
174
  def file_reporter(
176
175
  self,
177
- filename: str, # pylint: disable=unused-argument
178
- ) -> FileReporter | str: # str should be Literal["python"]
176
+ filename: str, # pylint: disable=unused-argument
177
+ ) -> FileReporter | str: # str should be Literal["python"]
179
178
  """Get the :class:`FileReporter` class to use for a file.
180
179
 
181
180
  Plug-in type: file tracer.
@@ -191,7 +190,7 @@ class CoveragePlugin:
191
190
 
192
191
  def dynamic_context(
193
192
  self,
194
- frame: FrameType, # pylint: disable=unused-argument
193
+ frame: FrameType, # pylint: disable=unused-argument
195
194
  ) -> str | None:
196
195
  """Get the dynamically computed context label for `frame`.
197
196
 
@@ -210,7 +209,7 @@ class CoveragePlugin:
210
209
 
211
210
  def find_executable_files(
212
211
  self,
213
- src_dir: str, # pylint: disable=unused-argument
212
+ src_dir: str, # pylint: disable=unused-argument
214
213
  ) -> Iterable[str]:
215
214
  """Yield all of the executable files in `src_dir`, recursively.
216
215
 
@@ -256,6 +255,7 @@ class CoveragePlugin:
256
255
 
257
256
  class CoveragePluginBase:
258
257
  """Plugins produce specialized objects, which point back to the original plugin."""
258
+
259
259
  _coverage_plugin: CoveragePlugin
260
260
 
261
261
 
@@ -311,8 +311,8 @@ class FileTracer(CoveragePluginBase):
311
311
 
312
312
  def dynamic_source_filename(
313
313
  self,
314
- filename: str, # pylint: disable=unused-argument
315
- frame: FrameType, # pylint: disable=unused-argument
314
+ filename: str, # pylint: disable=unused-argument
315
+ frame: FrameType, # pylint: disable=unused-argument
316
316
  ) -> str | None:
317
317
  """Get a dynamically computed source file name.
318
318
 
@@ -527,7 +527,7 @@ class FileReporter(CoveragePluginBase):
527
527
  self,
528
528
  start: TLineNo,
529
529
  end: TLineNo,
530
- executed_arcs: Iterable[TArc] | None = None, # pylint: disable=unused-argument
530
+ executed_arcs: Iterable[TArc] | None = None, # pylint: disable=unused-argument
531
531
  ) -> str:
532
532
  """Provide an English sentence describing a missing arc.
533
533
 
@@ -545,8 +545,8 @@ class FileReporter(CoveragePluginBase):
545
545
 
546
546
  def arc_description(
547
547
  self,
548
- start: TLineNo, # pylint: disable=unused-argument
549
- end: TLineNo
548
+ start: TLineNo, # pylint: disable=unused-argument
549
+ end: TLineNo,
550
550
  ) -> str:
551
551
  """Provide an English description of an arc's effect."""
552
552
  return f"jump to line {end}"
@@ -614,4 +614,4 @@ class FileReporter(CoveragePluginBase):
614
614
  return isinstance(other, FileReporter) and self.filename < other.filename
615
615
 
616
616
  # This object doesn't need to be hashed.
617
- __hash__ = None # type: ignore[assignment]
617
+ __hash__ = None # type: ignore[assignment]
@@ -8,17 +8,14 @@ from __future__ import annotations
8
8
  import os
9
9
  import os.path
10
10
  import sys
11
-
12
- from types import FrameType
13
- from typing import Any
14
11
  from collections.abc import Iterable, Iterator
12
+ from types import FrameType
13
+ from typing import Any, Callable
15
14
 
16
15
  from coverage.exceptions import PluginError
17
16
  from coverage.misc import isolate_module
18
- from coverage.plugin import CoveragePlugin, FileTracer, FileReporter
19
- from coverage.types import (
20
- TArc, TConfigurable, TDebugCtl, TLineNo, TPluginConfig, TSourceTokenLines,
21
- )
17
+ from coverage.plugin import CoveragePlugin, FileReporter, FileTracer
18
+ from coverage.types import TArc, TConfigurable, TDebugCtl, TLineNo, TPluginConfig, TSourceTokenLines
22
19
 
23
20
  os = isolate_module(os)
24
21
 
@@ -26,7 +23,7 @@ os = isolate_module(os)
26
23
  class Plugins:
27
24
  """The currently loaded collection of coverage.py plugins."""
28
25
 
29
- def __init__(self) -> None:
26
+ def __init__(self, debug: TDebugCtl | None = None) -> None:
30
27
  self.order: list[CoveragePlugin] = []
31
28
  self.names: dict[str, CoveragePlugin] = {}
32
29
  self.file_tracers: list[CoveragePlugin] = []
@@ -34,25 +31,17 @@ class Plugins:
34
31
  self.context_switchers: list[CoveragePlugin] = []
35
32
 
36
33
  self.current_module: str | None = None
37
- self.debug: TDebugCtl | None
34
+ self.debug = debug
38
35
 
39
- @classmethod
40
- def load_plugins(
41
- cls,
36
+ def load_from_config(
37
+ self,
42
38
  modules: Iterable[str],
43
39
  config: TPluginConfig,
44
- debug: TDebugCtl | None = None,
45
- ) -> Plugins:
46
- """Load plugins from `modules`.
47
-
48
- Returns a Plugins object with the loaded and configured plugins.
49
-
50
- """
51
- plugins = cls()
52
- plugins.debug = debug
40
+ ) -> None:
41
+ """Load plugin modules, and read their settings from configuration."""
53
42
 
54
43
  for module in modules:
55
- plugins.current_module = module
44
+ self.current_module = module
56
45
  __import__(module)
57
46
  mod = sys.modules[module]
58
47
 
@@ -63,10 +52,17 @@ class Plugins:
63
52
  )
64
53
 
65
54
  options = config.get_plugin_options(module)
66
- coverage_init(plugins, options)
55
+ coverage_init(self, options)
56
+
57
+ self.current_module = None
67
58
 
68
- plugins.current_module = None
69
- return plugins
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)
70
66
 
71
67
  def add_file_tracer(self, plugin: CoveragePlugin) -> None:
72
68
  """Add a file tracer plugin.
@@ -138,6 +134,9 @@ class Plugins:
138
134
  return self.names[plugin_name]
139
135
 
140
136
 
137
+ TCoverageInit = Callable[[Plugins], None]
138
+
139
+
141
140
  class LabelledDebug:
142
141
  """A Debug writer, but with labels for prepending to the messages."""
143
142
 
@@ -152,7 +151,7 @@ class LabelledDebug:
152
151
  def message_prefix(self) -> str:
153
152
  """The prefix to use on messages, combining the labels."""
154
153
  prefixes = self.labels + [""]
155
- return ":\n".join(" "*i+label for i, label in enumerate(prefixes))
154
+ return ":\n".join(" " * i + label for i, label in enumerate(prefixes))
156
155
 
157
156
  def write(self, message: str) -> None:
158
157
  """Write `message`, but with the labels prepended."""
@@ -211,10 +210,8 @@ class DebugFileTracerWrapper(FileTracer):
211
210
 
212
211
  def _show_frame(self, frame: FrameType) -> str:
213
212
  """A short string identifying a frame, for debug messages."""
214
- return "%s@%d" % (
215
- os.path.basename(frame.f_code.co_filename),
216
- frame.f_lineno,
217
- )
213
+ filename = os.path.basename(frame.f_code.co_filename)
214
+ return f"{filename}@{frame.f_lineno}"
218
215
 
219
216
  def source_filename(self) -> str:
220
217
  sfilename = self.tracer.source_filename()
@@ -228,9 +225,13 @@ class DebugFileTracerWrapper(FileTracer):
228
225
 
229
226
  def dynamic_source_filename(self, filename: str, frame: FrameType) -> str | None:
230
227
  dyn = self.tracer.dynamic_source_filename(filename, frame)
231
- self.debug.write("dynamic_source_filename({!r}, {}) --> {!r}".format(
232
- filename, self._show_frame(frame), dyn,
233
- ))
228
+ self.debug.write(
229
+ "dynamic_source_filename({!r}, {}) --> {!r}".format(
230
+ filename,
231
+ self._show_frame(frame),
232
+ dyn,
233
+ )
234
+ )
234
235
  return dyn
235
236
 
236
237
  def line_number_range(self, frame: FrameType) -> tuple[TLineNo, TLineNo]:
@@ -289,10 +290,10 @@ class DebugFileReporterWrapper(FileReporter):
289
290
 
290
291
  def source(self) -> str:
291
292
  ret = self.reporter.source()
292
- self.debug.write("source() --> %d chars" % (len(ret),))
293
+ self.debug.write(f"source() --> {len(ret)} chars")
293
294
  return ret
294
295
 
295
296
  def source_token_lines(self) -> TSourceTokenLines:
296
297
  ret = list(self.reporter.source_token_lines())
297
- self.debug.write("source_token_lines() --> %d tokens" % (len(ret),))
298
+ self.debug.write(f"source_token_lines() --> {len(ret)} tokens")
298
299
  return ret
coverage/python.py CHANGED
@@ -8,16 +8,15 @@ from __future__ import annotations
8
8
  import os.path
9
9
  import types
10
10
  import zipimport
11
-
12
- from typing import TYPE_CHECKING
13
11
  from collections.abc import Iterable
12
+ from typing import TYPE_CHECKING
14
13
 
15
14
  from coverage import env
16
15
  from coverage.exceptions import CoverageException, NoSource
17
16
  from coverage.files import canonical_filename, relative_filename, zip_location
18
17
  from coverage.misc import isolate_module, join_regex
19
18
  from coverage.parser import PythonParser
20
- from coverage.phystokens import source_token_lines, source_encoding
19
+ from coverage.phystokens import source_encoding, source_token_lines
21
20
  from coverage.plugin import CodeRegion, FileReporter
22
21
  from coverage.regions import code_regions
23
22
  from coverage.types import TArc, TLineNo, TMorf, TSourceTokenLines
@@ -62,7 +61,7 @@ def get_python_source(filename: str) -> str:
62
61
  break
63
62
  else:
64
63
  # Couldn't find source.
65
- raise NoSource(f"No source for code: '{filename}'.")
64
+ raise NoSource(f"No source for code: '{filename}'.", slug="no-source")
66
65
 
67
66
  # Replace \f because of http://bugs.python.org/issue19035
68
67
  source_bytes = source_bytes.replace(b"\f", b" ")
@@ -194,6 +193,10 @@ class PythonFileReporter(FileReporter):
194
193
  """Return the line numbers of statements in the file."""
195
194
  return self.parser.statements
196
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
+
197
200
  def excluded_lines(self) -> set[TLineNo]:
198
201
  """Return the line numbers of statements in the file."""
199
202
  return self.parser.excluded
@@ -207,10 +210,7 @@ class PythonFileReporter(FileReporter):
207
210
  def no_branch_lines(self) -> set[TLineNo]:
208
211
  assert self.coverage is not None
209
212
  no_branch = self.parser.lines_matching(
210
- join_regex(
211
- self.coverage.config.partial_list
212
- + self.coverage.config.partial_always_list
213
- )
213
+ join_regex(self.coverage.config.partial_list + self.coverage.config.partial_always_list)
214
214
  )
215
215
  return no_branch
216
216
 
@@ -228,11 +228,7 @@ class PythonFileReporter(FileReporter):
228
228
  ) -> str:
229
229
  return self.parser.missing_arc_description(start, end)
230
230
 
231
- def arc_description(
232
- self,
233
- start: TLineNo,
234
- end: TLineNo
235
- ) -> str:
231
+ def arc_description(self, start: TLineNo, end: TLineNo) -> str:
236
232
  return self.parser.arc_description(start, end)
237
233
 
238
234
  def source(self) -> str: