Coverage for coverage / report.py: 100.000%

144 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"""Summary reporting""" 

5 

6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

7 

8import sys 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

9from collections.abc import Iterable 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

10from typing import IO, TYPE_CHECKING, Any 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

11 

12from coverage.exceptions import ConfigError, NoDataError 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

13from coverage.misc import human_sorted_items, plural 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

14from coverage.plugin import FileReporter 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

15from coverage.report_core import get_analysis_to_report 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

16from coverage.results import Analysis, Numbers 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

17from coverage.types import TMorf 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

18 

19if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

20 from coverage import Coverage 

21 

22 

23class SummaryReporter: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

24 """A reporter for writing the summary report.""" 

25 

26 def __init__(self, coverage: Coverage) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

27 self.coverage = coverage 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

28 self.config = self.coverage.config 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

29 self.branches = coverage.get_data().has_arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

30 self.outfile: IO[str] | None = None 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

31 self.output_format = self.config.format or "text" 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

32 if self.output_format not in {"text", "markdown", "total"}: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

33 raise ConfigError(f"Unknown report format choice: {self.output_format!r}") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

34 self.fr_analyses: list[tuple[FileReporter, Analysis]] = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

35 self.skipped_count = 0 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

36 self.empty_count = 0 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

37 self.total = Numbers(precision=self.config.precision) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

38 

39 def write(self, line: str) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

40 """Write a line to the output, adding a newline.""" 

41 assert self.outfile is not None 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

42 self.outfile.write(line.rstrip()) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

43 self.outfile.write("\n") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

44 

45 def write_items(self, items: Iterable[str]) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

46 """Write a list of strings, joined together.""" 

47 self.write("".join(items)) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

48 

49 def report_text( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

50 self, 

51 header: list[str], 

52 lines_values: list[list[Any]], 

53 total_line: list[Any], 

54 end_lines: list[str], 

55 ) -> None: 

56 """Internal method that prints report data in text format. 

57 

58 `header` is a list with captions. 

59 `lines_values` is list of lists of sortable values. 

60 `total_line` is a list with values of the total line. 

61 `end_lines` is a list of ending lines with information about skipped files. 

62 

63 """ 

64 # Prepare the formatting strings, header, and column sorting. 

65 max_name = max([len(line[0]) for line in lines_values] + [5]) + 1 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

66 max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

67 max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values]) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

68 formats = dict( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

69 Name="{:{name_len}}", 

70 Stmts="{:>7}", 

71 Miss="{:>7}", 

72 Branch="{:>7}", 

73 BrPart="{:>7}", 

74 Cover="{:>{n}}", 

75 Missing="{:>10}", 

76 ) 

77 header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

78 header_str = "".join(header_items) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

79 rule = "-" * len(header_str) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

80 

81 # Write the header 

82 self.write(header_str) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

83 self.write(rule) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

84 

85 # Write the data lines 

86 formats.update( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

87 dict( 

88 Cover="{:>{n}}%", 

89 Missing=" {:9}", 

90 ) 

91 ) 

92 for values in lines_values: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

93 self.write_items( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

94 ( 

95 formats[item].format(str(value), name_len=max_name, n=max_n - 1) 

96 for item, value in zip(header, values) 

97 ) 

98 ) 

99 

100 # Write a TOTAL line 

101 if lines_values: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

102 self.write(rule) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

103 

104 self.write_items( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

105 ( 

106 formats[item].format(str(value), name_len=max_name, n=max_n - 1) 

107 for item, value in zip(header, total_line) 

108 ) 

109 ) 

110 

111 for end_line in end_lines: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

112 self.write(end_line) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

113 

114 def report_markdown( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

115 self, 

116 header: list[str], 

117 lines_values: list[list[Any]], 

118 total_line: list[Any], 

119 end_lines: list[str], 

120 ) -> None: 

121 """Internal method that prints report data in markdown format. 

122 

123 `header` is a list with captions. 

124 `lines_values` is a sorted list of lists containing coverage information. 

125 `total_line` is a list with values of the total line. 

126 `end_lines` is a list of ending lines with information about skipped files. 

127 

128 """ 

129 # Prepare the formatting strings, header, and column sorting. 

130 max_name = max((len(line[0].replace("_", "\\_")) for line in lines_values), default=0) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

131 max_name = max(max_name, len("**TOTAL**")) + 1 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

132 formats = dict( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

133 Name="| {:{name_len}}|", 

134 Stmts="{:>9} |", 

135 Miss="{:>9} |", 

136 Branch="{:>9} |", 

137 BrPart="{:>9} |", 

138 Cover="{:>{n}} |", 

139 Missing="{:>10} |", 

140 ) 

141 max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover ")) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

142 header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

143 header_str = "".join(header_items) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

144 rule_str = "|" + " ".join( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

145 ["- |".rjust(len(header_items[0]) - 1, "-")] 

146 + ["-: |".rjust(len(item) - 1, "-") for item in header_items[1:]], 

147 ) 

148 

149 # Write the header 

150 self.write(header_str) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

151 self.write(rule_str) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

152 

153 # Write the data lines 

154 for values in lines_values: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

155 formats.update( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

156 dict( 

157 Cover="{:>{n}}% |", 

158 ) 

159 ) 

