coverage 7.6.10__cp312-cp312-musllinux_1_2_aarch64.whl → 7.12.0__cp312-cp312-musllinux_1_2_aarch64.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 (57) hide show
  1. coverage/__init__.py +3 -1
  2. coverage/__main__.py +3 -1
  3. coverage/annotate.py +2 -3
  4. coverage/bytecode.py +178 -4
  5. coverage/cmdline.py +330 -155
  6. coverage/collector.py +32 -43
  7. coverage/config.py +167 -63
  8. coverage/context.py +5 -6
  9. coverage/control.py +165 -86
  10. coverage/core.py +71 -34
  11. coverage/data.py +4 -5
  12. coverage/debug.py +113 -57
  13. coverage/disposition.py +2 -1
  14. coverage/env.py +29 -78
  15. coverage/exceptions.py +29 -7
  16. coverage/execfile.py +19 -14
  17. coverage/files.py +24 -19
  18. coverage/html.py +118 -75
  19. coverage/htmlfiles/coverage_html.js +12 -10
  20. coverage/htmlfiles/index.html +45 -10
  21. coverage/htmlfiles/pyfile.html +2 -2
  22. coverage/htmlfiles/style.css +54 -6
  23. coverage/htmlfiles/style.scss +85 -3
  24. coverage/inorout.py +62 -45
  25. coverage/jsonreport.py +22 -9
  26. coverage/lcovreport.py +16 -18
  27. coverage/misc.py +51 -47
  28. coverage/multiproc.py +12 -7
  29. coverage/numbits.py +4 -5
  30. coverage/parser.py +150 -251
  31. coverage/patch.py +166 -0
  32. coverage/phystokens.py +25 -26
  33. coverage/plugin.py +14 -14
  34. coverage/plugin_support.py +37 -36
  35. coverage/python.py +13 -14
  36. coverage/pytracer.py +31 -33
  37. coverage/regions.py +3 -2
  38. coverage/report.py +60 -44
  39. coverage/report_core.py +7 -10
  40. coverage/results.py +152 -68
  41. coverage/sqldata.py +261 -211
  42. coverage/sqlitedb.py +37 -29
  43. coverage/sysmon.py +237 -162
  44. coverage/templite.py +19 -7
  45. coverage/tomlconfig.py +13 -13
  46. coverage/tracer.cpython-312-aarch64-linux-musl.so +0 -0
  47. coverage/tracer.pyi +3 -1
  48. coverage/types.py +26 -23
  49. coverage/version.py +4 -19
  50. coverage/xmlreport.py +17 -14
  51. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/METADATA +50 -28
  52. coverage-7.12.0.dist-info/RECORD +59 -0
  53. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/WHEEL +1 -1
  54. coverage-7.6.10.dist-info/RECORD +0 -58
  55. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/entry_points.txt +0 -0
  56. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info/licenses}/LICENSE.txt +0 -0
  57. {coverage-7.6.10.dist-info → coverage-7.12.0.dist-info}/top_level.txt +0 -0
coverage/sysmon.py CHANGED
@@ -1,12 +1,11 @@
1
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
2
+ # For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
3
3
 
4
4
  """Callback functions and support for sys.monitoring data collection."""
5
5
 
6
- # TODO: https://github.com/python/cpython/issues/111963#issuecomment-2386584080
7
-
8
6
  from __future__ import annotations
9
7
 
8
+ import collections
10
9
  import functools
11
10
  import inspect
12
11
  import os
@@ -14,46 +13,49 @@ import os.path
14
13
  import sys
15
14
  import threading
16
15
  import traceback
17
-
18
16
  from dataclasses import dataclass
19
- from types import CodeType, FrameType
20
- from typing import (
21
- Any,
22
- Callable,
23
- TYPE_CHECKING,
24
- cast,
25
- )
17
+ from types import CodeType
18
+ from typing import Any, Callable, NewType, Optional, cast
26
19
 
20
+ from coverage import env
21
+ from coverage.bytecode import TBranchTrails, always_jumps, branch_trails
27
22
  from coverage.debug import short_filename, short_stack
