summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/annotate.py2
-rw-r--r--coverage/control.py4
-rw-r--r--coverage/html.py2
-rw-r--r--coverage/jsonreport.py2
-rw-r--r--coverage/lcovreport.py2
-rw-r--r--coverage/report.py364
-rw-r--r--coverage/report_core.py117
-rw-r--r--coverage/summary.py281
-rw-r--r--coverage/xmlreport.py2
9 files changed, 388 insertions, 388 deletions
diff --git a/coverage/annotate.py b/coverage/annotate.py
index b4a02cb4..2ef89c96 100644
--- a/coverage/annotate.py
+++ b/coverage/annotate.py
@@ -13,7 +13,7 @@ from typing import Iterable, Optional, TYPE_CHECKING
from coverage.files import flat_rootname
from coverage.misc import ensure_dir, isolate_module
from coverage.plugin import FileReporter
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis
from coverage.types import TMorf
diff --git a/coverage/control.py b/coverage/control.py
index e405a5bf..2a84ce71 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -43,9 +43,9 @@ from coverage.multiproc import patch_multiprocessing
from coverage.plugin import FileReporter
from coverage.plugin_support import Plugins
from coverage.python import PythonFileReporter
-from coverage.report import render_report
+from coverage.report import SummaryReporter
+from coverage.report_core import render_report
from coverage.results import Analysis
-from coverage.summary import SummaryReporter
from coverage.types import (
FilePath, TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigValueOut,
TFileDisposition, TLineNo, TMorf,
diff --git a/coverage/html.py b/coverage/html.py
index f11d85e1..532eb66c 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -23,7 +23,7 @@ from coverage.exceptions import NoDataError
from coverage.files import flat_rootname
from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime
from coverage.misc import human_sorted, plural, stdout_link
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis, Numbers
from coverage.templite import Templite
from coverage.types import TLineNo, TMorf
diff --git a/coverage/jsonreport.py b/coverage/jsonreport.py
index 24e33585..9780e261 100644
--- a/coverage/jsonreport.py
+++ b/coverage/jsonreport.py
@@ -12,7 +12,7 @@ import sys
from typing import Any, Dict, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
from coverage import __version__
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis, Numbers
from coverage.types import TMorf, TLineNo
diff --git a/coverage/lcovreport.py b/coverage/lcovreport.py
index b9fe2568..a4ad9cd9 100644
--- a/coverage/lcovreport.py
+++ b/coverage/lcovreport.py
@@ -12,7 +12,7 @@ from hashlib import md5
from typing import IO, Iterable, Optional, TYPE_CHECKING
from coverage.plugin import FileReporter
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis, Numbers
from coverage.types import TMorf
diff --git a/coverage/report.py b/coverage/report.py
index 09eed0a8..e1c7a071 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -1,117 +1,281 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-"""Reporter foundation for coverage.py."""
+"""Summary reporting"""
from __future__ import annotations
import sys
-from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING
+from typing import Any, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
-from coverage.exceptions import NoDataError, NotPython
-from coverage.files import prep_patterns, GlobMatcher
-from coverage.misc import ensure_dir_for_file, file_be_gone
+from coverage.exceptions import ConfigError, NoDataError
+from coverage.misc import human_sorted_items
from coverage.plugin import FileReporter
-from coverage.results import Analysis
-from coverage.types import Protocol, TMorf
+from coverage.report_core import get_analysis_to_report
+from coverage.results import Analysis, Numbers
+from coverage.types import TMorf
if TYPE_CHECKING:
from coverage import Coverage
-class Reporter(Protocol):
- """What we expect of reporters."""
-
- report_type: str
-
- def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
- """Generate a report of `morfs`, written to `outfile`."""
-
-
-def render_report(
- output_path: str,
- reporter: Reporter,
- morfs: Optional[Iterable[TMorf]],
- msgfn: Callable[[str], None],
-) -> float:
- """Run a one-file report generator, managing the output file.
-
- This function ensures the output file is ready to be written to. Then writes
- the report to it. Then closes the file and cleans up.
-
- """
- file_to_close = None
- delete_file = False
-
- if output_path == "-":
- outfile = sys.stdout
- else:
- # Ensure that the output directory is created; done here because this
- # report pre-opens the output file. HtmlReporter does this on its own
- # because its task is more complex, being multiple files.
- ensure_dir_for_file(output_path)
- outfile = open(output_path, "w", encoding="utf-8")
- file_to_close = outfile
- delete_file = True
-
- try:
- ret = reporter.report(morfs, outfile=outfile)
- if file_to_close is not None:
- msgfn(f"Wrote {reporter.report_type} to {output_path}")
- delete_file = False
- return ret
- finally:
- if file_to_close is not None:
- file_to_close.close()
- if delete_file:
- file_be_gone(output_path) # pragma: part covered (doesn't return)
-
-
-def get_analysis_to_report(
- coverage: Coverage,
- morfs: Optional[Iterable[TMorf]],
-) -> Iterator[Tuple[FileReporter, Analysis]]:
- """Get the files to report on.
-
- For each morf in `morfs`, if it should be reported on (based on the omit
- and include configuration options), yield a pair, the `FileReporter` and
- `Analysis` for the morf.
-
- """
- file_reporters = coverage._get_file_reporters(morfs)
- config = coverage.config
-
- if config.report_include:
- matcher = GlobMatcher(prep_patterns(config.report_include), "report_include")
- file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
-
- if config.report_omit:
- matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit")
- file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
-
- if not file_reporters:
- raise NoDataError("No data to report.")
-
- for fr in sorted(file_reporters):
- try:
- analysis = coverage._analyze(fr)
- except NotPython:
- # Only report errors for .py files, and only if we didn't
- # explicitly suppress those errors.
- # NotPython is only raised by PythonFileReporter, which has a
- # should_be_python() method.
- if fr.should_be_python(): # type: ignore[attr-defined]
- if config.ignore_errors:
- msg = f"Couldn't parse Python file '{fr.filename}'"
- coverage._warn(msg, slug="couldnt-parse")
- else:
- raise
- except Exception as exc:
- if config.ignore_errors:
- msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip()
- coverage._warn(msg, slug="couldnt-parse")
+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: Optional[IO[str]] = 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:
- raise
+ 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: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = 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]), # type: ignore[index]
+ 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:
- yield (fr, analysis)
+ self.fr_analysis.append((fr, analysis))
diff --git a/coverage/report_core.py b/coverage/report_core.py
new file mode 100644
index 00000000..09eed0a8
--- /dev/null
+++ b/coverage/report_core.py
@@ -0,0 +1,117 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Reporter foundation for coverage.py."""
+
+from __future__ import annotations
+
+import sys
+
+from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING
+
+from coverage.exceptions import NoDataError, NotPython
+from coverage.files import prep_patterns, GlobMatcher
+from coverage.misc import ensure_dir_for_file, file_be_gone
+from coverage.plugin import FileReporter
+from coverage.results import Analysis
+from coverage.types import Protocol, TMorf
+
+if TYPE_CHECKING:
+ from coverage import Coverage
+
+
+class Reporter(Protocol):
+ """What we expect of reporters."""
+
+ report_type: str
+
+ def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
+ """Generate a report of `morfs`, written to `outfile`."""
+
+
+def render_report(
+ output_path: str,
+ reporter: Reporter,
+ morfs: Optional[Iterable[TMorf]],
+ msgfn: Callable[[str], None],
+) -> float:
+ """Run a one-file report generator, managing the output file.
+
+ This function ensures the output file is ready to be written to. Then writes
+ the report to it. Then closes the file and cleans up.
+
+ """
+ file_to_close = None
+ delete_file = False
+
+ if output_path == "-":
+ outfile = sys.stdout
+ else:
+ # Ensure that the output directory is created; done here because this
+ # report pre-opens the output file. HtmlReporter does this on its own
+ # because its task is more complex, being multiple files.
+ ensure_dir_for_file(output_path)
+ outfile = open(output_path, "w", encoding="utf-8")
+ file_to_close = outfile
+ delete_file = True
+
+ try:
+ ret = reporter.report(morfs, outfile=outfile)
+ if file_to_close is not None:
+ msgfn(f"Wrote {reporter.report_type} to {output_path}")
+ delete_file = False
+ return ret
+ finally:
+ if file_to_close is not None:
+ file_to_close.close()
+ if delete_file:
+ file_be_gone(output_path) # pragma: part covered (doesn't return)
+
+
+def get_analysis_to_report(
+ coverage: Coverage,
+ morfs: Optional[Iterable[TMorf]],
+) -> Iterator[Tuple[FileReporter, Analysis]]:
+ """Get the files to report on.
+
+ For each morf in `morfs`, if it should be reported on (based on the omit
+ and include configuration options), yield a pair, the `FileReporter` and
+ `Analysis` for the morf.
+
+ """
+ file_reporters = coverage._get_file_reporters(morfs)
+ config = coverage.config
+
+ if config.report_include:
+ matcher = GlobMatcher(prep_patterns(config.report_include), "report_include")
+ file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
+
+ if config.report_omit:
+ matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit")
+ file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
+
+ if not file_reporters:
+ raise NoDataError("No data to report.")
+
+ for fr in sorted(file_reporters):
+ try:
+ analysis = coverage._analyze(fr)
+ except NotPython:
+ # Only report errors for .py files, and only if we didn't
+ # explicitly suppress those errors.
+ # NotPython is only raised by PythonFileReporter, which has a
+ # should_be_python() method.
+ if fr.should_be_python(): # type: ignore[attr-defined]
+ if config.ignore_errors:
+ msg = f"Couldn't parse Python file '{fr.filename}'"
+ coverage._warn(msg, slug="couldnt-parse")
+ else:
+ raise
+ except Exception as exc:
+ if config.ignore_errors:
+ msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip()
+ coverage._warn(msg, slug="couldnt-parse")
+ else:
+ raise
+ else:
+ yield (fr, analysis)
diff --git a/coverage/summary.py b/coverage/summary.py
deleted file mode 100644
index 5d373ec5..00000000
--- a/coverage/summary.py
+++ /dev/null
@@ -1,281 +0,0 @@
-# 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, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
-
-from coverage.exceptions import ConfigError, NoDataError
-from coverage.misc import human_sorted_items
-from coverage.plugin import FileReporter
-from coverage.report import get_analysis_to_report
-from coverage.results import Analysis, 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: Optional[IO[str]] = 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: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = 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]), # type: ignore[index]
- 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))
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index 82e60fc1..cac4c977 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -17,7 +17,7 @@ from typing import Any, Dict, IO, Iterable, Optional, TYPE_CHECKING, cast
from coverage import __version__, files
from coverage.misc import isolate_module, human_sorted, human_sorted_items
from coverage.plugin import FileReporter
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis
from coverage.types import TMorf
from coverage.version import __url__