summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst16
-rw-r--r--CONTRIBUTORS.txt (renamed from AUTHORS.txt)2
-rw-r--r--MANIFEST.in2
-rw-r--r--Makefile3
-rw-r--r--coverage/config.py8
-rw-r--r--coverage/execfile.py18
-rw-r--r--coverage/parser.py66
-rw-r--r--coverage/python.py2
-rw-r--r--doc/faq.rst2
-rw-r--r--doc/install.rst17
-rw-r--r--setup.py7
-rw-r--r--tests/test_cmdline.py6
-rw-r--r--tests/test_config.py2
-rw-r--r--tests/test_execfile.py19
-rw-r--r--tests/test_process.py3
15 files changed, 130 insertions, 43 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 569f4b50..81b1c998 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -23,6 +23,10 @@ Unreleased
``[coverage:run]`` section of tox.ini. Implements part of `issue 519`_.
Thanks, Stephen Finucane.
+- Specifying both ``--source`` and ``--include`` no longer silently ignores the
+ include setting, instead it fails with a message. Thanks, Nathan Land and
+ Loïc Dachary. Closes `issue 265`_.
+
- The ``Coverage.combine`` method has a new parameter, ``strict=False``, to
support failing if there are no data files to combine.
@@ -34,6 +38,9 @@ Unreleased
- The text report now properly sizes headers when skipping some files, fixing
`issue 524`_. Thanks, Anthony Sottile and Loïc Dachary.
+- Coverage.py can now search .pex files for source, just as it can .zip and
+ .egg. Thanks, Peter Ebden.
+
- Data files are now about 15% smaller.
- Improvements in the ``[run] debug`` setting:
@@ -51,6 +58,9 @@ Unreleased
- Fixed an unusual bug involving multiple coding declarations affecting code
containing code in multi-line strings: `issue 529`_.
+- If you try to run a non-Python file with coverage.py, you will now get a more
+ useful error message. `Issue 514`_.
+
- The default pragma regex changed slightly, but this will only matter to you
if you are deranged and use mixed-case pragmas.
@@ -69,8 +79,14 @@ Unreleased
- Switched to pytest from nose for running the coverage.py tests.
+- Renamed AUTHORS.txt to CONTRIBUTORS.txt, since there are other ways to
+ contribute than by writing code. Also put the count of contributors into the
+ author string in setup.py, though this might be too cute.
+
+.. _issue 265: https://bitbucket.org/ned/coveragepy/issues/265/when-using-source-include-is-silently
.. _issue 412: https://bitbucket.org/ned/coveragepy/issues/412/coverage-combine-should-error-if-no
.. _issue 505: https://bitbucket.org/ned/coveragepy/issues/505/use-canonical-filename-for-debounce
+.. _issue 514: https://bitbucket.org/ned/coveragepy/issues/514/path-to-problem-file-not-reported-when
.. _issue 510: https://bitbucket.org/ned/coveragepy/issues/510/erase-still-needed-in-42
.. _issue 511: https://bitbucket.org/ned/coveragepy/issues/511/version-42-coverage-combine-empties
.. _issue 516: https://bitbucket.org/ned/coveragepy/issues/516/running-coverage-combine-twice-deletes-all
diff --git a/AUTHORS.txt b/CONTRIBUTORS.txt
index 36db14bb..a7e7b858 100644
--- a/AUTHORS.txt
+++ b/CONTRIBUTORS.txt
@@ -17,6 +17,7 @@ Calen Pennington
Carl Gieringer
Catherine Proulx
Chris Adams
+Chris Jerdonek
Chris Rose
Christian Heimes
Christine Lytwynec
@@ -64,6 +65,7 @@ Nathan Land
Noel O'Boyle
Pablo Carballo
Patrick Mezard
+Peter Ebden
Peter Portante
Rodrigue Cloutier
Roger Hu
diff --git a/MANIFEST.in b/MANIFEST.in
index 31e2230c..462f24ff 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,7 +3,7 @@
# MANIFEST.in file for coverage.py
-include AUTHORS.txt
+include CONTRIBUTORS.txt
include CHANGES.rst
include LICENSE.txt
include MANIFEST.in
diff --git a/Makefile b/Makefile
index eb2434c4..f9c43e88 100644
--- a/Makefile
+++ b/Makefile
@@ -44,6 +44,9 @@ pep8:
test:
tox -e py27,py35 $(ARGS)
+smoke:
+ COVERAGE_NO_PYTRACER=1 tox -e py27,py35 -- -n 6 -m "not expensive" $(ARGS)
+
metacov:
COVERAGE_COVERAGE=yes tox $(ARGS)
diff --git a/coverage/config.py b/coverage/config.py
index ad3efa91..287844b8 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -368,7 +368,6 @@ class CoverageConfig(object):
Returns the value of the option.
"""
-
# Check all the hard-coded options.
for option_spec in self.CONFIG_FILE_OPTIONS:
attr, where = option_spec[:2]
@@ -383,6 +382,11 @@ class CoverageConfig(object):
# If we get here, we didn't find the option.
raise CoverageException("No such option: %r" % option_name)
+ def sanity_check(self):
+ """Check interactions among settings, and raise if there's a problem."""
+ if (self.source is not None) and (self.include is not None):
+ raise CoverageException("--include and --source are mutually exclusive")
+
def read_coverage_config(config_file, **kwargs):
"""Read the coverage.py configuration.
@@ -439,4 +443,6 @@ def read_coverage_config(config_file, **kwargs):
# 4) from constructor arguments:
config.from_args(**kwargs)
+ config.sanity_check()
+
return config_file, config
diff --git a/coverage/execfile.py b/coverage/execfile.py
index 3e20a527..58b05402 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -10,7 +10,7 @@ import types
from coverage.backward import BUILTINS
from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec
-from coverage.misc import ExceptionDuringRun, NoCode, NoSource, isolate_module
+from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module
from coverage.phystokens import compile_unicode
from coverage.python import get_python_source
@@ -166,11 +166,17 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None):
sys.path[0] = path0 if path0 is not None else my_path0
try:
- # Make a code object somehow.
- if filename.endswith((".pyc", ".pyo")):
- code = make_code_from_pyc(filename)
- else:
- code = make_code_from_py(filename)
+ try:
+ # Make a code object somehow.
+ if filename.endswith((".pyc", ".pyo")):
+ code = make_code_from_pyc(filename)
+ else:
+ code = make_code_from_py(filename)
+ except CoverageException:
+ raise
+ except Exception as exc:
+ msg = "Couldn't run {filename!r} as Python code: {exc.__class__.__name__}: {exc}"
+ raise CoverageException(msg.format(filename=filename, exc=exc))
# Execute the code object.
try:
diff --git a/coverage/parser.py b/coverage/parser.py
index 3d46bfa1..e75694f9 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -433,23 +433,35 @@ class ByteParser(object):
class LoopBlock(object):
"""A block on the block stack representing a `for` or `while` loop."""
+ @contract(start=int)
def __init__(self, start):
+ # The line number where the loop starts.
self.start = start
+ # A set of ArcStarts, the arcs from break statements exiting this loop.
self.break_exits = set()
class FunctionBlock(object):
"""A block on the block stack representing a function definition."""
+ @contract(start=int, name=str)
def __init__(self, start, name):
+ # The line number where the function starts.
self.start = start
+ # The name of the function.
self.name = name
class TryBlock(object):
"""A block on the block stack representing a `try` block."""
- def __init__(self, handler_start=None, final_start=None):
+ @contract(handler_start='int|None', final_start='int|None')
+ def __init__(self, handler_start, final_start):
+ # The line number of the first "except" handler, if any.
self.handler_start = handler_start
+ # The line number of the "finally:" clause, if any.
self.final_start = final_start
+
+ # The ArcStarts for breaks/continues/returns/raises inside the "try:"
+ # that need to route through the "finally:" clause.
self.break_from = set()
self.continue_from = set()
self.return_from = set()
@@ -459,8 +471,13 @@ class TryBlock(object):
class ArcStart(collections.namedtuple("Arc", "lineno, cause")):
"""The information needed to start an arc.
- `lineno` is the line number the arc starts from. `cause` is a fragment
- used as the startmsg for AstArcAnalyzer.missing_arc_fragments.
+ `lineno` is the line number the arc starts from.
+
+ `cause` is an English text fragment used as the `startmsg` for
+ AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an
+ arc wasn't executed, so should fit well into a sentence of the form,
+ "Line 17 didn't run because {cause}." The fragment can include "{lineno}"
+ to have `lineno` interpolated into it.
"""
def __new__(cls, lineno, cause=None):
@@ -493,7 +510,9 @@ class AstArcAnalyzer(object):
self.arcs = set()
- # A map from arc pairs to a pair of sentence fragments: (startmsg, endmsg).
+ # A map from arc pairs to a list of pairs of sentence fragments:
+ # { (start, end): [(startmsg, endmsg), ...], }
+ #
# For an arc from line 17, they should be usable like:
# "Line 17 {endmsg}, because {startmsg}"
self.missing_arc_fragments = collections.defaultdict(list)
@@ -570,6 +589,7 @@ class AstArcAnalyzer(object):
# Modules have no line number, they always start at 1.
return 1
+ # The node types that just flow to the next node with no complications.
OK_TO_DEFAULT = set([
"Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global",
"Import", "ImportFrom", "Nonlocal", "Pass", "Print",
@@ -586,12 +606,15 @@ class AstArcAnalyzer(object):
handler = getattr(self, "_handle__" + node_name, None)
if handler is not None:
return handler(node)
-
- if 0:
- node_name = node.__class__.__name__
- if node_name not in self.OK_TO_DEFAULT:
+ else:
+ # No handler: either it's something that's ok to default (a simple
+ # statement), or it's something we overlooked. Change this 0 to 1
+ # to see if it's overlooked.
+ if 0 and node_name not in self.OK_TO_DEFAULT:
print("*** Unhandled: {0}".format(node))
- return set([ArcStart(self.line_for_node(node), cause=None)])
+
+ # Default for simple statements: one exit from this node.
+ return set([ArcStart(self.line_for_node(node))])
@contract(returns='ArcStarts')
def add_body_arcs(self, body, from_start=None, prev_starts=None):
@@ -634,6 +657,15 @@ class AstArcAnalyzer(object):
# listcomps hidden in lists: x = [[i for i in range(10)]]
# nested function definitions
+
+ # Exit processing: process_*_exits
+ #
+ # These functions process the four kinds of jump exits: break, continue,
+ # raise, and return. To figure out where an exit goes, we have to look at
+ # the block stack context. For example, a break will jump to the nearest
+ # enclosing loop block, or the nearest enclosing finally block, whichever
+ # is nearer.
+
@contract(exits='ArcStarts')
def process_break_exits(self, exits):
"""Add arcs due to jumps from `exits` being breaks."""
@@ -692,7 +724,12 @@ class AstArcAnalyzer(object):
)
break
- ## Handlers
+
+ # Handlers: _handle__*
+ #
+ # Each handler deals with a specific AST node type, dispatched from
+ # add_arcs. These functions mirror the Python semantics of each syntactic
+ # construct.
@contract(returns='ArcStarts')
def _handle__Break(self, node):
@@ -722,7 +759,7 @@ class AstArcAnalyzer(object):
self.add_arc(last, lineno)
last = lineno
# The body is handled in collect_arcs.
- return set([ArcStart(last, cause=None)])
+ return set([ArcStart(last)])
_handle__ClassDef = _handle_decorated
@@ -749,7 +786,7 @@ class AstArcAnalyzer(object):
else_exits = self.add_body_arcs(node.orelse, from_start=from_start)
exits |= else_exits
else:
- # no else clause: exit from the for line.
+ # No else clause: exit from the for line.
exits.add(from_start)
return exits
@@ -795,11 +832,11 @@ class AstArcAnalyzer(object):
else:
final_start = None
- try_block = TryBlock(handler_start=handler_start, final_start=final_start)
+ try_block = TryBlock(handler_start, final_start)
self.block_stack.append(try_block)
start = self.line_for_node(node)
- exits = self.add_body_arcs(node.body, from_start=ArcStart(start, cause=None))
+ exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
# We're done with the `try` body, so this block no longer handles
# exceptions. We keep the block so the `finally` clause can pick up
@@ -860,6 +897,7 @@ class AstArcAnalyzer(object):
return exits
+ @contract(returns='ArcStarts')
def _combine_finally_starts(self, starts, exits):
"""Helper for building the cause of `finally` branches."""
causes = []
diff --git a/coverage/python.py b/coverage/python.py
index 601318c5..c3ca0e1e 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -75,7 +75,7 @@ def get_zip_bytes(filename):
an empty string if the file is empty.
"""
- markers = ['.zip'+os.sep, '.egg'+os.sep]
+ markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep]
for marker in markers:
if marker in filename:
parts = filename.split(marker)
diff --git a/doc/faq.rst b/doc/faq.rst
index 6609ab32..c0c6759a 100644
--- a/doc/faq.rst
+++ b/doc/faq.rst
@@ -123,4 +123,4 @@ Since 2004, `Ned Batchelder`_ has extended and maintained it with the help of
.. _Gareth Rees: http://garethrees.org/
.. _Ned Batchelder: http://nedbatchelder.com
-.. _many others: http://bitbucket.org/ned/coveragepy/src/tip/AUTHORS.txt
+.. _many others: http://bitbucket.org/ned/coveragepy/src/tip/CONTRIBUTORS.txt
diff --git a/doc/install.rst b/doc/install.rst
index bcea93f1..5774d1b1 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -24,17 +24,16 @@ Installation
.. :history: 20131005T210600, updated for 3.7.
.. :history: 20131212T213500, updated for 3.7.1.
.. :history: 20140927T102700, updated for 4.0a1.
+.. :history: 20161218T173000, remove alternate instructions w/ Distribute
.. highlight:: console
.. _coverage_pypi: http://pypi.python.org/pypi/coverage
.. _setuptools: http://pypi.python.org/pypi/setuptools
-.. _Distribute: http://packages.python.org/distribute/
-Installing coverage.py is done in the usual ways. The simplest way is with
-pip::
+You can install coverage.py in the usual ways. The simplest way is with pip::
$ pip install coverage
@@ -45,18 +44,6 @@ pip::
$ pip install --pre coverage
-The alternate old-school technique is:
-
-#. Install (or already have installed) `setuptools`_ or `Distribute`_.
-
-#. Download the appropriate kit from the
- `coverage.py page on the Python Package Index`__.
-
-#. Run ``python setup.py install``.
-
-.. __: coverage_pypi_
-
-
.. _install_extension:
C Extension
diff --git a/setup.py b/setup.py
index b304864d..0e893062 100644
--- a/setup.py
+++ b/setup.py
@@ -46,6 +46,11 @@ with open(cov_ver_py) as version_file:
with open("README.rst") as readme:
long_description = readme.read().replace("http://coverage.readthedocs.io", __url__)
+with open("CONTRIBUTORS.txt") as contributors:
+ paras = contributors.read().split("\n\n")
+ num_others = len(paras[-1].splitlines())
+ num_others += 1 # Count Gareth Rees, who is mentioned in the top paragraph.
+
classifier_list = classifiers.splitlines()
if version_info[3] == 'alpha':
@@ -86,7 +91,7 @@ setup_args = dict(
# We need to get HTML assets from our htmlfiles directory.
zip_safe=False,
- author='Ned Batchelder and others',
+ author='Ned Batchelder and {0} others'.format(num_others),
author_email='ned@nedbatchelder.com',
description=doc,
long_description=long_description,
diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py
index 3b982ebe..3b674de7 100644
--- a/tests/test_cmdline.py
+++ b/tests/test_cmdline.py
@@ -16,7 +16,7 @@ import coverage.cmdline
from coverage import env
from coverage.config import CoverageConfig
from coverage.data import CoverageData, CoverageDataFiles
-from coverage.misc import ExceptionDuringRun
+from coverage.misc import CoverageException, ExceptionDuringRun
from tests.coveragetest import CoverageTest, OK, ERR
@@ -459,6 +459,10 @@ class CmdLineTest(BaseCmdLineTest):
.save()
""")
+ def test_bad_run_args_with_both_source_and_include(self):
+ with self.assertRaisesRegex(CoverageException, 'mutually exclusive'):
+ self.command_line("run --include=pre1,pre2 --source=lol,wut foo.py", ret=ERR)
+
def test_bad_concurrency(self):
self.command_line("run --concurrency=nothing", ret=ERR)
out = self.stdout()
diff --git a/tests/test_config.py b/tests/test_config.py
index 6cb5e468..2aa592b9 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -239,7 +239,6 @@ class ConfigFileTest(CoverageTest):
branch = 1
cover_pylib = TRUE
parallel = on
- include = a/ , b/
concurrency = thread
source = myapp
plugins =
@@ -329,7 +328,6 @@ class ConfigFileTest(CoverageTest):
self.assertEqual(cov.get_exclude_list(), ["if 0:", r"pragma:?\s+no cover", "another_tab"])
self.assertTrue(cov.config.ignore_errors)
- self.assertEqual(cov.config.include, ["a/", "b/"])
self.assertEqual(cov.config.omit, ["one", "another", "some_more", "yet_more"])
self.assertEqual(cov.config.precision, 3)
diff --git a/tests/test_execfile.py b/tests/test_execfile.py
index 889d6cfd..8585b16d 100644
--- a/tests/test_execfile.py
+++ b/tests/test_execfile.py
@@ -145,6 +145,25 @@ class RunPycFileTest(CoverageTest):
with self.assertRaisesRegex(NoCode, "No file to run: 'xyzzy.pyc'"):
run_python_file("xyzzy.pyc", [])
+ def test_running_py_from_binary(self):
+ # Use make_file to get the bookkeeping. Ideally, it would
+ # be able to write binary files.
+ bf = self.make_file("binary")
+ with open(bf, "wb") as f:
+ f.write(b'\x7fELF\x02\x01\x01\x00\x00\x00')
+
+ msg = (
+ r"Couldn't run 'binary' as Python code: "
+ r"(TypeError|ValueError): "
+ r"("
+ r"compile\(\) expected string without null bytes" # for py2
+ r"|"
+ r"source code string cannot contain null bytes" # for py3
+ r")"
+ )
+ with self.assertRaisesRegex(Exception, msg):
+ run_python_file(bf, [bf])
+
class RunModuleTest(CoverageTest):
"""Test run_python_module."""
diff --git a/tests/test_process.py b/tests/test_process.py
index 81cd5ade..57639db7 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -11,6 +11,8 @@ import re
import sys
import textwrap
+import pytest
+
import coverage
from coverage import env, CoverageData
from coverage.misc import output_encoding
@@ -692,6 +694,7 @@ class ProcessTest(CoverageTest):
self.assertEqual(len(infos), 1)
self.assertEqual(infos[0]['note'], u"These are musical notes: ♫𝅗𝅥♩")
+ @pytest.mark.expensive
def test_fullcoverage(self): # pragma: not covered
if env.PY2: # This doesn't work on Python 2.
self.skipTest("fullcoverage doesn't work on Python 2.")