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/sqlitedb.py CHANGED
@@ -8,9 +8,8 @@ from __future__ import annotations
8
8
  import contextlib
9
9
  import re
10
10
  import sqlite3
11
-
12
- from typing import cast, Any
13
11
  from collections.abc import Iterable, Iterator
12
+ from typing import Any, cast
14
13
 
15
14
  from coverage.debug import auto_repr, clipped_repr, exc_one_line
16
15
  from coverage.exceptions import DataError
@@ -29,9 +28,11 @@ class SqliteDb:
29
28
  etc(a, b)
30
29
 
31
30
  """
32
- def __init__(self, filename: str, debug: TDebugCtl) -> None:
31
+
32
+ def __init__(self, filename: str, debug: TDebugCtl, no_disk: bool = False) -> None:
33
33
  self.debug = debug
34
34
  self.filename = filename
35
+ self.no_disk = no_disk
35
36
  self.nest = 0
36
37
  self.con: sqlite3.Connection | None = None
37
38
 
@@ -50,7 +51,11 @@ class SqliteDb:
50
51
  if self.debug.should("sql"):
51
52
  self.debug.write(f"Connecting to {self.filename!r}")
52
53
  try:
53
- self.con = sqlite3.connect(self.filename, check_same_thread=False)
54
+ # Use uri=True when connecting to memory URIs
55
+ if self.filename.startswith("file:"):
56
+ self.con = sqlite3.connect(self.filename, check_same_thread=False, uri=True)
57
+ else:
58
+ self.con = sqlite3.connect(self.filename, check_same_thread=False)
54
59
  except sqlite3.Error as exc:
55
60
  raise DataError(f"Couldn't use data file {self.filename!r}: {exc}") from exc
56
61
 
@@ -64,8 +69,9 @@ class SqliteDb:
64
69
  # In Python 3.12+, we can change the config to allow journal_mode=off.
65
70
  if hasattr(sqlite3, "SQLITE_DBCONFIG_DEFENSIVE"):
66
71
  # Turn off defensive mode, so that journal_mode=off can succeed.
67
- self.con.setconfig( # type: ignore[attr-defined, unused-ignore]
68
- sqlite3.SQLITE_DBCONFIG_DEFENSIVE, False,
72
+ self.con.setconfig( # type: ignore[attr-defined, unused-ignore]
73
+ sqlite3.SQLITE_DBCONFIG_DEFENSIVE,
74
+ False,
69
75
  )
70
76
 
71
77
  # This pragma makes writing faster. It disables rollbacks, but we never need them.
@@ -76,13 +82,14 @@ class SqliteDb:
76
82
  # to keep things going.
77
83
  self.execute_void("pragma synchronous=off", fail_ok=True)
78
84
 
79
- def close(self) -> None:
85
+ def close(self, force: bool = False) -> None:
80
86
  """If needed, close the connection."""
81
- if self.con is not None and self.filename != ":memory:":
82
- if self.debug.should("sql"):
83
- self.debug.write(f"Closing {self.con!r} on {self.filename!r}")
84
- self.con.close()
85
- self.con = None
87
+ if self.con is not None:
88
+ if force or not self.no_disk:
89
+ if self.debug.should("sql"):
90
+ self.debug.write(f"Closing {self.con!r} on {self.filename!r}")
91
+ self.con.close()
92
+ self.con = None
86
93
 
87
94
  def __enter__(self) -> SqliteDb:
88
95
  if self.nest == 0:
@@ -92,7 +99,7 @@ class SqliteDb:
92
99
  self.nest += 1
93
100
  return self
94
101
 
95
- def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[no-untyped-def]
102
+ def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore[no-untyped-def]
96
103
  self.nest -= 1
97
104
  if self.nest == 0:
98
105
  try:
@@ -112,15 +119,15 @@ class SqliteDb:
112
119
  try:
113
120
  assert self.con is not None
114
121
  try:
115
- return self.con.execute(sql, parameters) # type: ignore[arg-type]
122
+ return self.con.execute(sql, parameters) # type: ignore[arg-type]
116
123
  except Exception:
117
124
  # In some cases, an error might happen that isn't really an
118
125
  # error. Try again immediately.
119
126
  # https://github.com/nedbat/coveragepy/issues/1010
120
- return self.con.execute(sql, parameters) # type: ignore[arg-type]
127
+ return self.con.execute(sql, parameters) # type: ignore[arg-type]
121
128
  except sqlite3.Error as exc:
122
129
  msg = str(exc)
123
- if self.filename != ":memory:":
130
+ if not self.no_disk:
124
131
  try:
125
132
  # `execute` is the first thing we do with the database, so try
126
133
  # hard to provide useful hints if something goes wrong now.
@@ -128,8 +135,8 @@ class SqliteDb:
128
135
  cov4_sig = b"!coverage.py: This is a private format"
129
136
  if bad_file.read(len(cov4_sig)) == cov4_sig:
130
137
  msg = (
131
- "Looks like a coverage 4.x data file. " +
132
- "Are you mixing versions of coverage?"
138
+ "Looks like a coverage 4.x data file. "
139
+ + "Are you mixing versions of coverage?"
133
140
  )
134
141
  except Exception:
135
142
  pass
@@ -210,18 +217,19 @@ class SqliteDb:
210
217
  # https://github.com/nedbat/coveragepy/issues/1010
211
218
  return self.con.executemany(sql, data)
212
219
 
213
- def executemany_void(self, sql: str, data: Iterable[Any]) -> None:
220
+ def executemany_void(self, sql: str, data: list[Any]) -> None:
214
221
  """Same as :meth:`python:sqlite3.Connection.executemany` when you don't need the cursor."""
