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

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"""Source file annotation for coverage.py.""" 

5 

6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234

7 

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

12 

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

19 

20if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234

21 from coverage import Coverage 

22 

23os = isolate_module(os) 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234

24 

25 

26class AnnotateReporter: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234

27 """Generate annotated source files showing line coverage. 

28 

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:: 

32 

33 > def h(x): 

34 - if 0: #pragma: no cover 

35 - pass 

36 > if x == 1: 

37 ! a = 1 

38 > else: 

39 > a = 2 

40 

41 > h(2) 

42 

43 Executed lines use ">", lines not executed use "!", lines excluded from 

44 consideration use "-". 

45 

46 """ 

47 

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

52 

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

55 

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. 

58 

59 See `coverage.report()` for arguments. 

60 

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

66 

67 def annotate_file(self, fr: FileReporter, analysis: Analysis) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234

68 """Annotate a single file. 

69 

70 `fr` is the FileReporter for the file to annotate. 

71 

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

76 

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

85 

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

113 

114 dest.write(line) 1abcdefghijklmnopqrstuvwxyzABCDEF%GH5IJ6KL'MN7OP8QR(ST9UV!WX)YZ#01$234