summaryrefslogtreecommitdiff
path: root/coverage
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
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')
-rw-r--r--coverage/cmdline.py7
-rw-r--r--coverage/config.py2
-rw-r--r--coverage/control.py11
-rw-r--r--coverage/summary.py205
4 files changed, 169 insertions, 56 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 65ee73f8..89b0807d 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -96,6 +96,10 @@ class Opts:
'', '--fail-under', action='store', metavar="MIN", type="float",
help="Exit with a status of 2 if the total coverage is less than MIN.",
)
+ output_format = optparse.make_option(
+ '', '--format', action='store', metavar="FORMAT", dest="output_format",
+ help="Output format, either text (default) or markdown",
+ )
help = optparse.make_option(
'-h', '--help', action='store_true',
help="Get help on this command.",
@@ -245,6 +249,7 @@ class CoverageOptionParser(optparse.OptionParser):
debug=None,
directory=None,
fail_under=None,
+ output_format=None,
help=None,
ignore_errors=None,
include=None,
@@ -482,6 +487,7 @@ COMMANDS = {
Opts.contexts,
Opts.input_datafile,
Opts.fail_under,
+ Opts.output_format,
Opts.ignore_errors,
Opts.include,
Opts.omit,
@@ -689,6 +695,7 @@ class CoverageScript:
skip_covered=options.skip_covered,
skip_empty=options.skip_empty,
sort=options.sort,
+ output_format=options.output_format,
**report_args
)
elif options.action == "annotate":
diff --git a/coverage/config.py b/coverage/config.py
index c2375d03..1f239ea3 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -199,6 +199,7 @@ class CoverageConfig:
# Defaults for [report]
self.exclude_list = DEFAULT_EXCLUDE[:]
self.fail_under = 0.0
+ self.output_format = None
self.ignore_errors = False
self.report_include = None
self.report_omit = None
@@ -374,6 +375,7 @@ class CoverageConfig:
# [report]
('exclude_list', 'report:exclude_lines', 'regexlist'),
('fail_under', 'report:fail_under', 'float'),
+ ('output_format', 'report:output_format', 'boolean'),
('ignore_errors', 'report:ignore_errors', 'boolean'),
('partial_always_list', 'report:partial_branches_always', 'regexlist'),
('partial_list', 'report:partial_branches', 'regexlist'),
diff --git a/coverage/control.py b/coverage/control.py
index 91e604e0..a8cf1649 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -908,7 +908,8 @@ class Coverage:
def report(
self, morfs=None, show_missing=None, ignore_errors=None,
file=None, omit=None, include=None, skip_covered=None,
- contexts=None, skip_empty=None, precision=None, sort=None
+ contexts=None, skip_empty=None, precision=None, sort=None,
+ output_format=None,
):
"""Write a textual summary report to `file`.
@@ -922,6 +923,9 @@ class Coverage:
`file` is a file-like object, suitable for writing.
+ `output_format` provides options, to print eitehr as plain text, or as
+ markdown code
+
`include` is a list of file name patterns. Files that match will be
included in the report. Files matching `omit` will not be included in
the report.
@@ -953,13 +957,16 @@ class Coverage:
.. versionadded:: 5.2
The `precision` parameter.
+ .. versionadded:: 6.6
+ The `format` parameter.
+
"""
with override_config(
self,
ignore_errors=ignore_errors, report_omit=omit, report_include=include,
show_missing=show_missing, skip_covered=skip_covered,
report_contexts=contexts, skip_empty=skip_empty, precision=precision,
- sort=sort
+ sort=sort, output_format=output_format,
):
reporter = SummaryReporter(self)
return reporter.report(morfs, outfile=file)
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