23
+ from coverage.exceptions import NoSource, NotPython
28
24
  from coverage.misc import isolate_module
25
+ from coverage.parser import PythonParser
29
26
  from coverage.types import (
30
27
  AnyCallable,
31
- TArc,
32
28
  TFileDisposition,
33
29
  TLineNo,
30
+ TOffset,
31
+ Tracer,
34
32
  TShouldStartContextFn,
35
33
  TShouldTraceFn,
36
34
  TTraceData,
37
35
  TTraceFileData,
38
- Tracer,
39
36
  TWarnFn,
40
37
  )
41
38
 
39
+ # Only needed for some of the commented-out logging:
40
+ # from coverage.debug import ppformat
41
+
42
42
  os = isolate_module(os)
43
43
 
44
44
  # pylint: disable=unused-argument
45
45
 
46
- LOG = False
46
+ # $set_env.py: COVERAGE_SYSMON_LOG - Log sys.monitoring activity
47
+ LOG = bool(int(os.getenv("COVERAGE_SYSMON_LOG", 0)))
48
+
49
+ # $set_env.py: COVERAGE_SYSMON_STATS - Collect sys.monitoring stats
50
+ COLLECT_STATS = bool(int(os.getenv("COVERAGE_SYSMON_STATS", 0)))
47
51
 
48
52
  # This module will be imported in all versions of Python, but only used in 3.12+
49
53
  # It will be type-checked for 3.12, but not for earlier versions.
50
54
  sys_monitoring = getattr(sys, "monitoring", None)
51
55
 
52
- if TYPE_CHECKING:
53
- assert sys_monitoring is not None
54
- # I want to say this but it's not allowed:
55
- # MonitorReturn = Literal[sys.monitoring.DISABLE] | None
56
- MonitorReturn = Any
56
+ DISABLE_TYPE = NewType("DISABLE_TYPE", object)
57
+ MonitorReturn = Optional[DISABLE_TYPE]
58
+ DISABLE = cast(MonitorReturn, getattr(sys_monitoring, "DISABLE", None))
57
59
 
58
60
 
59
61
  if LOG: # pragma: debugging
@@ -76,7 +78,10 @@ if LOG: # pragma: debugging
76
78
  assert sys_monitoring is not None
77
79
 
78
80
  short_stack = functools.partial(
79
- short_stack, full=True, short_filenames=True, frame_ids=True,
81
+ short_stack,
82
+ full=True,
83
+ short_filenames=True,
84
+ frame_ids=True,
80
85
  )
81
86
  seen_threads: set[int] = set()
82
87
 
@@ -98,8 +103,11 @@ if LOG: # pragma: debugging
98
103
  # f"{root}-{pid}.out",
99
104
  # f"{root}-{pid}-{tslug}.out",
100
105
  ]:
101
- with open(filename, "a") as f:
102
- print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
106
+ with open(filename, "a", encoding="utf-8") as f:
107
+ try:
108
+ print(f"{pid}:{tslug}: {msg}", file=f, flush=True)
109
+ except UnicodeError:
110
+ print(f"{pid}:{tslug}: {ascii(msg)}", file=f, flush=True)
103
111
 
104
112
  def arg_repr(arg: Any) -> str:
105
113
  """Make a customized repr for logged values."""
@@ -130,7 +138,8 @@ if LOG: # pragma: debugging
130
138
  return ret
131
139
  except Exception as exc:
132
140
  log(f"!!{exc.__class__.__name__}: {exc}")
133
- log("".join(traceback.format_exception(exc))) # pylint: disable=[no-value-for-parameter]
141
+ if 1:
142
+ log("".join(traceback.format_exception(exc)))
134
143
  try:
135
144
  assert sys_monitoring is not None
136
145
  sys_monitoring.set_events(sys.monitoring.COVERAGE_ID, 0)
@@ -163,11 +172,22 @@ class CodeInfo:
163
172
 
164
173
  tracing: bool
165
174
  file_data: TTraceFileData | None
166
- # TODO: what is byte_to_line for?
167
- byte_to_line: dict[int, int] | None
175
+ byte_to_line: dict[TOffset, TLineNo] | None
168
176
 
