pre-commit-hooks/.venv/lib/python3.10/site-packages/coverage/report.py
2024-04-13 00:00:20 +00:00

288 lines
10 KiB
Python

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
"""Summary reporting"""
from __future__ import annotations
import sys
from typing import Any
from typing import IO
from typing import Iterable
from typing import TYPE_CHECKING
from coverage.exceptions import ConfigError
from coverage.exceptions import NoDataError
from coverage.misc import human_sorted_items
from coverage.plugin import FileReporter
from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis
from coverage.results import Numbers
from coverage.types import TMorf
if TYPE_CHECKING:
from coverage import Coverage
class SummaryReporter:
"""A reporter for writing the summary report."""
def __init__(self, coverage: Coverage) -> None:
self.coverage = coverage
self.config = self.coverage.config
self.branches = coverage.get_data().has_arcs()
self.outfile: IO[str] | None = None
self.output_format = self.config.format or 'text'
if self.output_format not in {'text', 'markdown', 'total'}:
raise ConfigError(f'Unknown report format choice: {self.output_format!r}')
self.fr_analysis: list[tuple[FileReporter, Analysis]] = []
self.skipped_count = 0
self.empty_count = 0
self.total = Numbers(precision=self.config.precision)
def write(self, line: str) -> None:
"""Write a line to the output, adding a newline."""
assert self.outfile is not None
self.outfile.write(line.rstrip())
self.outfile.write('\n')
def write_items(self, items: Iterable[str]) -> None:
"""Write a list of strings, joined together."""
self.write(''.join(items))
def _report_text(
self,
header: list[str],
lines_values: list[list[Any]],
total_line: list[Any],
end_lines: list[str],
) -> None:
"""Internal method that prints report data in text format.
`header` is a list with captions.
`lines_values` is list of lists of sortable values.
`total_line` is a list with values of the total line.
`end_lines` is a list 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])
formats = dict(
Name='{:{name_len}}',
Stmts='{:>7}',
Miss='{:>7}',
Branch='{:>7}',
BrPart='{:>7}',
Cover='{:>{n}}',
Missing='{:>10}',
)
header_items = [
formats[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.write(header_str)
self.write(rule)
formats.update(dict(Cover='{:>{n}}%'), Missing=' {:9}')
for values in lines_values:
# build string with line values
line_items = [
formats[item].format(
str(value),
name_len=max_name, n=max_n - 1,
) for item, value in zip(header, values)
]
self.write_items(line_items)
# Write a TOTAL line
if lines_values:
self.write(rule)
line_items = [
formats[item].format(
str(value),
name_len=max_name, n=max_n - 1,
) for item, value in zip(header, total_line)
]
self.write_items(line_items)
for end_line in end_lines:
self.write(end_line)
def _report_markdown(
self,
header: list[str],
lines_values: list[list[Any]],
total_line: list[Any],
end_lines: list[str],
) -> None:
"""Internal method that prints report data in markdown format.
`header` is a list with captions.
`lines_values` is a sorted list of lists containing coverage information.
`total_line` is a list with values of the total line.
`end_lines` is a list 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), default=0)
max_name = max(max_name, len('**TOTAL**')) + 1
formats = 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 = [formats[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.write(header_str)
self.write(rule_str)
for values in lines_values:
# build string with line values
formats.update(dict(Cover='{:>{n}}% |'))
line_items = [
formats[item].format(str(value).replace('_', '\\_'), name_len=max_name, n=max_n - 1)
for item, value in zip(header, values)
]
self.write_items(line_items)
# Write the TOTAL line
formats.update(dict(Name='|{:>{name_len}} |', Cover='{:>{n}} |'))
total_line_items: list[str] = []
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 += formats[item].format(insert, name_len=max_name, n=max_n)
self.write_items(total_line_items)
for end_line in end_lines:
self.write(end_line)
def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str] | None = None) -> float:
"""Writes a report summarizing coverage statistics per module.
`outfile` is a text-mode file object to write the summary to.
"""
self.outfile = outfile or sys.stdout
self.coverage.get_data().set_query_contexts(self.config.report_contexts)
for fr, analysis in get_analysis_to_report(self.coverage, morfs):
self.report_one_file(fr, analysis)
if not self.total.n_files and not self.skipped_count:
raise NoDataError('No data to report.')
if self.output_format == 'total':
self.write(self.total.pc_covered_str)
else:
self.tabular_report()
return self.total.pc_covered
def tabular_report(self) -> None:
"""Writes tabular report formats."""
# Prepare the header line and column sorting.
header = ['Name', 'Stmts', 'Miss']
if self.branches:
header += ['Branch', 'BrPart']
header += ['Cover']
if self.config.show_missing:
header += ['Missing']
column_order = dict(name=0, stmts=1, miss=2, cover=-1)
if self.branches:
column_order.update(dict(branch=3, brpart=4))
# `lines_values` is list of lists of sortable values.
lines_values = []
for (fr, analysis) in self.fr_analysis:
nums = analysis.numbers
args = [fr.relative_filename(), nums.n_statements, nums.n_missing]
if self.branches:
args += [nums.n_branches, nums.n_partial_branches]
args += [nums.pc_covered_str]
if self.config.show_missing:
args += [analysis.missing_formatted(branches=True)]
args += [nums.pc_covered]
lines_values.append(args)
# Line sorting.
sort_option = (self.config.sort or 'name').lower()
reverse = False
if sort_option[0] == '-':
reverse = True
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_values = human_sorted_items(lines_values, reverse=reverse)
else:
lines_values.sort(
key=lambda line: (line[sort_idx], line[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 += ['']
# Create other final lines.
end_lines = []
if self.config.skip_covered and self.skipped_count:
file_suffix = 's' if self.skipped_count > 1 else ''
end_lines.append(
f'\n{self.skipped_count} file{file_suffix} skipped due to complete coverage.',
)
if self.config.skip_empty and self.empty_count:
file_suffix = 's' if self.empty_count > 1 else ''
end_lines.append(f'\n{self.empty_count} empty file{file_suffix} skipped.')
if self.output_format == 'markdown':
formatter = self._report_markdown
else:
formatter = self._report_text
formatter(header, lines_values, total_line, end_lines)
def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None:
"""Report on just one file, the callback from report()."""
nums = analysis.numbers
self.total += nums
no_missing_lines = (nums.n_missing == 0)
no_missing_branches = (nums.n_partial_branches == 0)
if self.config.skip_covered and no_missing_lines and no_missing_branches:
# Don't report on 100% files.
self.skipped_count += 1
elif self.config.skip_empty and nums.n_statements == 0:
# Don't report on empty files.
self.empty_count += 1
else:
self.fr_analysis.append((fr, analysis))