160 self.write_items( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

161 ( 

162 formats[item].format( 

163 str(value).replace("_", "\\_"), name_len=max_name, n=max_n - 1 

164 ) 

165 for item, value in zip(header, values) 

166 ) 

167 ) 

168 

169 # Write the TOTAL line 

170 formats.update( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

171 dict( 

172 Name="|{:{name_len}} |", 

173 Cover="{:>{n}} |", 

174 ), 

175 ) 

176 total_line_items: list[str] = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

177 for item, value in zip(header, total_line): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

178 if value == "": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

179 insert = value 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST5UVWXYZ01234

180 elif item == "Cover": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

181 insert = f" **{value}%**" 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

182 else: 

183 insert = f" **{value}**" 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

184 total_line_items += formats[item].format(insert, name_len=max_name, n=max_n) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

185 self.write_items(total_line_items) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

186 

187 for end_line in end_lines: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

188 self.write(end_line) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

189 

190 def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str] | None = None) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

191 """Writes a report summarizing coverage statistics per module. 

192 

193 `outfile` is a text-mode file object to write the summary to. 

194 

195 """ 

196 self.outfile = outfile or sys.stdout 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

197 

198 self.coverage.get_data().set_query_contexts(self.config.report_contexts) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

199 for fr, analysis in get_analysis_to_report(self.coverage, morfs): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

200 self.report_one_file(fr, analysis) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

201 

202 if not self.total.n_files and not self.skipped_count: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

203 raise NoDataError("No data to report.") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

204 

205 if self.output_format == "total": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

206 self.write(self.total.pc_covered_str) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

207 else: 

208 self.tabular_report() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

209 

210 return self.total.pc_covered 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

211 

212 def tabular_report(self) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

213 """Writes tabular report formats.""" 

214 # Prepare the header line and column sorting. 

215 header = ["Name", "Stmts", "Miss"] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

216 if self.branches: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

217 header += ["Branch", "BrPart"] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

218 header += ["Cover"] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

219 if self.config.show_missing: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

220 header += ["Missing"] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

221 

222 column_order = dict(name=0, stmts=1, miss=2, cover=-1) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

223 if self.branches: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

224 column_order.update(dict(branch=3, brpart=4)) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

225 

226 # `lines_values` is list of lists of sortable values. 

227 lines_values = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

228 

229 for fr, analysis in self.fr_analyses: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

230 nums = analysis.numbers 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

231 args = [fr.relative_filename(), nums.n_statements, nums.n_missing] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

232 if self.branches: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

233 args += [nums.n_branches, nums.n_partial_branches] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

234 args += [nums.pc_covered_str] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

235 if self.config.show_missing: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

236 args += [analysis.missing_formatted(branches=True)] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

237 args += [nums.pc_covered] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

238 lines_values.append(args) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

239 

240 # Line sorting. 

241 sort_option = (self.config.sort or "name").lower() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

242 reverse = False 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

243 if sort_option[0] == "-": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

244 reverse = True 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

245 sort_option = sort_option[1:] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

246 elif sort_option[0] == "+": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

247 sort_option = sort_option[1:] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

248 sort_idx = column_order.get(sort_option) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

249 if sort_idx is None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

250 raise ConfigError(f"Invalid sorting option: {self.config.sort!r}") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

251 if sort_option == "name": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

252 lines_values = human_sorted_items(lines_values, reverse=reverse) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

253 else: 

254 lines_values.sort( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

255 key=lambda line: (line[sort_idx], line[0]), 

256 reverse=reverse, 

257 ) 

258 

259 # Calculate total if we had at least one file. 

260 total_line = ["TOTAL", self.total.n_statements, self.total.n_missing] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

261 if self.branches: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

262 total_line += [self.total.n_branches, self.total.n_partial_branches] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

263 total_line += [self.total.pc_covered_str] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

264 if self.config.show_missing: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

265 total_line += [""] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

266 

267 # Create other final lines. 

268 end_lines = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

269 if self.config.skip_covered and self.skipped_count: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

270 files = plural(self.skipped_count, "file") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

271 end_lines.append( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

272 f"\n{self.skipped_count} {files} skipped due to complete coverage.", 

273 ) 

274 if self.config.skip_empty and self.empty_count: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

275 files = plural(self.empty_count, "file") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

276 end_lines.append(f"\n{self.empty_count} empty {files} skipped.") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

277 

278 if self.output_format == "markdown": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

279 formatter = self.report_markdown 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

280 else: 

281 formatter = self.report_text 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

282 formatter(header, lines_values, total_line, end_lines) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

283 

284 def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

285 """Report on just one file, the callback from report().""" 

286 nums = analysis.numbers 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

287 self.total += nums 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

288 

289 no_missing_lines = (nums.n_missing == 0) # fmt: skip 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

290 no_missing_branches = (nums.n_partial_branches == 0) # fmt: skip 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

291 if self.config.skip_covered and no_missing_lines and no_missing_branches: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

292 # Don't report on 100% files. 

293 self.skipped_count += 1 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

294 elif self.config.skip_empty and nums.n_statements == 0: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234

295 # Don't report on empty files. 

296 self.empty_count += 1 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

297 else: 

298 self.fr_analyses.append((fr, analysis)) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN!OP#QR$ST5UV%WX'YZ(01)234