177
+ # Keys are start instruction offsets for branches.
178
+ # Values are dicts:
179
+ # {
180
+ # (from_line, to_line): {offset, offset, ...},
181
+ # (from_line, to_line): {offset, offset, ...},
182
+ # }
183
+ branch_trails: TBranchTrails
169
184
 
170
- def bytes_to_lines(code: CodeType) -> dict[int, int]:
185
+ # Always-jumps are bytecode offsets that do no work but move
186
+ # to another offset.
187
+ always_jumps: dict[TOffset, TOffset]
188
+
189
+
190
+ def bytes_to_lines(code: CodeType) -> dict[TOffset, TLineNo]:
171
191
  """Make a dict mapping byte code offsets to line numbers."""
172
192
  b2l = {}
173
193
  for bstart, bend, lineno in code.co_lines():
@@ -204,17 +224,20 @@ class SysMonitor(Tracer):
204
224
  # A list of code_objects, just to keep them alive so that id's are
205
225
  # useful as identity.
206
226
  self.code_objects: list[CodeType] = []
207
- self.last_lines: dict[FrameType, int] = {}
208
- # Map id(code_object) -> code_object
209
- self.local_event_codes: dict[int, CodeType] = {}
227
+
228
+ # Map filename:__name__ -> set(id(code_object))
229
+ self.filename_code_ids: dict[str, set[int]] = collections.defaultdict(set)
230
+
210
231
  self.sysmon_on = False
211
232
  self.lock = threading.Lock()
212
233
 
213
- self.stats = {
214
- "starts": 0,
215
- }
234
+ self.stats: dict[str, int] | None = None
235
+ if COLLECT_STATS:
236
+ self.stats = dict.fromkeys(
237
+ "starts start_tracing returns line_lines line_arcs branches branch_trails".split(),
238
+ 0,
239
+ )
216
240
 
217
- self.stopped = False
218
241
  self._activity = False
219
242
 
220
243
  def __repr__(self) -> str:
@@ -225,44 +248,59 @@ class SysMonitor(Tracer):
225
248
  @panopticon()
226
249
  def start(self) -> None:
227
250
  """Start this Tracer."""
228
- self.stopped = False
229
-
230
- assert sys_monitoring is not None
231
- sys_monitoring.use_tool_id(self.myid, "coverage.py")
232
- register = functools.partial(sys_monitoring.register_callback, self.myid)
233
- events = sys_monitoring.events
234
- if self.trace_arcs:
235
- sys_monitoring.set_events(
236
- self.myid,
237
- events.PY_START | events.PY_UNWIND,
238
- )
239
- register(events.PY_START, self.sysmon_py_start)
240
- register(events.PY_RESUME, self.sysmon_py_resume_arcs)
241
- register(events.PY_RETURN, self.sysmon_py_return_arcs)
242
- register(events.PY_UNWIND, self.sysmon_py_unwind_arcs)
243
- register(events.LINE, self.sysmon_line_arcs)
244
- else:
251
+ with self.lock:
252
+ assert sys_monitoring is not None
253
+ sys_monitoring.use_tool_id(self.myid, "coverage.py")
254
+ register = functools.partial(sys_monitoring.register_callback, self.myid)
255
+ events = sys.monitoring.events
256
+
245
257
  sys_monitoring.set_events(self.myid, events.PY_START)
246
258
  register(events.PY_START, self.sysmon_py_start)
247
- register(events.LINE, self.sysmon_line_lines)
248
- sys_monitoring.restart_events()
249
- self.sysmon_on = True
259
+ if self.trace_arcs:
260
+ register(events.PY_RETURN, self.sysmon_py_return)
261
+ register(events.LINE, self.sysmon_line_arcs)
262
+ if env.PYBEHAVIOR.branch_right_left:
263
+ register(
264
+ events.BRANCH_RIGHT, # type:ignore[attr-defined]
265
+ self.sysmon_branch_either,
266
+ )
267
+ register(
268
+ events.BRANCH_LEFT, # type:ignore[attr-defined]
269
+ self.sysmon_branch_either,
270
+ )
271
+ else:
272
+ register(events.LINE, self.sysmon_line_lines)
273
+ sys_monitoring.restart_events()
274
+ self.sysmon_on = True
250
275
 
