diff options
author | stepeos <82703776+stepeos@users.noreply.github.com> | 2022-11-05 17:29:04 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-05 09:29:04 -0700 |
commit | cf1efa814e905ab1e2bc17795b1dbe6d437b39e5 (patch) | |
tree | e7f236575db700b37b595669bd8dbe803b1b53d4 /coverage/summary.py | |
parent | 27fd4a9b8999dba408d1bc5a5df675d7caaa85b8 (diff) | |
download | python-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.py | 205 |
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 |