215
- data = list(data)
216
- if data:
217
- self._executemany(sql, data).close()
222
+ self._executemany(sql, data).close()
218
223
 
219
224
  def executescript(self, script: str) -> None:
220
225
  """Same as :meth:`python:sqlite3.Connection.executescript`."""
221
226
  if self.debug.should("sql"):
222
- self.debug.write("Executing script with {} chars: {}".format(
223
- len(script), clipped_repr(script, 100),
224
- ))
227
+ self.debug.write(
228
+ "Executing script with {} chars: {}".format(
229
+ len(script),
230
+ clipped_repr(script, 100),
231
+ )
232
+ )
225
233
  assert self.con is not None
226
234
  self.con.executescript(script).close()
227
235
 
coverage/sysmon.py CHANGED
@@ -12,46 +12,48 @@ import os.path
12
12
  import sys
13
13
  import threading
14
14
  import traceback
15
-
16
15
  from dataclasses import dataclass
17
- from types import CodeType, FrameType
18
- from typing import (
19
- Any,
20
- Callable,
21
- TYPE_CHECKING,
22
- cast,
23
- )
16
+ from types import CodeType
17
+ from typing import Any, Callable, NewType, Optional, cast
24
18
 
19
+ from coverage import env
20
+ from coverage.bytecode import TBranchTrails, always_jumps, branch_trails
25
21
  from coverage.debug import short_filename, short_stack
26
22
  from coverage.misc import isolate_module
23
+ from coverage.parser import PythonParser
27
24
  from coverage.types import (
28
25
  AnyCallable,
29
- TArc,
30
26
  TFileDisposition,
31
27
  TLineNo,
28
+ TOffset,
29
+ Tracer,
32
30
  TShouldStartContextFn,
33
31
  TShouldTraceFn,
34
32
  TTraceData,
35
33
  TTraceFileData,
36
- Tracer,
37
34
  TWarnFn,
38
35
  )
39
36
 
37
+ # Only needed for some of the commented-out logging:
38
+ # from coverage.debug import ppformat
39
+
40
40
  os = isolate_module(os)
41
41
 
42
42
  # pylint: disable=unused-argument
43
43
 
44
- LOG = False
44
+ # $set_env.py: COVERAGE_SYSMON_LOG - Log sys.monitoring activity
45
+ LOG = bool(int(os.getenv("COVERAGE_SYSMON_LOG", 0)))
46
+
47
+ # $set_env.py: COVERAGE_SYSMON_STATS - Collect sys.monitoring stats
48
+ COLLECT_STATS = bool(int(os.getenv("COVERAGE_SYSMON_STATS", 0)))
45
49
 
46
50
  # This module will be imported in all versions of Python, but only used in 3.12+
47
51
  # It will be type-checked for 3.12, but not for earlier versions.
