Coverage for coverage / annotate.py: 100.000%
60 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
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
4"""Source file annotation for coverage.py."""
6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
8import os 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
9import re 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
10from collections.abc import Iterable 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
11from typing import TYPE_CHECKING 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
13from coverage.files import flat_rootname 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
14from coverage.misc import ensure_dir, isolate_module 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
15from coverage.plugin import FileReporter 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
16from coverage.report_core import get_analysis_to_report 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
17from coverage.results import Analysis 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
18from coverage.types import TMorf 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
20if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
21 from coverage import Coverage
23os = isolate_module(os) 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
26class AnnotateReporter: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
27 """Generate annotated source files showing line coverage.
29 This reporter creates annotated copies of the measured source files. Each
30 .py file is copied as a .py,cover file, with a left-hand margin annotating
31 each line::
33 > def h(x):
34 - if 0: #pragma: no cover
35 - pass
36 > if x == 1:
37 ! a = 1
38 > else:
39 > a = 2
41 > h(2)
43 Executed lines use ">", lines not executed use "!", lines excluded from
44 consideration use "-".
46 """
48 def __init__(self, coverage: Coverage) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
49 self.coverage = coverage 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
50 self.config = self.coverage.config 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
51 self.directory: str | None = None 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
53 blank_re = re.compile(r"\s*(#|$)") 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
54 else_re = re.compile(r"\s*else\s*:\s*(#|$)") 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
56 def report(self, morfs: Iterable[TMorf] | None, directory: str | None = None) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
57 """Run the report.
59 See `coverage.report()` for arguments.
61 """
62 self.directory = directory 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
63 self.coverage.get_data() 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
64 for fr, analysis in get_analysis_to_report(self.coverage, morfs): 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
65 self.annotate_file(fr, analysis) 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
67 def annotate_file(self, fr: FileReporter, analysis: Analysis) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
68 """Annotate a single file.
70 `fr` is the FileReporter for the file to annotate.
72 """
73 statements = sorted(analysis.statements) 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
74 missing = sorted(analysis.missing) 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
75 excluded = sorted(analysis.excluded) 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
77 if self.directory: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
78 ensure_dir(self.directory) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
79 dest_file = os.path.join(self.directory, flat_rootname(fr.relative_filename())) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
80 assert dest_file.endswith("_py") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
81 dest_file = dest_file[:-3] + ".py" 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
82 else:
83 dest_file = fr.filename 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
84 dest_file += ",cover" 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
86 with open(dest_file, "w", encoding="utf-8") as dest: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
87 i = j = 0 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
88 covered = True 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
89 source = fr.source() 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
90 for lineno, line in enumerate(source.splitlines(True), start=1): 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
91 while i < len(statements) and statements[i] < lineno: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
92 i += 1 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
93 while j < len(missing) and missing[j] < lineno: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
94 j += 1 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJ6KLMN7OP8QRST9UV!WXYZ#01$234
95 if i < len(statements) and statements[i] == lineno: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
96 covered = j >= len(missing) or missing[j] > lineno 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
97 if self.blank_re.match(line): 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
98 dest.write(" ") 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
99 elif self.else_re.match(line): 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
100 # Special logic for lines containing only "else:".
101 if j >= len(missing): 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
102 dest.write("> ") 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJ6KLMN7OP8QRST9UV!WXYZ#01$234
103 elif statements[i] == missing[j]: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
104 dest.write("! ") 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
105 else:
106 dest.write("> ") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
107 elif lineno in excluded: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
108 dest.write("- ") 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJ6KLMN7OP8QRST9UV!WXYZ#01$234
109 elif covered: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
110 dest.write("> ") 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
111 else:
112 dest.write("! ") 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234
114 dest.write(line) 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234