summaryrefslogtreecommitdiff
path: root/coverage/summary.py
diff options
context:
space:
mode:
authorstepeos <82703776+stepeos@users.noreply.github.com>2022-11-05 17:29:04 +0100
committerGitHub <noreply@github.com>2022-11-05 09:29:04 -0700
commitcf1efa814e905ab1e2bc17795b1dbe6d437b39e5 (patch)
treee7f236575db700b37b595669bd8dbe803b1b53d4 /coverage/summary.py
parent27fd4a9b8999dba408d1bc5a5df675d7caaa85b8 (diff)
downloadpython-coveragepy-git-cf1efa814e905ab1e2bc17795b1dbe6d437b39e5.tar.gz
feat: report terminal output in Markdown Table format #1418 (#1479)
* refactoring normal reporting text output * implemented markdown feature from #1418 * minor changes * fixed text output * fixed precision for text and markdown report format * minor changes * finished testing for markdown format feature * fixed testing outside test_summary.py * removed fixed-length widespace padding for tests * removed whitespaces * refactoring, fixing docs, rewriting cmd args * fixing code quality * implementing requested changes * doc fix * test: add another test of correct report formatting * fixed precision printing test * style: adjust the formatting Co-authored-by: Ned Batchelder <ned@nedbatchelder.com>
Diffstat (limited to 'coverage/summary.py')
-rw-r--r--coverage/summary.py205
1 files changed, 151 insertions, 54 deletions
diff --git a/coverage/summary.py b/coverage/summary.py
index 861fbc53..94be1a08 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -6,7 +6,7 @@
import sys
from coverage.exceptions import ConfigError, NoDataError
-from coverage.misc import human_sorted_items
+from coverage.misc import human_key
from coverage.report import get_analysis_to_report
from coverage.results import Numbers
@@ -30,6 +30,119 @@ class SummaryReporter:
self.outfile.write(line.rstrip())
self.outfile.write("\n")
+ def _report_text(self, header, lines_values, total_line, end_lines):
+ """Internal method that prints report data in text format.
+
+ `header` is a tuple with captions.
+ `lines_values` is list of tuples of sortable values.
+ `total_line` is a tuple with values of the total line.
+ `end_lines` is a tuple of ending lines with information about skipped files.
+
+ """
+ # Prepare the formatting strings, header, and column sorting.
+ max_name = max([len(line[0]) for line in lines_values] + [5]) + 1
+ max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1
+ max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values])
+ h_form = dict(
+ Name="{:{name_len}}",
+ Stmts="{:>7}",
+ Miss="{:>7}",
+ Branch="{:>7}",
+ BrPart="{:>7}",
+ Cover="{:>{n}}",
+ Missing="{:>10}",
+ )
+ header_items = [
+ h_form[item].format(item, name_len=max_name, n=max_n)
+ for item in header
+ ]
+ header_str = "".join(header_items)
+ rule = "-" * len(header_str)
+
+ # Write the header
+ self.writeout(header_str)
+ self.writeout(rule)
+
+ h_form.update(dict(Cover="{:>{n}}%"), Missing=" {:9}")
+ for values in lines_values:
+ # build string with line values
+ line_items = [
+ h_form[item].format(str(value),
+ name_len=max_name, n=max_n-1) for item, value in zip(header, values)
+ ]
+ text = "".join(line_items)
+ self.writeout(text)
+
+ # Write a TOTAL line
+ self.writeout(rule)
+ line_items = [
+ h_form[item].format(str(value),
+ name_len=max_name, n=max_n-1) for item, value in zip(header, total_line)
+ ]
+ text = "".join(line_items)
+ self.writeout(text)
+
+ for end_line in end_lines:
+ self.writeout(end_line)
+
+ def _report_markdown(self, header, lines_values, total_line, end_lines):
+ """Internal method that prints report data in markdown format.
+
+ `header` is a tuple with captions.
+ `lines_values` is a sorted list of tuples containing coverage information.
+ `total_line` is a tuple with values of the total line.
+ `end_lines` is a tuple of ending lines with information about skipped files.
+
+ """
+ # Prepare the formatting strings, header, and column sorting.
+ max_name = max([len(line[0].replace("_", "\\_")) for line in lines_values] + [9])
+ max_name += 1
+ h_form = dict(
+ Name="| {:{name_len}}|",
+ Stmts="{:>9} |",
+ Miss="{:>9} |",
+ Branch="{:>9} |",
+ BrPart="{:>9} |",
+ Cover="{:>{n}} |",
+ Missing="{:>10} |",
+ )
+ max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover "))
+ header_items = [h_form[item].format(item, name_len=max_name, n=max_n) for item in header]
+ header_str = "".join(header_items)
+ rule_str = "|" + " ".join(["- |".rjust(len(header_items[0])-1, '-')] +
+ ["-: |".rjust(len(item)-1, '-') for item in header_items[1:]]
+ )
+
+ # Write the header
+ self.writeout(header_str)
+ self.writeout(rule_str)
+
+ for values in lines_values:
+ # build string with line values
+ h_form.update(dict(Cover="{:>{n}}% |"))
+ line_items = [
+ h_form[item].format(str(value).replace("_", "\\_"),
+ name_len=max_name, n=max_n-1) for item, value in zip(header, values)
+ ]
+ text = "".join(line_items)
+ self.writeout(text)
+
+ # Write the TOTAL line
+ h_form.update(dict(Name="|{:>{name_len}} |", Cover="{:>{n}} |"))
+ total_line_items = []
+ for item, value in zip(header, total_line):
+ if value == '':
+ insert = value
+ elif item == "Cover":
+ insert = f" **{value}%**"
+ else:
+ insert = f" **{value}**"
+ total_line_items += h_form[item].format(insert, name_len=max_name, n=max_n)
+ total_row_str = "".join(total_line_items)
+ self.writeout(total_row_str)
+ for end_line in end_lines:
+ self.writeout(end_line)
+
def report(self, morfs, outfile=None):
"""Writes a report summarizing coverage statistics per module.
@@ -44,36 +157,19 @@ class SummaryReporter:
self.report_one_file(fr, analysis)
# Prepare the formatting strings, header, and column sorting.
- max_name = max([len(fr.relative_filename()) for (fr, analysis) in self.fr_analysis] + [5])
- fmt_name = "%%- %ds " % max_name
- fmt_skip_covered = "\n%s file%s skipped due to complete coverage."
- fmt_skip_empty = "\n%s empty file%s skipped."
-
- header = (fmt_name % "Name") + " Stmts Miss"
- fmt_coverage = fmt_name + "%6d %6d"
+ header = ("Name", "Stmts", "Miss",)
if self.branches:
- header += " Branch BrPart"
- fmt_coverage += " %6d %6d"
- width100 = Numbers(precision=self.config.precision).pc_str_width()
- header += "%*s" % (width100+4, "Cover")
- fmt_coverage += "%%%ds%%%%" % (width100+3,)
+ header += ("Branch", "BrPart",)
+ header += ("Cover",)
if self.config.show_missing:
- header += " Missing"
- fmt_coverage += " %s"
- rule = "-" * len(header)
+ header += ("Missing",)
column_order = dict(name=0, stmts=1, miss=2, cover=-1)
if self.branches:
column_order.update(dict(branch=3, brpart=4))
- # Write the header
- self.writeout(header)
- self.writeout(rule)
-
- # `lines` is a list of pairs, (line text, line values). The line text
- # is a string that will be printed, and line values is a tuple of
- # sortable values.
- lines = []
+ # `lines_values` is list of tuples of sortable values.
+ lines_values = []
for (fr, analysis) in self.fr_analysis:
nums = analysis.numbers
@@ -84,12 +180,10 @@ class SummaryReporter:
args += (nums.pc_covered_str,)
if self.config.show_missing:
args += (analysis.missing_formatted(branches=True),)
- text = fmt_coverage % args
- # Add numeric percent coverage so that sorting makes sense.
args += (nums.pc_covered,)
- lines.append((text, args))
+ lines_values.append(args)
- # Sort the lines and write them out.
+ # line-sorting.
sort_option = (self.config.sort or "name").lower()
reverse = False
if sort_option[0] == '-':
@@ -97,41 +191,44 @@ class SummaryReporter:
sort_option = sort_option[1:]
elif sort_option[0] == '+':
sort_option = sort_option[1:]
-
+ sort_idx = column_order.get(sort_option)
+ if sort_idx is None:
+ raise ConfigError(f"Invalid sorting option: {self.config.sort!r}")
if sort_option == "name":
- lines = human_sorted_items(lines, reverse=reverse)
+ lines_values.sort(key=lambda tup: (human_key(tup[0]), tup[1]), reverse=reverse)
else:
- position = column_order.get(sort_option)
- if position is None:
- raise ConfigError(f"Invalid sorting option: {self.config.sort!r}")
- lines.sort(key=lambda l: (l[1][position], l[0]), reverse=reverse)
-
- for line in lines:
- self.writeout(line[0])
-
- # Write a TOTAL line if we had at least one file.
- if self.total.n_files > 0:
- self.writeout(rule)
- args = ("TOTAL", self.total.n_statements, self.total.n_missing)
- if self.branches:
- args += (self.total.n_branches, self.total.n_partial_branches)
- args += (self.total.pc_covered_str,)
- if self.config.show_missing:
- args += ("",)
- self.writeout(fmt_coverage % args)
+ lines_values.sort(key=lambda tup: (tup[sort_idx], tup[0]), reverse=reverse)
+
+ # calculate total if we had at least one file.
+ total_line = ("TOTAL", self.total.n_statements, self.total.n_missing)
+ if self.branches:
+ total_line += (self.total.n_branches, self.total.n_partial_branches)
+ total_line += (self.total.pc_covered_str,)
+ if self.config.show_missing:
+ total_line += ("",)
- # Write other final lines.
+ # create other final lines
+ end_lines = []
if not self.total.n_files and not self.skipped_count:
raise NoDataError("No data to report.")
if self.config.skip_covered and self.skipped_count:
- self.writeout(
- fmt_skip_covered % (self.skipped_count, 's' if self.skipped_count > 1 else '')
+ file_suffix = 's' if self.skipped_count>1 else ''
+ fmt_skip_covered = (
+ f"\n{self.skipped_count} file{file_suffix} skipped due to complete coverage."
)
+ end_lines.append(fmt_skip_covered)
if self.config.skip_empty and self.empty_count:
- self.writeout(
- fmt_skip_empty % (self.empty_count, 's' if self.empty_count > 1 else '')
- )
+ file_suffix = 's' if self.empty_count>1 else ''
+ fmt_skip_empty = f"\n{self.empty_count} empty file{file_suffix} skipped."
+ end_lines.append(fmt_skip_empty)
+
+ text_format = self.config.output_format or "text"
+ if text_format == "markdown":
+ formatter = self._report_markdown
+ else:
+ formatter = self._report_text
+ formatter(header, lines_values, total_line, end_lines)
return self.total.n_statements and self.total.pc_covered