48
52
  sys_monitoring = getattr(sys, "monitoring", None)
49
53
 
50
- if TYPE_CHECKING:
51
- assert sys_monitoring is not None
52
- # I want to say this but it's not allowed:
53
- # MonitorReturn = Literal[sys.monitoring.DISABLE] | None
54
- MonitorReturn = Any
54
+ DISABLE_TYPE = NewType("DISABLE_TYPE", object)
55
+ MonitorReturn = Optional[DISABLE_TYPE]
56
+ DISABLE = cast(MonitorReturn, getattr(sys_monitoring, "DISABLE", None))
55
57
 
56
58
 
57
59
  if LOG: # pragma: debugging
@@ -74,7 +76,10 @@ if LOG: # pragma: debugging
74
76
  assert sys_monitoring is not None
75
77
 
76
78
  short_stack = functools.partial(
77
- short_stack, full=True, short_filenames=True, frame_ids=True,
79
+ short_stack,
80
+ full=True,
81
+ short_filenames=True,
82
+ frame_ids=True,
78
83
  )
79
84
  seen_threads: set[int] = set()
80
85
 
@@ -96,8 +101,11 @@ if LOG: # pragma: debugging
96
101
  # f"{root}-{pid}.out",
97
102
  # f"{root}-{pid}-{tslug}.out",
98
103
  ]:
99
- with open(filename, "a") as f:
100
- print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
104
+ with open(filename, "a", encoding="utf-8") as f:
105
+ try:
106
+ print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
107
+ except UnicodeError:
108
+ print(f"{pid}:{tslug}: {ascii(msg)}", file=f, flush=True)
101
109
 
102
110
  def arg_repr(arg: Any) -> str:
103
111
  """Make a customized repr for logged values."""
@@ -128,7 +136,8 @@ if LOG: # pragma: debugging
128
136
  return ret
129
137
  except Exception as exc:
130
138
  log(f"!!{exc.__class__.__name__}: {exc}")
131
- log("".join(traceback.format_exception(exc))) # pylint: disable=[no-value-for-parameter]
139
+ if 1:
140
+ log("".join(traceback.format_exception(exc)))
132
141
  try:
133
142
  assert sys_monitoring is not None
134
143
  sys_monitoring.set_events(sys.monitoring.COVERAGE_ID, 0)
@@ -161,11 +170,22 @@ class CodeInfo:
161
170
 
162
171
  tracing: bool
163
172
  file_data: TTraceFileData | None
164
- # TODO: what is byte_to_line for?
165
- byte_to_line: dict[int, int] | None
173
+ byte_to_line: dict[TOffset, TLineNo] | None
174
+
175
+ # Keys are start instruction offsets for branches.
176
+ # Values are dicts:
177
+ # {
178
+ # (from_line, to_line): {offset, offset, ...},
179
+ # (from_line, to_line): {offset, offset, ...},
180
+ # }
181
+ branch_trails: TBranchTrails
166
182
 
183
+ # Always-jumps are bytecode offsets that do no work but move
184
+ # to another offset.
185
+ always_jumps: dict[TOffset, TOffset]
167
186
 
168
- def bytes_to_lines(code: CodeType) -> dict[int, int]:
187
+
188
+ def bytes_to_lines(code: CodeType) -> dict[TOffset, TLineNo]:
169
189
  """Make a dict mapping byte code offsets to line numbers."""
170
190
  b2l = {}
171
191
  for bstart, bend, lineno in code.co_lines():
@@ -202,17 +222,16 @@ class SysMonitor(Tracer):
202
222
  # A list of code_objects, just to keep them alive so that id's are
203
223
  # useful as identity.
204
224
  self.code_objects: list[CodeType] = []
205
- self.last_lines: dict[FrameType, int] = {}
206
- # Map id(code_object) -> code_object
207
- self.local_event_codes: dict[int, CodeType] = {}
208
225
  self.sysmon_on = False
209
226
  self.lock = threading.Lock()
210
227
 
211
- self.stats = {
212
- "starts": 0,
213
- }
228
+ self.stats: dict[str, int] | None = None
229
+ if COLLECT_STATS:
230
+ self.stats = dict.fromkeys(
231
+ "starts start_tracing returns line_lines line_arcs branches branch_trails".split(),
232
+ 0,
233
+ )
214
234
 