251
276
  @panopticon()
252
277
  def stop(self) -> None:
253
278
  """Stop this Tracer."""
254
- if not self.sysmon_on:
255
- # In forking situations, we might try to stop when we are not
256
- # started. Do nothing in that case.
257
- return
258
- assert sys_monitoring is not None
259
- sys_monitoring.set_events(self.myid, 0)
260
279
  with self.lock:
280
+ if not self.sysmon_on:
281
+ # In forking situations, we might try to stop when we are not
282
+ # started. Do nothing in that case.
283
+ return
284
+ assert sys_monitoring is not None
285
+ sys_monitoring.set_events(self.myid, 0)
261
286
  self.sysmon_on = False
262
- for code in self.local_event_codes.values():
263
- sys_monitoring.set_local_events(self.myid, code, 0)
264
- self.local_event_codes = {}
265
- sys_monitoring.free_tool_id(self.myid)
287
+ sys_monitoring.free_tool_id(self.myid)
288
+
289
+ if LOG: # pragma: debugging
290
+ items = sorted(
291
+ self.filename_code_ids.items(),
292
+ key=lambda item: len(item[1]),
293
+ reverse=True,
294
+ )
295
+ code_objs = sum(len(code_ids) for _, code_ids in items)
296
+ dupes = code_objs - len(items)
297
+ if dupes:
298
+ log(f"==== Duplicate code objects: {dupes} duplicates, {code_objs} total")
299
+ for filename, code_ids in items:
300
+ if len(code_ids) > 1:
301
+ log(f"{len(code_ids):>5} objects: {filename}")
302
+ else:
303
+ log("==== Duplicate code objects: none")
266
304
 
267
305
  @panopticon()
268
306
  def post_fork(self) -> None:
@@ -279,30 +317,16 @@ class SysMonitor(Tracer):
279
317
 
280
318
  def get_stats(self) -> dict[str, int] | None:
281
319
  """Return a dictionary of statistics, or None."""
282
- return None
283
-
284
- # The number of frames in callers_frame takes @panopticon into account.
285
- if LOG:
286
-
287
- def callers_frame(self) -> FrameType:
288
- """Get the frame of the Python code we're monitoring."""
289
- return (
290
- inspect.currentframe().f_back.f_back.f_back # type: ignore[union-attr,return-value]
291
- )
292
-
293
- else:
294
-
295
- def callers_frame(self) -> FrameType:
296
- """Get the frame of the Python code we're monitoring."""
297
- return inspect.currentframe().f_back.f_back # type: ignore[union-attr,return-value]
320
+ return self.stats
298
321
 
299
322
  @panopticon("code", "@")
300
- def sysmon_py_start(self, code: CodeType, instruction_offset: int) -> MonitorReturn:
323
+ def sysmon_py_start(self, code: CodeType, instruction_offset: TOffset) -> MonitorReturn:
301
324
  """Handle sys.monitoring.events.PY_START events."""
302
- # Entering a new frame. Decide if we should trace in this file.
303
325
  self._activity = True
304
- self.stats["starts"] += 1
326
+ if self.stats is not None:
327
+ self.stats["starts"] += 1
305
328
 
329
+ # Entering a new frame. Decide if we should trace in this file.
306
330
  code_info = self.code_infos.get(id(code))
307
331
  tracing_code: bool | None = None
308
332
  file_data: TTraceFileData | None = None
@@ -314,10 +338,12 @@ class SysMonitor(Tracer):
314
338
  filename = code.co_filename
315
339
  disp = self.should_trace_cache.get(filename)
316
340
  if disp is None:
317
- frame = inspect.currentframe().f_back # type: ignore[union-attr]
318
- if LOG:
319
- # @panopticon adds a frame.
320
- frame = frame.f_back # type: ignore[union-attr]
341
+ frame = inspect.currentframe()
342
+ if frame is not None:
343
+ frame = inspect.currentframe().f_back # type: ignore[union-attr]
344
+ if LOG: # pragma: debugging
345
+ # @panopticon adds a frame.
346
+ frame = frame.f_back # type: ignore[union-attr]
321
347
  disp = self.should_trace(filename, frame) # type: ignore[arg-type]
