diff options
-rw-r--r-- | CHANGES.rst | 16 | ||||
-rw-r--r-- | CONTRIBUTORS.txt (renamed from AUTHORS.txt) | 2 | ||||
-rw-r--r-- | MANIFEST.in | 2 | ||||
-rw-r--r-- | Makefile | 3 | ||||
-rw-r--r-- | coverage/config.py | 8 | ||||
-rw-r--r-- | coverage/execfile.py | 18 | ||||
-rw-r--r-- | coverage/parser.py | 66 | ||||
-rw-r--r-- | coverage/python.py | 2 | ||||
-rw-r--r-- | doc/faq.rst | 2 | ||||
-rw-r--r-- | doc/install.rst | 17 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rw-r--r-- | tests/test_cmdline.py | 6 | ||||
-rw-r--r-- | tests/test_config.py | 2 | ||||
-rw-r--r-- | tests/test_execfile.py | 19 | ||||
-rw-r--r-- | tests/test_process.py | 3 |
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 @@ -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 @@ -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.") |