215
- self.stopped = False
216
235
  self._activity = False
217
236
 
218
237
  def __repr__(self) -> str:
@@ -223,44 +242,43 @@ class SysMonitor(Tracer):
223
242
  @panopticon()
224
243
  def start(self) -> None:
225
244
  """Start this Tracer."""
226
- self.stopped = False
227
-
228
- assert sys_monitoring is not None
229
- sys_monitoring.use_tool_id(self.myid, "coverage.py")
230
- register = functools.partial(sys_monitoring.register_callback, self.myid)
231
- events = sys_monitoring.events
232
- if self.trace_arcs:
233
- sys_monitoring.set_events(
234
- self.myid,
235
- events.PY_START | events.PY_UNWIND,
236
- )
237
- register(events.PY_START, self.sysmon_py_start)
238
- register(events.PY_RESUME, self.sysmon_py_resume_arcs)
239
- register(events.PY_RETURN, self.sysmon_py_return_arcs)
240
- register(events.PY_UNWIND, self.sysmon_py_unwind_arcs)
241
- register(events.LINE, self.sysmon_line_arcs)
242
- else:
245
+ with self.lock:
246
+ assert sys_monitoring is not None
247
+ sys_monitoring.use_tool_id(self.myid, "coverage.py")
248
+ register = functools.partial(sys_monitoring.register_callback, self.myid)
249
+ events = sys.monitoring.events
250
+
243
251
  sys_monitoring.set_events(self.myid, events.PY_START)
244
252
  register(events.PY_START, self.sysmon_py_start)
245
- register(events.LINE, self.sysmon_line_lines)
246
- sys_monitoring.restart_events()
247
- self.sysmon_on = True
253
+ if self.trace_arcs:
254
+ register(events.PY_RETURN, self.sysmon_py_return)
255
+ register(events.LINE, self.sysmon_line_arcs)
256
+ if env.PYBEHAVIOR.branch_right_left:
257
+ register(
258
+ events.BRANCH_RIGHT, # type:ignore[attr-defined]
259
+ self.sysmon_branch_either,
260
+ )
261
+ register(
262
+ events.BRANCH_LEFT, # type:ignore[attr-defined]
263
+ self.sysmon_branch_either,
264
+ )
265
+ else:
266
+ register(events.LINE, self.sysmon_line_lines)
267
+ sys_monitoring.restart_events()
268
+ self.sysmon_on = True
248
269
 
249
270
  @panopticon()
250
271
  def stop(self) -> None:
251
272
  """Stop this Tracer."""
252
- if not self.sysmon_on:
253
- # In forking situations, we might try to stop when we are not
254
- # started. Do nothing in that case.
255
- return
256
- assert sys_monitoring is not None
257
- sys_monitoring.set_events(self.myid, 0)
258
273
  with self.lock:
274
+ if not self.sysmon_on:
275
+ # In forking situations, we might try to stop when we are not
276
+ # started. Do nothing in that case.
277
+ return
278
+ assert sys_monitoring is not None
279
+ sys_monitoring.set_events(self.myid, 0)
259
280
  self.sysmon_on = False
260
- for code in self.local_event_codes.values():
261
- sys_monitoring.set_local_events(self.myid, code, 0)
262
- self.local_event_codes = {}
263
- sys_monitoring.free_tool_id(self.myid)
281
+ sys_monitoring.free_tool_id(self.myid)
264
282
 
265
283
  @panopticon()
266
284
  def post_fork(self) -> None:
@@ -277,29 +295,15 @@ class SysMonitor(Tracer):
277
295
 
278
296
  def get_stats(self) -> dict[str, int] | None:
279
297
  """Return a dictionary of statistics, or None."""
280
- return None
281
-
282
- # The number of frames in callers_frame takes @panopticon into account.
283
- if LOG:
284
-
285
- def callers_frame(self) -> FrameType:
286
- """Get the frame of the Python code we're monitoring."""
287
- return (
288
- inspect.currentframe().f_back.f_back.f_back # type: ignore[union-attr,return-value]
289
- )
290
-
291
- else:
292
-
293
- def callers_frame(self) -> FrameType:
294
- """Get the frame of the Python code we're monitoring."""
295
- return inspect.currentframe().f_back.f_back # type: ignore[union-attr,return-value]
298
+ return self.stats
296
299
 