322
348
  self.should_trace_cache[filename] = disp
323
349
 
@@ -337,102 +363,151 @@ class SysMonitor(Tracer):
337
363
  file_data = None
338
364
  b2l = None
339
365
 
340
- self.code_infos[id(code)] = CodeInfo(
366
+ code_info = CodeInfo(
341
367
  tracing=tracing_code,
342
368
  file_data=file_data,
343
369
  byte_to_line=b2l,
370
+ branch_trails={},
371
+ always_jumps={},
344
372
  )
373
+ self.code_infos[id(code)] = code_info
345
374
  self.code_objects.append(code)
346
375
 
347
376
  if tracing_code:
377
+ if self.stats is not None:
378
+ self.stats["start_tracing"] += 1
348
379
  events = sys.monitoring.events
349
380
  with self.lock:
350
381
  if self.sysmon_on:
351
382
  assert sys_monitoring is not None
352
- sys_monitoring.set_local_events(
353
- self.myid,
354
- code,
355
- events.PY_RETURN
356
- #
357
- | events.PY_RESUME
358
- # | events.PY_YIELD
359
- | events.LINE,
360
- # | events.BRANCH
361
- # | events.JUMP
362
- )
363
- self.local_event_codes[id(code)] = code
364
-
365
- if tracing_code and self.trace_arcs:
366
- frame = self.callers_frame()
367
- self.last_lines[frame] = -code.co_firstlineno
368
- return None
369
- else:
370
- return sys.monitoring.DISABLE
371
-
372
- @panopticon("code", "@")
373
- def sysmon_py_resume_arcs(
374
- self, code: CodeType, instruction_offset: int,
375
- ) -> MonitorReturn:
376
- """Handle sys.monitoring.events.PY_RESUME events for branch coverage."""
377
- frame = self.callers_frame()
378
- self.last_lines[frame] = frame.f_lineno
383
+ local_events = events.PY_RETURN | events.PY_RESUME | events.LINE
384
+ if self.trace_arcs:
385
+ assert env.PYBEHAVIOR.branch_right_left
386
+ local_events |= (
387
+ events.BRANCH_RIGHT # type:ignore[attr-defined]
388
+ | events.BRANCH_LEFT # type:ignore[attr-defined]
389
+ )
390
+ sys_monitoring.set_local_events(self.myid, code, local_events)
391
+
392
+ if LOG: # pragma: debugging
393
+ if code.co_filename not in {"<string>"}:
394
+ self.filename_code_ids[f"{code.co_filename}:{code.co_name}"].add(
395
+ id(code)
396
+ )
397
+
398
+ return DISABLE
379
399
 
380
400
  @panopticon("code", "@", None)
381
- def sysmon_py_return_arcs(
382
- self, code: CodeType, instruction_offset: int, retval: object,
401
+ def sysmon_py_return(
402
+ self,
403
+ code: CodeType,
404
+ instruction_offset: TOffset,
405
+ retval: object,
383
406
  ) -> MonitorReturn:
384
407
  """Handle sys.monitoring.events.PY_RETURN events for branch coverage."""
385
- frame = self.callers_frame()
386
- code_info = self.code_infos.get(id(code))
387
- if code_info is not None and code_info.file_data is not None:
388
- last_line = self.last_lines.get(frame)
389
- if last_line is not None:
390
- arc = (last_line, -code.co_firstlineno)
391
- # log(f"adding {arc=}")
392
- cast(set[TArc], code_info.file_data).add(arc)
393
-
394
- # Leaving this function, no need for the frame any more.
395
- self.last_lines.pop(frame, None)
396
-
397
- @panopticon("code", "@", "exc")
398
- def sysmon_py_unwind_arcs(
399
- self, code: CodeType, instruction_offset: int, exception: BaseException,
400
- ) -> MonitorReturn:
401
- """Handle sys.monitoring.events.PY_UNWIND events for branch coverage."""
402
- frame = self.callers_frame()
403
- # Leaving this function.
404
- last_line = self.last_lines.pop(frame, None)
405
- if isinstance(exception, GeneratorExit):
406
- # We don't want to count generator exits as arcs.
407
- return
408
+ if self.stats is not None:
409
+ self.stats["returns"] += 1
408
410
  code_info = self.code_infos.get(id(code))
409
- if code_info is not None and code_info.file_data is not None:
410
- if last_line is not None:
411
- arc = (last_line, -code.co_firstlineno)
412
- # log(f"adding {arc=}")
413
- cast(set[TArc], code_info.file_data).add(arc)
414
-
411
+ # code_info is not None and code_info.file_data is not None, since we
412
+ # wouldn't have enabled this event if they were.
413
+ last_line = code_info.byte_to_line.get(instruction_offset) # type: ignore
414
+ if last_line is not None:
415
+ arc = (last_line, -code.co_firstlineno)
416
+ code_info.file_data.add(arc) # type: ignore
417
+ # log(f"adding {arc=}")
418
+ return DISABLE
415
419
 
416
420
  @panopticon("code", "line")
417
- def sysmon_line_lines(self, code: CodeType, line_number: int) -> MonitorReturn:
421
+ def sysmon_line_lines(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
418
422
  """Handle sys.monitoring.events.LINE events for line coverage."""
419
- code_info = self.code_infos[id(code)]
420
- if code_info.file_data is not None:
421
- cast(set[TLineNo], code_info.file_data).add(line_number)
422
- # log(f"adding {line_number=}")
423
- return sys.monitoring.DISABLE
423
+ if self.stats is not None:
424
+ self.stats["line_lines"] += 1
425
+ code_info = self.code_infos.get(id(code))
426
+ # It should be true that code_info is not None and code_info.file_data
427
+ # is not None, since we wouldn't have enabled this event if they were.
428
+ # But somehow code_info can be None here, so we have to check.
429
+ if code_info is not None and code_info.file_data is not None:
430
+ code_info.file_data.add(line_number) # type: ignore
431
+ # log(f"adding {line_number=}")
432
+ return DISABLE
424
433
 
425
434
  @panopticon("code", "line")
426
- def sysmon_line_arcs(self, code: CodeType, line_number: int) -> MonitorReturn:
435
+ def sysmon_line_arcs(self, code: CodeType, line_number: TLineNo) -> MonitorReturn:
427
436
  """Handle sys.monitoring.events.LINE events for branch coverage."""
437
+ if self.stats is not None:
438
+ self.stats["line_arcs"] += 1
428
439
  code_info = self.code_infos[id(code)]
429
- ret = None
430
- if code_info.file_data is not None:
431
- frame = self.callers_frame()
432
- last_line = self.last_lines.get(frame)
433
- if last_line is not None:
434
- arc = (last_line, line_number)
435
- cast(set[TArc], code_info.file_data).add(arc)
436
- # log(f"adding {arc=}")
437
- self.last_lines[frame] = line_number
438
- return ret
440
+ # code_info is not None and code_info.file_data is not None, since we
441
+ # wouldn't have enabled this event if they were.
442
+ arc = (line_number, line_number)
443
+ code_info.file_data.add(arc) # type: ignore
444
+ # log(f"adding {arc=}")
445
+ return DISABLE
446
+
447
+ @panopticon("code", "@", "@")
448
+ def sysmon_branch_either(
449
+ self, code: CodeType, instruction_offset: TOffset, destination_offset: TOffset
450
+ ) -> MonitorReturn:
451
+ """Handle BRANCH_RIGHT and BRANCH_LEFT events."""
452
+ if self.stats is not None:
453
+ self.stats["branches"] += 1
454
+ code_info = self.code_infos[id(code)]
455
+ # code_info is not None and code_info.file_data is not None, since we
456
+ # wouldn't have enabled this event if they were.
457
+ if not code_info.branch_trails:
458
+ if self.stats is not None:
459
+ self.stats["branch_trails"] += 1
460
+ multiline_map = get_multiline_map(code.co_filename)
461
+ code_info.branch_trails = branch_trails(code, multiline_map=multiline_map)
462
+ code_info.always_jumps = always_jumps(code)
463
+ # log(f"branch_trails for {code}:\n{ppformat(code_info.branch_trails)}")
464
+ added_arc = False
465
+ dest_info = code_info.branch_trails.get(instruction_offset)
466
+
467
+ # Re-map the destination offset through always-jumps to deal with NOP etc.
468
+ dests = {destination_offset}
469
+ while (dest := code_info.always_jumps.get(destination_offset)) is not None:
470
+ destination_offset = dest
471
+ dests.add(destination_offset)
472
+
473
+ # log(f"dest_info = {ppformat(dest_info)}")
474
+ if dest_info is not None:
475
+ for arc, offsets in dest_info.items():
476
+ if arc is None:
477
+ continue
478
+ if dests & offsets:
479
+ code_info.file_data.add(arc) # type: ignore
480
+ # log(f"adding {arc=}")
481
+ added_arc = True
482
+ break
483
+
484
+ if not added_arc:
485
+ # This could be an exception jumping from line to line.
486
+ assert code_info.byte_to_line is not None
487
+ l1 = code_info.byte_to_line.get(instruction_offset)
488
+ if l1 is not None:
489
+ l2 = code_info.byte_to_line.get(destination_offset)
490
+ if l2 is not None and l1 != l2:
491
+ arc = (l1, l2)
492
+ code_info.file_data.add(arc) # type: ignore
493
+ # log(f"adding unforeseen {arc=}")
494
+
495
+ return DISABLE
496
+
497
+
498
+ @functools.lru_cache(maxsize=5)
499
+ def get_multiline_map(filename: str) -> dict[TLineNo, TLineNo]:
500
+ """Get a PythonParser for the given filename, cached."""
501
+ try:
502
+ parser = PythonParser(filename=filename)
503
+ parser.parse_source()
504
+ except NotPython:
505
+ # The file was not Python. This can happen when the code object refers
506
+ # to an original non-Python source file, like a Jinja template.
507
+ # In that case, just return an empty map, which might lead to slightly
508
+ # wrong branch coverage, but we don't have any better option.
509
+ return {}
510
+ except NoSource:
511
+ # This can happen if open() in python.py fails.
512
+ return {}
513
+ return parser.multiline_map
coverage/templite.py CHANGED
@@ -1,5 +1,5 @@
1
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
2
+ # For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
3
3
 
4
4
  """A simple Python template renderer, for a nano-subset of Django syntax.
5
5
 
@@ -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."""
@@ -90,6 +89,10 @@ class Templite:
90
89
 
91
90
  {% if var %}...{% endif %}
92
91
 
92
+ if-else::
93
+
94
+ {% if var %}...{% else %}...{% endif %}
95
+
93
96
  Comments are within curly-hash markers::
94
97
 
95
98
  {# This will be ignored #}
@@ -117,6 +120,7 @@ class Templite:
117
120
  })
118
121
 
119
122
  """
123
+
120
124
  def __init__(self, text: str, *contexts: dict[str, Any]) -> None:
121
125
  """Construct a Templite with the given `text`.
122
126
 
@@ -163,7 +167,7 @@ class Templite:
163
167
  for token in tokens:
164
168
  if token.startswith("{"):
165
169
  start, end = 2, -2
166
- squash = (token[-3] == "-")
170
+ squash = (token[-3] == "-") # fmt: skip
167
171
  if squash:
168
172
  end = -3
169
173
 
@@ -187,6 +191,14 @@ class Templite:
187
191
  ops_stack.append("if")
188
192
  code.add_line("if %s:" % self._expr_code(words[1]))
189
193
  code.indent()
194
+ elif words[0] == "else":
195
+ if len(words) != 1:
196
+ self._syntax_error("Don't understand else", token)
197
+ if not ops_stack or ops_stack[-1] != "if":
198
+ self._syntax_error("Mismatched else", token)
199
+ code.dedent()
200
+ code.add_line("else:")
201
+ code.indent()
190
202
  elif words[0] == "for":
191
203
  # A loop: iterate over expression result.
192
204
  if len(words) != 4 or words[2] != "in":