summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2019-06-16 15:38:26 -0400
committerNed Batchelder <ned@nedbatchelder.com>2019-06-16 15:38:26 -0400
commit3a04a88e88a92882ef92cf6778e42eaf3d4001f4 (patch)
treed3cad2010fc7726cd0185b710d3792a183962f1a
parent591ac5b27974e10386559f54b7c5a81e69e5210d (diff)
downloadpython-coveragepy-git-3a04a88e88a92882ef92cf6778e42eaf3d4001f4.tar.gz
Refactor text reporting to use the same code paths as other reports
-rw-r--r--CHANGES.rst6
-rw-r--r--coverage/cmdline.py7
-rw-r--r--coverage/report.py3
-rw-r--r--coverage/summary.py110
-rw-r--r--doc/cmd.rst5
-rw-r--r--tests/test_summary.py25
6 files changed, 78 insertions, 78 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index da596a97..6b9bdede 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -32,6 +32,12 @@ Unreleased
information to each covered line. Hovering over the "ctx" marker at the
end of the line reveals a list of the contexts that covered the line.
+- Error handling during reporting has changed slightly. All reporting methods
+ now behave the same. The ``--ignore-errors`` option keeps errors from
+ stopping the reporting, but files that couldn't parse as Python will always
+ be reported as warnings. As with other warnings, you can suppress them with
+ the ``[run] disable_warnings`` configuration setting.
+
.. _changes_50a5:
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index a6a72c32..c3d011d9 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -20,7 +20,7 @@ from coverage.collector import CTracer
from coverage.data import line_counts
from coverage.debug import info_formatter, info_header
from coverage.execfile import PyRunner
-from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource
+from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource, output_encoding
from coverage.results import should_fail_under
@@ -788,7 +788,10 @@ def main(argv=None):
status = ERR
except BaseCoverageException as err:
# A controlled error inside coverage.py: print the message to the user.
- print(err)
+ msg = err.args[0]
+ if env.PY2:
+ msg = msg.encode(output_encoding())
+ print(msg)
status = ERR
except SystemExit as err:
# The user called `sys.exit()`. Exit with their argument, if any.
diff --git a/coverage/report.py b/coverage/report.py
index 4f81d99c..2675cf0f 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -81,6 +81,7 @@ class Reporter(object):
# should_be_python() method.
if fr.should_be_python():
if self.config.ignore_errors:
- self.coverage._warn("Could not parse Python file {0}".format(fr.filename))
+ msg = "Could not parse Python file {0}".format(fr.filename)
+ self.coverage._warn(msg, slug="couldnt-parse")
else:
raise
diff --git a/coverage/summary.py b/coverage/summary.py
index 9e0ccbfd..5b197f60 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -8,7 +8,7 @@ import sys
from coverage import env
from coverage.report import Reporter
from coverage.results import Numbers
-from coverage.misc import NotPython, CoverageException, output_encoding, StopEverything
+from coverage.misc import NotPython, CoverageException, output_encoding
class SummaryReporter(Reporter):
@@ -16,8 +16,19 @@ class SummaryReporter(Reporter):
def __init__(self, coverage, config):
super(SummaryReporter, self).__init__(coverage, config)
- data = coverage.get_data()
- self.branches = data.has_arcs()
+ self.branches = coverage.get_data().has_arcs()
+ self.outfile = None
+ self.fr_analysis = []
+ self.skipped_count = 0
+ self.total = Numbers()
+ self.fmt_err = u"%s %s: %s"
+
+ def writeout(self, line):
+ """Write a line to the output, adding a newline."""
+ if env.PY2:
+ line = line.encode(output_encoding())
+ self.outfile.write(line.rstrip())
+ self.outfile.write("\n")
def report(self, morfs, outfile=None):
"""Writes a report summarizing coverage statistics per module.
@@ -26,53 +37,13 @@ class SummaryReporter(Reporter):
for native strings (bytes on Python 2, Unicode on Python 3).
"""
- if outfile is None:
- outfile = sys.stdout
-
- def writeout(line):
- """Write a line to the output, adding a newline."""
- if env.PY2:
- line = line.encode(output_encoding())
- outfile.write(line.rstrip())
- outfile.write("\n")
-
- fr_analysis = []
- skipped_count = 0
- total = Numbers()
-
- fmt_err = u"%s %s: %s"
+ self.outfile = outfile or sys.stdout
self.coverage.get_data().set_query_contexts(self.config.query_contexts)
- for fr in self.find_file_reporters(morfs):
- try:
- analysis = self.coverage._analyze(fr)
- nums = analysis.numbers
- total += nums
-
- if self.config.skip_covered:
- # Don't report on 100% files.
- no_missing_lines = (nums.n_missing == 0)
- no_missing_branches = (nums.n_partial_branches == 0)
- if no_missing_lines and no_missing_branches:
- skipped_count += 1
- continue
- fr_analysis.append((fr, analysis))
- except StopEverything:
- # Don't report this on single files, it's a systemic problem.
- raise
- except Exception:
- report_it = not self.config.ignore_errors
- if report_it:
- typ, msg = sys.exc_info()[:2]
- # NotPython is only raised by PythonFileReporter, which has a
- # should_be_python() method.
- if issubclass(typ, NotPython) and not fr.should_be_python():
- report_it = False
- if report_it:
- writeout(fmt_err % (fr.relative_filename(), typ.__name__, msg))
+ self.report_files(self.report_one_file, morfs)
# Prepare the formatting strings, header, and column sorting.
- max_name = max([len(fr.relative_filename()) for (fr, analysis) in fr_analysis] + [5])
+ max_name = max([len(fr.relative_filename()) for (fr, analysis) in self.fr_analysis] + [5])
fmt_name = u"%%- %ds " % max_name
fmt_skip_covered = u"\n%s file%s skipped due to complete coverage."
@@ -94,15 +65,15 @@ class SummaryReporter(Reporter):
column_order.update(dict(branch=3, brpart=4))
# Write the header
- writeout(header)
- writeout(rule)
+ 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 = []
- for (fr, analysis) in fr_analysis:
+ for (fr, analysis) in self.fr_analysis:
try:
nums = analysis.numbers
@@ -125,7 +96,7 @@ class SummaryReporter(Reporter):
if typ is NotPython and not fr.should_be_python():
report_it = False
if report_it:
- writeout(fmt_err % (fr.relative_filename(), typ.__name__, msg))
+ self.writeout(self.fmt_err % (fr.relative_filename(), typ.__name__, msg))
# Sort the lines and write them out.
if getattr(self.config, 'sort', None):
@@ -135,24 +106,39 @@ class SummaryReporter(Reporter):
lines.sort(key=lambda l: (l[1][position], l[0]))
for line in lines:
- writeout(line[0])
+ self.writeout(line[0])
# Write a TOTAl line if we had more than one file.
- if total.n_files > 1:
- writeout(rule)
- args = ("TOTAL", total.n_statements, total.n_missing)
+ if self.total.n_files > 1:
+ self.writeout(rule)
+ args = ("TOTAL", self.total.n_statements, self.total.n_missing)
if self.branches:
- args += (total.n_branches, total.n_partial_branches)
- args += (total.pc_covered_str,)
+ args += (self.total.n_branches, self.total.n_partial_branches)
+ args += (self.total.pc_covered_str,)
if self.config.show_missing:
args += ("",)
- writeout(fmt_coverage % args)
+ self.writeout(fmt_coverage % args)
# Write other final lines.
- if not total.n_files and not skipped_count:
+ if not self.total.n_files and not self.skipped_count:
raise CoverageException("No data to report.")
- if self.config.skip_covered and skipped_count:
- writeout(fmt_skip_covered % (skipped_count, 's' if skipped_count > 1 else ''))
-
- return total.n_statements and total.pc_covered
+ if self.config.skip_covered and self.skipped_count:
+ self.writeout(
+ fmt_skip_covered % (self.skipped_count, 's' if self.skipped_count > 1 else '')
+ )
+
+ return self.total.n_statements and self.total.pc_covered
+
+ def report_one_file(self, fr, analysis):
+ """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
+ else:
+ self.fr_analysis.append((fr, analysis))
diff --git a/doc/cmd.rst b/doc/cmd.rst
index c6d5fe27..42738493 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -143,6 +143,11 @@ Warnings
During execution, coverage.py may warn you about conditions it detects that
could affect the measurement process. The possible warnings include:
+* "Could not parse Python file XXX (couldnt-parse)"
+
+ During reporting, a file was thought to be Python, but it couldn't be parsed
+ as Python.
+
* "Trace function changed, measurement is likely wrong: XXX (trace-changed)"
Coverage measurement depends on a Python setting called the trace function.
diff --git a/tests/test_summary.py b/tests/test_summary.py
index eed77dba..f7824ce4 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -426,13 +426,11 @@ class SummaryTest(UsingModulesMixin, CoverageTest):
def test_report_skip_covered_no_data(self):
report = self.report_from_command("coverage report --skip-covered")
- # Name Stmts Miss Branch BrPart Cover
- # -------------------------------------------
# No data to report.
- self.assertEqual(self.line_count(report), 3, report)
+ self.assertEqual(self.line_count(report), 1, report)
squeezed = self.squeezed_lines(report)
- self.assertEqual(squeezed[2], "No data to report.")
+ self.assertEqual(squeezed[0], "No data to report.")
def test_report_precision(self):
self.make_file(".coveragerc", """\
@@ -487,7 +485,7 @@ class SummaryTest(UsingModulesMixin, CoverageTest):
self.make_file("mycode.py", "This isn't python at all!")
report = self.report_from_command("coverage report mycode.py")
- # mycode NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1
+ # Couldn't parse '...' as Python source: 'invalid syntax' at line 1
# Name Stmts Miss Cover
# ----------------------------
# No data to report.
@@ -498,8 +496,8 @@ class SummaryTest(UsingModulesMixin, CoverageTest):
# The actual error message varies version to version
errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg)
self.assertEqual(
+ "Couldn't parse 'mycode.py' as Python source: 'error' at line 1",
errmsg,
- "mycode.py NotPython: Couldn't parse 'mycode.py' as Python source: 'error' at line 1"
)
def test_accenteddotpy_not_python(self):
@@ -514,7 +512,7 @@ class SummaryTest(UsingModulesMixin, CoverageTest):
self.make_file(u"accented\xe2.py", "This isn't python at all!")
report = self.report_from_command(u"coverage report accented\xe2.py")
- # xxxx NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1
+ # Couldn't parse '...' as Python source: 'invalid syntax' at line 1
# Name Stmts Miss Cover
# ----------------------------
# No data to report.
@@ -524,27 +522,28 @@ class SummaryTest(UsingModulesMixin, CoverageTest):
errmsg = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", errmsg)
# The actual error message varies version to version
errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg)
- expected = (
- u"accented\xe2.py NotPython: "
- u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1"
- )
+ expected = u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1"
if env.PY2:
expected = expected.encode(output_encoding())
self.assertEqual(expected, errmsg)
def test_dotpy_not_python_ignored(self):
# We run a .py file, and when reporting, we can't parse it as Python,
- # but we've said to ignore errors, so there's no error reported.
+ # but we've said to ignore errors, so there's no error reported,
+ # though we still get a warning.
self.make_mycode()
self.run_command("coverage run mycode.py")
self.make_file("mycode.py", "This isn't python at all!")
report = self.report_from_command("coverage report -i mycode.py")
+ # Coverage.py warning: Could not parse Python file blah_blah/mycode.py (couldnt-parse)
# Name Stmts Miss Cover
# ----------------------------
+ # No data to report.
- self.assertEqual(self.line_count(report), 3)
+ self.assertEqual(self.line_count(report), 4)
self.assertIn('No data to report.', report)
+ self.assertIn('(couldnt-parse)', report)
def test_dothtml_not_python(self):
# We run a .html file, and when reporting, we can't parse it as