297
300
  @panopticon("code", "@")
298
- def sysmon_py_start(self, code: CodeType, instruction_offset: int) -> MonitorReturn:
301
+ def sysmon_py_start(self, code: CodeType, instruction_offset: TOffset) -> MonitorReturn:
299
302
  """Handle sys.monitoring.events.PY_START events."""
300
303
  # Entering a new frame. Decide if we should trace in this file.
301
304
  self._activity = True
302
- self.stats["starts"] += 1
305
+ if self.stats is not None:
306
+ self.stats["starts"] += 1
303
307
 
304
308
  code_info = self.code_infos.get(id(code))
305
309
  tracing_code: bool | None = None
@@ -312,10 +316,12 @@ class SysMonitor(Tracer):
312
316
  filename = code.co_filename
313
317
  disp = self.should_trace_cache.get(filename)
314
318
  if disp is None:
315
- frame = inspect.currentframe().f_back # type: ignore[union-attr]
316
- if LOG:
317
- # @panopticon adds a frame.
318
- frame = frame.f_back # type: ignore[union-attr]
319
+ frame = inspect.currentframe()
320
+ if frame is not None:
321
+ frame = inspect.currentframe().f_back # type: ignore[union-attr]
322
+ if LOG:
323
+ # @panopticon adds a frame.
324
+ frame = frame.f_back # type: ignore[union-attr]
319
325
  disp = self.should_trace(filename, frame) # type: ignore[arg-type]
320
326
  self.should_trace_cache[filename] = disp
321
327
 
@@ -335,102 +341,134 @@ class SysMonitor(Tracer):
335
341
  file_data = None
336
342
  b2l = None
337
343
 
