summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt4
-rw-r--r--coverage/parser.py5
-rw-r--r--test/modules/pkg1/__init__.py3
-rw-r--r--test/modules/pkg2/__init__.py2
-rw-r--r--test/test_summary.py43
5 files changed, 55 insertions, 2 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 69f005b..e114dbd 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -39,6 +39,9 @@ Version 3.4a1
it needs to be. A cache of tracing decisions was broken, but has now been
fixed.
+- Python files with no statements, for example, empty ``__init__.py`` files,
+ are now reported as having zero statements instead of one. Fixes `issue 1`_.
+
- Reports now have a column of missed line counts rather than executed line
counts, since developers should focus on reducing the missed lines to zero,
rather than increasing the executed lines to varying targets. Once
@@ -70,6 +73,7 @@ Version 3.4a1
- Asking for an HTML report with no files now shows a nice error message rather
than a cryptic failure ('int' object is unsubscriptable). Fixes `issue 59`_.
+.. _issue 1: http://bitbucket.org/ned/coveragepy/issue/1/empty-__init__py-files-are-reported-as-1-executable
.. _issue 34: http://bitbucket.org/ned/coveragepy/issue/34/enhanced-omit-globbing-handling
.. _issue 36: http://bitbucket.org/ned/coveragepy/issue/36/provide-regex-style-omit
.. _issue 46: http://bitbucket.org/ned/coveragepy/issue/46
diff --git a/coverage/parser.py b/coverage/parser.py
index d883df4..8285d43 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -85,6 +85,7 @@ class CodeParser(object):
excluding = False
prev_toktype = token.INDENT
first_line = None
+ empty = True
tokgen = tokenize.generate_tokens(StringIO(self.text).readline)
for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
@@ -128,6 +129,7 @@ class CodeParser(object):
if ttext.strip() and toktype != tokenize.COMMENT:
# A non-whitespace token.
+ empty = False
if first_line is None:
# The token is not whitespace, and is the first in a
# statement.
@@ -141,7 +143,8 @@ class CodeParser(object):
prev_toktype = toktype
# Find the starts of the executable statements.
- self.statement_starts.update(self.byte_parser._find_statements())
+ if not empty:
+ self.statement_starts.update(self.byte_parser._find_statements())
def first_line(self, line):
"""Return the first line number of the statement including `line`."""
diff --git a/test/modules/pkg1/__init__.py b/test/modules/pkg1/__init__.py
index e69de29..2dfeb9c 100644
--- a/test/modules/pkg1/__init__.py
+++ b/test/modules/pkg1/__init__.py
@@ -0,0 +1,3 @@
+# This __init__.py has a module-level docstring, which is counted as a
+# statement.
+"""A simple package for testing with."""
diff --git a/test/modules/pkg2/__init__.py b/test/modules/pkg2/__init__.py
index e69de29..090efbf 100644
--- a/test/modules/pkg2/__init__.py
+++ b/test/modules/pkg2/__init__.py
@@ -0,0 +1,2 @@
+# This is an __init__.py file, with no executable statements in it.
+# This comment shouldn't confuse the parser.
diff --git a/test/test_summary.py b/test/test_summary.py
index d797cc5..c6598c6 100644
--- a/test/test_summary.py
+++ b/test/test_summary.py
@@ -1,6 +1,9 @@
"""Test text-based summary reporting for coverage.py"""
-import os, re, sys
+import os, re, sys, textwrap
+
+import coverage
+from coverage.backward import StringIO
sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
from coveragetest import CoverageTest
@@ -16,6 +19,8 @@ class SummaryTest(CoverageTest):
a = 1
print ('done')
""")
+ # Parent class saves and restores sys.path, we can just modify it.
+ sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules'))
def report_from_command(self, cmd):
"""Return the report from the `cmd`, with some convenience added."""
@@ -105,3 +110,39 @@ class SummaryTest(CoverageTest):
self.assertTrue("mybranch " in report)
self.assertEqual(self.last_line_squeezed(report),
"mybranch 5 0 2 1 85%")
+
+class SummaryTest2(CoverageTest):
+ """Another bunch of summary tests."""
+ # This class exists because tests naturally clump into classes based on the
+ # needs of their setUp and tearDown, rather than the product features they
+ # are testing. There's probably a better way to organize these.
+
+ run_in_temp_dir = False
+
+ def setUp(self):
+ super(SummaryTest2, self).setUp()
+ # Parent class saves and restores sys.path, we can just modify it.
+ sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules'))
+
+ def test_empty_files(self):
+ cov = coverage.coverage()
+ cov.start()
+ import usepkgs # pylint: disable-msg=F0401,W0612
+ cov.stop()
+
+ repout = StringIO()
+ cov.report(file=repout, show_missing=False)
+
+ self.assertMultiLineEqual(repout.getvalue(), textwrap.dedent("""\
+ Name Stmts Miss Cover
+ ------------------------------------------------
+ test\modules\pkg1\__init__ 1 0 100%
+ test\modules\pkg1\p1a 3 0 100%
+ test\modules\pkg1\p1b 3 0 100%
+ test\modules\pkg2\__init__ 0 0 100%
+ test\modules\pkg2\p2a 3 0 100%
+ test\modules\pkg2\p2b 3 0 100%
+ test\modules\usepkgs 2 0 100%
+ ------------------------------------------------
+ TOTAL 15 0 100%
+ """))