338
- self.code_infos[id(code)] = CodeInfo(
344
+ code_info = CodeInfo(
339
345
  tracing=tracing_code,
340
346
  file_data=file_data,
341
347
  byte_to_line=b2l,
348
+ branch_trails={},
349
+ always_jumps={},
342
350
  )
351
+ self.code_infos[id(code)] = code_info
343
352
  self.code_objects.append(code)
344
353
 
345
354
  if tracing_code:
355
+ if self.stats is not None:
356
+ self.stats["start_tracing"] += 1
346
357
  events = sys.monitoring.events
347
358
  with self.lock:
348
359
  if self.sysmon_on:
349
360
  assert sys_monitoring is not None
350
- sys_monitoring.set_local_events(
351
- self.myid,
352
- code,
353
- events.PY_RETURN
354
- #
355
- | events.PY_RESUME
356
- # | events.PY_YIELD
357
- | events.LINE,
358
- # | events.BRANCH
359
- # | events.JUMP
360
- )
361
- self.local_event_codes[id(code)] = code
362
-
363
- if tracing_code and self.trace_arcs:
364
- frame = self.callers_frame()
365
- self.last_lines[frame] = -code.co_firstlineno
366
- return None
367
- else:
368
- return sys.monitoring.DISABLE
361
+ local_events = events.PY_RETURN | events.PY_RESUME | events.LINE
362
+ if self.trace_arcs:
363
+ assert env.PYBEHAVIOR.branch_right_left
364
+ local_events |= (
365
+ events.BRANCH_RIGHT # type:ignore[attr-defined]
366
+ | events.BRANCH_LEFT # type:ignore[attr-defined]
367
+ )
368
+ sys_monitoring.set_local_events(self.myid, code, local_events)
369
369
 
370
- @panopticon("code", "@")
371
- def sysmon_py_resume_arcs(
372
- self, code: CodeType, instruction_offset: int,
373
- ) -> MonitorReturn:
374
- """Handle sys.monitoring.events.PY_RESUME events for branch coverage."""
375
- frame = self.callers_frame()
376
- self.last_lines[frame] = frame.f_lineno
370
+ return DISABLE
377
371
 
378
372
  @panopticon("code", "@", None)
379
- def sysmon_py_return_arcs(
380
- self, code: CodeType, instruction_offset: int, retval: object,
373
+ def sysmon_py_return(
374
+ self,
375
+ code: CodeType,
376
+ instruction_offset: TOffset,
377
+ retval: object,
381
378
  ) -> MonitorReturn:
382
379
  """Handle sys.monitoring.events.PY_RETURN events for branch coverage."""
383
- frame = self.callers_frame()
384
- code_info = self.code_infos.get(id(code))
385
- if code_info is not None and code_info.file_data is not None:
386
- last_line = self.last_lines.get(frame)
387
- if last_line is not None:
388
- arc = (last_line, -code.co_firstlineno)
389
- # log(f"adding {arc=}")
390
- cast(set[TArc], code_info.file_data).add(arc)
391
-
392
- # Leaving this function, no need for the frame any more.
393
- self.last_lines.pop(frame, None)
394
-
395
- @panopticon("code", "@", "exc")
396
- def sysmon_py_unwind_arcs(
397
- self, code: CodeType, instruction_offset: int, exception: BaseException,
398
- ) -> MonitorReturn:
399
- """Handle sys.monitoring.events.PY_UNWIND events for branch coverage."""
400
- frame = self.callers_frame()
401
- # Leaving this function.
402
- last_line = self.last_lines.pop(frame, None)
403
- if isinstance(exception, GeneratorExit):
404
- # We don't want to count generator exits as arcs.
405
- return
380
+ if self.stats is not None:
381
+ self.stats["returns"] += 1
406
382
  code_info = self.code_infos.get(id(code))
407
- if code_info is not None and code_info.file_data is not None:
408
- if last_line is not None:
409
- arc = (last_line, -code.co_firstlineno)
410
- # log(f"adding {arc=}")
411
- cast(set[TArc], code_info.file_data).add(arc)
412
-
383
+ # code_info is not None and code_info.file_data is not None, since we
384
+ # wouldn't have enabled this event if they were.
385
+ last_line = code_info.byte_to_line[instruction_offset] # type: ignore
386
+ if last_line is not None:
387
+ arc = (last_line, -code.co_firstlineno)
388
+ code_info.file_data.add(arc) # type: ignore
389
+ # log(f"adding {arc=}")
390
+ return DISABLE
413
391
 
414
392
  @panopticon("code", "line")
415
- def sysmon_line_lines(self, code: CodeType, line_number: int) -> MonitorReturn:
393
+ def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
416
394
  """Handle sys.monitoring.events.LINE events for line coverage."""
417
- code_info = self.code_infos[id(code)]
418
- if code_info.file_data is not None:
419
- cast(set[TLineNo], code_info.file_data).add(line_number)
420
- # log(f"adding {line_number=}")
421
- return sys.monitoring.DISABLE
395
+ if self.stats is not None:
396
+ self.stats["line_lines"] += 1
397
+ code_info = self.code_infos.get(id(code))
398
+ # It should be true that code_info is not None and code_info.file_data
399
+ # is not None, since we wouldn't have enabled this event if they were.
400
+ # But somehow code_info can be None here, so we have to check.
401
+ if code_info is not None and code_info.file_data is not None:
402
+ code_info.file_data.add(line_number) # type: ignore
403
+ # log(f"adding {line_number=}")
404
+ return DISABLE
422
405
 
423
406
  @panopticon("code", "line")
424
- def sysmon_line_arcs(self, code: CodeType, line_number: int) -> MonitorReturn:
407
+ def sysmon_line_arcs(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
425
408
  """Handle sys.monitoring.events.LINE events for branch coverage."""
409
+ if self.stats is not None:
410
+ self.stats["line_arcs"] += 1
426
411
  code_info = self.code_infos[id(code)]
427
- ret = None
428
- if code_info.file_data is not None:
429
- frame = self.callers_frame()
430
- last_line = self.last_lines.get(frame)
431
- if last_line is not None:
432
- arc = (last_line, line_number)
433
- cast(set[TArc], code_info.file_data).add(arc)
434
- # log(f"adding {arc=}")
435
- self.last_lines[frame] = line_number
436
- return ret
412
+ # code_info is not None and code_info.file_data is not None, since we
413
+ # wouldn't have enabled this event if they were.
414
+ arc = (line_number, line_number)
415
+ code_info.file_data.add(arc) # type: ignore
416
+ # log(f"adding {arc=}")
417
+ return DISABLE
418
+
419
+ @panopticon("code", "@", "@")
420
+ def sysmon_branch_either(
421
+ self, code: CodeType, instruction_offset: TOffset, destination_offset: TOffset
422
+ ) -> MonitorReturn:
423
+ """Handle BRANCH_RIGHT and BRANCH_LEFT events."""
424
+ if self.stats is not None:
425
+ self.stats["branches"] += 1
426
+ code_info = self.code_infos[id(code)]
427
+ # code_info is not None and code_info.file_data is not None, since we
428
+ # wouldn't have enabled this event if they were.
429
+ if not code_info.branch_trails:
430
+ if self.stats is not None:
431
+ self.stats["branch_trails"] += 1
432
+ multiline_map = get_multiline_map(code.co_filename)
433
+ code_info.branch_trails = branch_trails(code, multiline_map=multiline_map)
434
+ code_info.always_jumps = always_jumps(code)
435
+ # log(f"branch_trails for {code}:\n{ppformat(code_info.branch_trails)}")
436
+ added_arc = False
437
+ dest_info = code_info.branch_trails.get(instruction_offset)
438
+
439
+ # Re-map the destination offset through always-jumps to deal with NOP etc.
440
+ dests = {destination_offset}
441
+ while (dest := code_info.always_jumps.get(destination_offset)) is not None:
442
+ destination_offset = dest
443
+ dests.add(destination_offset)
444
+
445
+ # log(f"dest_info = {ppformat(dest_info)}")
446
+ if dest_info is not None:
447
+ for arc, offsets in dest_info.items():
448
+ if arc is None:
449
+ continue
450
+ if dests & offsets:
451
+ code_info.file_data.add(arc) # type: ignore
452
+ # log(f"adding {arc=}")
453
+ added_arc = True
454
+ break
455
+
456
+ if not added_arc:
457
+ # This could be an exception jumping from line to line.
458
+ assert code_info.byte_to_line is not None
459
+ l1 = code_info.byte_to_line[instruction_offset]
460
+ l2 = code_info.byte_to_line.get(destination_offset)
461
+ if l2 is not None and l1 != l2:
462
+ arc = (l1, l2)
463
+ code_info.file_data.add(arc) # type: ignore
464
+ # log(f"adding unforeseen {arc=}")
465
+
466
+ return DISABLE
467
+
468
+
469
+ @functools.lru_cache(maxsize=5)
470
+ def get_multiline_map(filename: str) -> dict[TLineNo, TLineNo]:
471
+ """Get a PythonParser for the given filename, cached."""
472
+ parser = PythonParser(filename=filename)
473
+ parser.parse_source()
474
+ return parser.multiline_map
coverage/templite.py CHANGED
@@ -13,19 +13,18 @@ http://aosabook.org/en/500L/a-template-engine.html
13
13
  from __future__ import annotations
14
14
 
15
15
  import re
16
-
17
- from typing import (
18
- Any, Callable, NoReturn, cast,
19
- )
16
+ from typing import Any, Callable, NoReturn, cast
20
17
 
21
18
 
22
19
  class TempliteSyntaxError(ValueError):
23
20
  """Raised when a template has a syntax error."""
21
+
24
22
  pass
25
23
 
26
24
 
27
25
  class TempliteValueError(ValueError):
28
26
  """Raised when an expression won't evaluate in a template."""
27
+
29
28
  pass
30
29
 
31
30
 
@@ -53,7 +52,7 @@ class CodeBuilder:
53
52
  self.code.append(section)
54
53
  return section
55
54
 
56
- INDENT_STEP = 4 # PEP8 says so!
55
+ INDENT_STEP = 4 # PEP8 says so!
57
56
 
58
57
  def indent(self) -> None:
59
58
  """Increase the current indent for following lines."""
@@ -117,6 +116,7 @@ class Templite:
117
116
  })
118
117
 
119
118
  """
119
+
120
120
  def __init__(self, text: str, *contexts: dict[str, Any]) -> None:
121
121
  """Construct a Templite with the given `text`.
122
122
 
@@ -163,7 +163,7 @@ class Templite:
163
163
  for token in tokens:
164
164
  if token.startswith("{"):
165
165
  start, end = 2, -2
166
- squash = (token[-3] == "-")
166
+ squash = (token[-3] == "-") # fmt: skip
167
167
  if squash:
168
168
  end = -3
169
169