summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/__init__.py28
-rw-r--r--coverage/__main__.py2
-rw-r--r--coverage/backward.py16
-rw-r--r--coverage/bytecode.py1
-rw-r--r--coverage/cmdline.py334
-rw-r--r--coverage/config.py184
-rw-r--r--coverage/control.py97
-rw-r--r--coverage/data.py33
-rw-r--r--coverage/execfile.py2
-rw-r--r--coverage/files.py83
-rw-r--r--coverage/fullcoverage/encodings.py8
-rw-r--r--coverage/html.py14
-rw-r--r--coverage/htmlfiles/coverage_html.js1
-rw-r--r--coverage/htmlfiles/index.html4
-rw-r--r--coverage/misc.py14
-rw-r--r--coverage/phystokens.py2
-rw-r--r--coverage/report.py5
-rw-r--r--coverage/results.py6
-rw-r--r--coverage/summary.py2
-rw-r--r--coverage/tracer.c5
-rw-r--r--coverage/version.py9
-rw-r--r--coverage/xmlreport.py3
22 files changed, 516 insertions, 337 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py
index e2db3a56..0ccc699f 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -5,19 +5,13 @@ http://nedbatchelder.com/code/coverage
"""
-__version__ = "3.5.4b1" # see detailed history in CHANGES.txt
-
-__url__ = "http://nedbatchelder.com/code/coverage"
-if max(__version__).isalpha():
- # For pre-releases, use a version-specific URL.
- __url__ += "/" + __version__
+from coverage.version import __version__, __url__
from coverage.control import coverage, process_startup
from coverage.data import CoverageData
from coverage.cmdline import main, CoverageScript
from coverage.misc import CoverageException
-
# Module-level functions. The original API to this module was based on
# functions defined directly in the module, with a singleton of the coverage()
# class. That design hampered programmability, so the current api uses
@@ -36,6 +30,10 @@ def _singleton_method(name):
called.
"""
+ # Disable pylint msg W0612, because a bunch of variables look unused, but
+ # they're accessed via locals().
+ # pylint: disable=W0612
+
def wrapper(*args, **kwargs):
"""Singleton wrapper around a coverage method."""
global _the_coverage
@@ -75,6 +73,22 @@ report = _singleton_method('report')
annotate = _singleton_method('annotate')
+# On Windows, we encode and decode deep enough that something goes wrong and
+# the encodings.utf_8 module is loaded and then unloaded, I don't know why.
+# Adding a reference here prevents it from being unloaded. Yuk.
+import encodings.utf_8
+
+# Because of the "from coverage.control import fooey" lines at the top of the
+# file, there's an entry for coverage.coverage in sys.modules, mapped to None.
+# This makes some inspection tools (like pydoc) unable to find the class
+# coverage.coverage. So remove that entry.
+import sys
+try:
+ del sys.modules['coverage.coverage']
+except KeyError:
+ pass
+
+
# COPYRIGHT AND LICENSE
#
# Copyright 2001 Gareth Rees. All rights reserved.
diff --git a/coverage/__main__.py b/coverage/__main__.py
index 111ca2e0..55e0d259 100644
--- a/coverage/__main__.py
+++ b/coverage/__main__.py
@@ -1,4 +1,4 @@
-"""Coverage.py's main entrypoint."""
+"""Coverage.py's main entry point."""
import sys
from coverage.cmdline import main
sys.exit(main())
diff --git a/coverage/backward.py b/coverage/backward.py
index 637a5976..6347501a 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -49,6 +49,16 @@ try:
except NameError:
range = range
+# A function to iterate listlessly over a dict's items.
+if "iteritems" in dir({}):
+ def iitems(d):
+ """Produce the items from dict `d`."""
+ return d.iteritems()
+else:
+ def iitems(d):
+ """Produce the items from dict `d`."""
+ return d.items()
+
# Exec is a statement in Py2, a function in Py3
if sys.version_info >= (3, 0):
def exec_code_object(code, global_map):
@@ -66,12 +76,6 @@ else:
)
)
-# ConfigParser was renamed to the more-standard configparser
-try:
- import configparser
-except ImportError:
- import ConfigParser as configparser
-
# Reading Python source and interpreting the coding comment is a big deal.
if sys.version_info >= (3, 0):
# Python 3.2 provides `tokenize.open`, the best way to open source files.
diff --git a/coverage/bytecode.py b/coverage/bytecode.py
index 61c311eb..fd5c7da2 100644
--- a/coverage/bytecode.py
+++ b/coverage/bytecode.py
@@ -27,6 +27,7 @@ class ByteCodes(object):
Returns `ByteCode` objects.
"""
+ # pylint: disable=R0924
def __init__(self, code):
self.code = code
self.offset = 0
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 1ce5e0f5..5217a313 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -1,6 +1,6 @@
"""Command-line support for Coverage."""
-import optparse, re, sys, traceback
+import optparse, sys, traceback
from coverage.backward import sorted # pylint: disable=W0622
from coverage.execfile import run_python_file, run_python_module
@@ -20,10 +20,13 @@ class Opts(object):
help="Measure branch coverage in addition to statement coverage."
)
directory = optparse.make_option(
- '-d', '--directory', action='store',
- metavar="DIR",
+ '-d', '--directory', action='store', metavar="DIR",
help="Write the output files to DIR."
)
+ fail_under = optparse.make_option(
+ '', '--fail-under', action='store', metavar="MIN", type="int",
+ help="Exit with a status of 2 if the total coverage is less than MIN."
+ )
help = optparse.make_option(
'-h', '--help', action='store_true',
help="Get help on this command."
@@ -89,6 +92,10 @@ class Opts(object):
help="Use a simpler but slower trace method. Try this if you get "
"seemingly impossible results!"
)
+ title = optparse.make_option(
+ '', '--title', action='store', metavar="TITLE",
+ help="A text string to use as the title on the HTML."
+ )
version = optparse.make_option(
'', '--version', action='store_true',
help="Display version information and exit."
@@ -111,6 +118,7 @@ class CoverageOptionParser(optparse.OptionParser, object):
actions=[],
branch=None,
directory=None,
+ fail_under=None,
help=None,
ignore_errors=None,
include=None,
@@ -122,6 +130,7 @@ class CoverageOptionParser(optparse.OptionParser, object):
show_missing=None,
source=None,
timid=None,
+ title=None,
erase_first=None,
version=None,
)
@@ -273,9 +282,11 @@ CMDS = {
'html': CmdOptionParser("html",
[
Opts.directory,
+ Opts.fail_under,
Opts.ignore_errors,
Opts.omit,
Opts.include,
+ Opts.title,
] + GLOBAL_ARGS,
usage = "[options] [modules]",
description = "Create an HTML report of the coverage of the files. "
@@ -285,6 +296,7 @@ CMDS = {
'report': CmdOptionParser("report",
[
+ Opts.fail_under,
Opts.ignore_errors,
Opts.omit,
Opts.include,
@@ -314,20 +326,20 @@ CMDS = {
'xml': CmdOptionParser("xml",
[
+ Opts.fail_under,
Opts.ignore_errors,
Opts.omit,
Opts.include,
Opts.output_xml,
] + GLOBAL_ARGS,
cmd = "xml",
- defaults = {'outfile': 'coverage.xml'},
usage = "[options] [modules]",
description = "Generate an XML report of coverage results."
),
}
-OK, ERR = 0, 1
+OK, ERR, FAIL_UNDER = 0, 1, 2
class CoverageScript(object):
@@ -346,27 +358,10 @@ class CoverageScript(object):
self.run_python_file = _run_python_file or run_python_file
self.run_python_module = _run_python_module or run_python_module
self.help_fn = _help_fn or self.help
+ self.classic = False
self.coverage = None
- def help(self, error=None, topic=None, parser=None):
- """Display an error message, or the named topic."""
- assert error or topic or parser
- if error:
- print(error)
- print("Use 'coverage help' for help.")
- elif parser:
- print(parser.format_help().strip())
- else:
- # Parse out the topic we want from HELP_TOPICS
- topic_list = re.split("(?m)^=+ (\w+) =+$", HELP_TOPICS)
- topics = dict(zip(topic_list[1::2], topic_list[2::2]))
- help_msg = topics.get(topic, '').strip()
- if help_msg:
- print(help_msg % self.covpkg.__dict__)
- else:
- print("Don't know topic %r" % topic)
-
def command_line(self, argv):
"""The bulk of the command line interface to Coverage.
@@ -376,15 +371,14 @@ class CoverageScript(object):
"""
# Collect the command-line options.
-
if not argv:
self.help_fn(topic='minimum_help')
return OK
# The command syntax we parse depends on the first argument. Classic
# syntax always starts with an option.
- classic = argv[0].startswith('-')
- if classic:
+ self.classic = argv[0].startswith('-')
+ if self.classic:
parser = ClassicOptionParser()
else:
parser = CMDS.get(argv[0])
@@ -398,58 +392,12 @@ class CoverageScript(object):
if not ok:
return ERR
- # Handle help.
- if options.help:
- if classic:
- self.help_fn(topic='help')
- else:
- self.help_fn(parser=parser)
- return OK
-
- if "help" in options.actions:
- if args:
- for a in args:
- parser = CMDS.get(a)
- if parser:
- self.help_fn(parser=parser)
- else:
- self.help_fn(topic=a)
- else:
- self.help_fn(topic='help')
- return OK
-
- # Handle version.
- if options.version:
- self.help_fn(topic='version')
+ # Handle help and version.
+ if self.do_help(options, args, parser):
return OK
# Check for conflicts and problems in the options.
- for i in ['erase', 'execute']:
- for j in ['annotate', 'html', 'report', 'combine']:
- if (i in options.actions) and (j in options.actions):
- self.help_fn("You can't specify the '%s' and '%s' "
- "options at the same time." % (i, j))
- return ERR
-
- if not options.actions:
- self.help_fn(
- "You must specify at least one of -e, -x, -c, -r, -a, or -b."
- )
- return ERR
- args_allowed = (
- 'execute' in options.actions or
- 'annotate' in options.actions or
- 'html' in options.actions or
- 'debug' in options.actions or
- 'report' in options.actions or
- 'xml' in options.actions
- )
- if not args_allowed and args:
- self.help_fn("Unexpected arguments: %s" % " ".join(args))
- return ERR
-
- if 'execute' in options.actions and not args:
- self.help_fn("Nothing to do.")
+ if not self.args_ok(options, args):
return ERR
# Listify the list options.
@@ -470,38 +418,7 @@ class CoverageScript(object):
)
if 'debug' in options.actions:
- if not args:
- self.help_fn("What information would you like: data, sys?")
- return ERR
- for info in args:
- if info == 'sys':
- print("-- sys ----------------------------------------")
- for label, info in self.coverage.sysinfo():
- if info == []:
- info = "-none-"
- if isinstance(info, list):
- print("%15s:" % label)
- for e in info:
- print("%15s %s" % ("", e))
- else:
- print("%15s: %s" % (label, info))
- elif info == 'data':
- print("-- data ---------------------------------------")
- self.coverage.load()
- print("path: %s" % self.coverage.data.filename)
- print("has_arcs: %r" % self.coverage.data.has_arcs())
- summary = self.coverage.data.summary(fullpath=True)
- if summary:
- filenames = sorted(summary.keys())
- print("\n%d files:" % len(filenames))
- for f in filenames:
- print("%s: %d lines" % (f, summary[f]))
- else:
- print("No data collected")
- else:
- self.help_fn("Don't know what you mean by %r" % info)
- return ERR
- return OK
+ return self.do_debug(args)
if 'erase' in options.actions or options.erase_first:
self.coverage.erase()
@@ -509,22 +426,7 @@ class CoverageScript(object):
self.coverage.load()
if 'execute' in options.actions:
- # Run the script.
- self.coverage.start()
- code_ran = True
- try:
- try:
- if options.module:
- self.run_python_module(args[0], args)
- else:
- self.run_python_file(args[0], args)
- except NoSource:
- code_ran = False
- raise
- finally:
- if code_ran:
- self.coverage.stop()
- self.coverage.save()
+ self.do_execute(options, args)
if 'combine' in options.actions:
self.coverage.combine()
@@ -539,18 +441,165 @@ class CoverageScript(object):
)
if 'report' in options.actions:
- self.coverage.report(
+ total = self.coverage.report(
show_missing=options.show_missing, **report_args)
if 'annotate' in options.actions:
self.coverage.annotate(
directory=options.directory, **report_args)
if 'html' in options.actions:
- self.coverage.html_report(
- directory=options.directory, **report_args)
+ total = self.coverage.html_report(
+ directory=options.directory, title=options.title,
+ **report_args)
if 'xml' in options.actions:
outfile = options.outfile
- self.coverage.xml_report(outfile=outfile, **report_args)
+ total = self.coverage.xml_report(outfile=outfile, **report_args)
+
+ if options.fail_under is not None:
+ if total >= options.fail_under:
+ return OK
+ else:
+ return FAIL_UNDER
+ else:
+ return OK
+
+ def help(self, error=None, topic=None, parser=None):
+ """Display an error message, or the named topic."""
+ assert error or topic or parser
+ if error:
+ print(error)
+ print("Use 'coverage help' for help.")
+ elif parser:
+ print(parser.format_help().strip())
+ else:
+ help_msg = HELP_TOPICS.get(topic, '').strip()
+ if help_msg:
+ print(help_msg % self.covpkg.__dict__)
+ else:
+ print("Don't know topic %r" % topic)
+
+ def do_help(self, options, args, parser):
+ """Deal with help requests.
+
+ Return True if it handled the request, False if not.
+
+ """
+ # Handle help.
+ if options.help:
+ if self.classic:
+ self.help_fn(topic='help')
+ else:
+ self.help_fn(parser=parser)
+ return True
+
+ if "help" in options.actions:
+ if args:
+ for a in args:
+ parser = CMDS.get(a)
+ if parser:
+ self.help_fn(parser=parser)
+ else:
+ self.help_fn(topic=a)
+ else:
+ self.help_fn(topic='help')
+ return True
+
+ # Handle version.
+ if options.version:
+ self.help_fn(topic='version')
+ return True
+
+ return False
+
+ def args_ok(self, options, args):
+ """Check for conflicts and problems in the options.
+
+ Returns True if everything is ok, or False if not.
+
+ """
+ for i in ['erase', 'execute']:
+ for j in ['annotate', 'html', 'report', 'combine']:
+ if (i in options.actions) and (j in options.actions):
+ self.help_fn("You can't specify the '%s' and '%s' "
+ "options at the same time." % (i, j))
+ return False
+ if not options.actions:
+ self.help_fn(
+ "You must specify at least one of -e, -x, -c, -r, -a, or -b."
+ )
+ return False
+ args_allowed = (
+ 'execute' in options.actions or
+ 'annotate' in options.actions or
+ 'html' in options.actions or
+ 'debug' in options.actions or
+ 'report' in options.actions or
+ 'xml' in options.actions
+ )
+ if not args_allowed and args:
+ self.help_fn("Unexpected arguments: %s" % " ".join(args))
+ return False
+
+ if 'execute' in options.actions and not args:
+ self.help_fn("Nothing to do.")
+ return False
+
+ return True
+
+ def do_execute(self, options, args):
+ """Implementation of 'coverage run'."""
+
+ # Run the script.
+ self.coverage.start()
+ code_ran = True
+ try:
+ try:
+ if options.module:
+ self.run_python_module(args[0], args)
+ else:
+ self.run_python_file(args[0], args)
+ except NoSource:
+ code_ran = False
+ raise
+ finally:
+ self.coverage.stop()
+ if code_ran:
+ self.coverage.save()
+
+ def do_debug(self, args):
+ """Implementation of 'coverage debug'."""
+
+ if not args:
+ self.help_fn("What information would you like: data, sys?")
+ return ERR
+ for info in args:
+ if info == 'sys':
+ print("-- sys ----------------------------------------")
+ for label, info in self.coverage.sysinfo():
+ if info == []:
+ info = "-none-"
+ if isinstance(info, list):
+ print("%15s:" % label)
+ for e in info:
+ print("%15s %s" % ("", e))
+ else:
+ print("%15s: %s" % (label, info))
+ elif info == 'data':
+ print("-- data ---------------------------------------")
+ self.coverage.load()
+ print("path: %s" % self.coverage.data.filename)
+ print("has_arcs: %r" % self.coverage.data.has_arcs())
+ summary = self.coverage.data.summary(fullpath=True)
+ if summary:
+ filenames = sorted(summary.keys())
+ print("\n%d files:" % len(filenames))
+ for f in filenames:
+ print("%s: %d lines" % (f, summary[f]))
+ else:
+ print("No data collected")
+ else:
+ self.help_fn("Don't know what you mean by %r" % info)
+ return ERR
return OK
@@ -568,10 +617,10 @@ def unshell_list(s):
return s.split(',')
-HELP_TOPICS = r"""
-
-== classic ====================================================================
-Coverage.py version %(__version__)s
+HELP_TOPICS = {
+# -------------------------
+'classic':
+r"""Coverage.py version %(__version__)s
Measure, collect, and report on code coverage in Python programs.
Usage:
@@ -615,8 +664,9 @@ coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...]
Coverage data is saved in the file .coverage by default. Set the
COVERAGE_FILE environment variable to save it somewhere else.
-
-== help =======================================================================
+""",
+# -------------------------
+'help': """\
Coverage.py, version %(__version__)s
Measure, collect, and report on code coverage in Python programs.
@@ -635,20 +685,22 @@ Commands:
Use "coverage help <command>" for detailed help on any command.
Use "coverage help classic" for help on older command syntax.
For more information, see %(__url__)s
-
-== minimum_help ===============================================================
+""",
+# -------------------------
+'minimum_help': """\
Code coverage for Python. Use 'coverage help' for help.
-
-== version ====================================================================
+""",
+# -------------------------
+'version': """\
Coverage.py, version %(__version__)s. %(__url__)s
-
-"""
+""",
+}
def main(argv=None):
- """The main entrypoint to Coverage.
+ """The main entry point to Coverage.
- This is installed as the script entrypoint.
+ This is installed as the script entry point.
"""
if argv is None:
diff --git a/coverage/config.py b/coverage/config.py
index 49d74e7a..8f1f6710 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -1,7 +1,55 @@
"""Config file for coverage.py"""
-import os
-from coverage.backward import configparser # pylint: disable=W0622
+import os, sys
+from coverage.backward import string_class, iitems
+
+# In py3, # ConfigParser was renamed to the more-standard configparser
+try:
+ import configparser # pylint: disable=F0401
+except ImportError:
+ import ConfigParser as configparser
+
+
+class HandyConfigParser(configparser.ConfigParser):
+ """Our specialization of ConfigParser."""
+
+ def read(self, filename):
+ """Read a filename as UTF-8 configuration data."""
+ kwargs = {}
+ if sys.version_info >= (3, 2):
+ kwargs['encoding'] = "utf-8"
+ configparser.ConfigParser.read(self, filename, **kwargs)
+
+ def getlist(self, section, option):
+ """Read a list of strings.
+
+ The value of `section` and `option` is treated as a comma- and newline-
+ separated list of strings. Each value is stripped of whitespace.
+
+ Returns the list of strings.
+
+ """
+ value_list = self.get(section, option)
+ values = []
+ for value_line in value_list.split('\n'):
+ for value in value_line.split(','):
+ value = value.strip()
+ if value:
+ values.append(value)
+ return values
+
+ def getlinelist(self, section, option):
+ """Read a list of full-line strings.
+
+ The value of `section` and `option` is treated as a newline-separated
+ list of strings. Each value is stripped of whitespace.
+
+ Returns the list of strings.
+
+ """
+ value_list = self.get(section, option)
+ return list(filter(None, value_list.split('\n')))
+
# The default line exclusion regexes
DEFAULT_EXCLUDE = [
@@ -29,7 +77,6 @@ class CoverageConfig(object):
operation of coverage.py.
"""
-
def __init__(self):
"""Initialize the configuration attributes to their defaults."""
# Defaults for [run]
@@ -53,6 +100,7 @@ class CoverageConfig(object):
# Defaults for [html]
self.html_dir = "htmlcov"
self.extra_css = None
+ self.html_title = "Coverage report"
# Defaults for [xml]
self.xml_output = "coverage.xml"
@@ -69,102 +117,66 @@ class CoverageConfig(object):
if env:
self.timid = ('--timid' in env)
+ MUST_BE_LIST = ["omit", "include"]
+
def from_args(self, **kwargs):
"""Read config values from `kwargs`."""
- for k, v in kwargs.items():
+ for k, v in iitems(kwargs):
if v is not None:
+ if k in self.MUST_BE_LIST and isinstance(v, string_class):
+ v = [v]
setattr(self, k, v)
- def from_file(self, *files):
- """Read configuration from .rc files.
+ def from_file(self, filename):
+ """Read configuration from a .rc file.
- Each argument in `files` is a file name to read.
+ `filename` is a file name to read.
"""
- cp = configparser.RawConfigParser()
- cp.read(files)
+ cp = HandyConfigParser()
+ cp.read(filename)
- # [run]
- if cp.has_option('run', 'branch'):
- self.branch = cp.getboolean('run', 'branch')
- if cp.has_option('run', 'cover_pylib'):
- self.cover_pylib = cp.getboolean('run', 'cover_pylib')
- if cp.has_option('run', 'data_file'):
- self.data_file = cp.get('run', 'data_file')
- if cp.has_option('run', 'include'):
- self.include = self.get_list(cp, 'run', 'include')
- if cp.has_option('run', 'omit'):
- self.omit = self.get_list(cp, 'run', 'omit')
- if cp.has_option('run', 'parallel'):
- self.parallel = cp.getboolean('run', 'parallel')
- if cp.has_option('run', 'source'):
- self.source = self.get_list(cp, 'run', 'source')
- if cp.has_option('run', 'timid'):
- self.timid = cp.getboolean('run', 'timid')
+ for option_spec in self.CONFIG_FILE_OPTIONS:
+ self.set_attr_from_config_option(cp, *option_spec)
- # [report]
- if cp.has_option('report', 'exclude_lines'):
- self.exclude_list = \
- self.get_line_list(cp, 'report', 'exclude_lines')
- if cp.has_option('report', 'ignore_errors'):
- self.ignore_errors = cp.getboolean('report', 'ignore_errors')
- if cp.has_option('report', 'include'):
- self.include = self.get_list(cp, 'report', 'include')
- if cp.has_option('report', 'omit'):
- self.omit = self.get_list(cp, 'report', 'omit')
- if cp.has_option('report', 'partial_branches'):
- self.partial_list = \
- self.get_line_list(cp, 'report', 'partial_branches')
- if cp.has_option('report', 'partial_branches_always'):
- self.partial_always_list = \
- self.get_line_list(cp, 'report', 'partial_branches_always')
- if cp.has_option('report', 'precision'):
- self.precision = cp.getint('report', 'precision')
- if cp.has_option('report', 'show_missing'):
- self.show_missing = cp.getboolean('report', 'show_missing')
-
- # [html]
- if cp.has_option('html', 'directory'):
- self.html_dir = cp.get('html', 'directory')
- if cp.has_option('html', 'extra_css'):
- self.extra_css = cp.get('html', 'extra_css')
-
- # [xml]
- if cp.has_option('xml', 'output'):
- self.xml_output = cp.get('xml', 'output')
-
- # [paths]
+ # [paths] is special
if cp.has_section('paths'):
for option in cp.options('paths'):
- self.paths[option] = self.get_list(cp, 'paths', option)
-
- def get_list(self, cp, section, option):
- """Read a list of strings from the ConfigParser `cp`.
-
- The value of `section` and `option` is treated as a comma- and newline-
- separated list of strings. Each value is stripped of whitespace.
-
- Returns the list of strings.
+ self.paths[option] = cp.getlist('paths', option)
- """
- value_list = cp.get(section, option)
- values = []
- for value_line in value_list.split('\n'):
- for value in value_line.split(','):
- value = value.strip()
- if value:
- values.append(value)
- return values
-
- def get_line_list(self, cp, section, option):
- """Read a list of full-line strings from the ConfigParser `cp`.
-
- The value of `section` and `option` is treated as a newline-separated
- list of strings. Each value is stripped of whitespace.
+ CONFIG_FILE_OPTIONS = [
+ # [run]
+ ('branch', 'run:branch', 'boolean'),
+ ('cover_pylib', 'run:cover_pylib', 'boolean'),
+ ('data_file', 'run:data_file'),
+ ('include', 'run:include', 'list'),
+ ('omit', 'run:omit', 'list'),
+ ('parallel', 'run:parallel', 'boolean'),
+ ('source', 'run:source', 'list'),
+ ('timid', 'run:timid', 'boolean'),
- Returns the list of strings.
+ # [report]
+ ('exclude_list', 'report:exclude_lines', 'linelist'),
+ ('ignore_errors', 'report:ignore_errors', 'boolean'),
+ ('include', 'report:include', 'list'),
+ ('omit', 'report:omit', 'list'),
+ ('partial_list', 'report:partial_branches', 'linelist'),
+ ('partial_always_list', 'report:partial_branches_always', 'linelist'),
+ ('precision', 'report:precision', 'int'),
+ ('show_missing', 'report:show_missing', 'boolean'),
- """
- value_list = cp.get(section, option)
- return list(filter(None, value_list.split('\n')))
+ # [html]
+ ('html_dir', 'html:directory'),
+ ('extra_css', 'html:extra_css'),
+ ('html_title', 'html:title'),
+ # [xml]
+ ('xml_output', 'xml:output'),
+ ]
+
+ def set_attr_from_config_option(self, cp, attr, where, type_=''):
+ """Set an attribute on self if it exists in the ConfigParser."""
+ section, option = where.split(":")
+ if cp.has_option(section, option):
+ method = getattr(cp, 'get'+type_)
+ setattr(self, attr, method(section, option))
diff --git a/coverage/control.py b/coverage/control.py
index c21d885e..f80e62b6 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -3,21 +3,22 @@
import atexit, os, random, socket, sys
from coverage.annotate import AnnotateReporter
-from coverage.backward import string_class
+from coverage.backward import string_class, iitems
from coverage.codeunit import code_unit_factory, CodeUnit
from coverage.collector import Collector
from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
-from coverage.files import PathAliases, find_python_files
+from coverage.files import PathAliases, find_python_files, prep_patterns
from coverage.html import HtmlReporter
from coverage.misc import CoverageException, bool_or_none, join_regex
+from coverage.misc import file_be_gone
from coverage.results import Analysis, Numbers
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
class coverage(object):
- """Programmatic access to Coverage.
+ """Programmatic access to coverage.py.
To use::
@@ -25,7 +26,7 @@ class coverage(object):
cov = coverage()
cov.start()
- #.. blah blah (run your code) blah blah ..
+ #.. call your code ..
cov.stop()
cov.html_report(directory='covhtml')
@@ -96,10 +97,6 @@ class coverage(object):
self.config.data_file = env_data_file
# 4: from constructor arguments:
- if isinstance(omit, string_class):
- omit = [omit]
- if isinstance(include, string_class):
- include = [include]
self.config.from_args(
data_file=data_file, cover_pylib=cover_pylib, timid=timid,
branch=branch, parallel=bool_or_none(data_suffix),
@@ -125,8 +122,8 @@ class coverage(object):
else:
self.source_pkgs.append(src)
- self.omit = self._prep_patterns(self.config.omit)
- self.include = self._prep_patterns(self.config.include)
+ self.omit = prep_patterns(self.config.omit)
+ self.include = prep_patterns(self.config.include)
self.collector = Collector(
self._should_trace, timid=self.config.timid,
@@ -183,14 +180,6 @@ class coverage(object):
# Set the reporting precision.
Numbers.set_precision(self.config.precision)
- # When tearing down the coverage object, modules can become None.
- # Saving the modules as object attributes avoids problems, but it is
- # quite ad-hoc which modules need to be saved and which references
- # need to use the object attributes.
- self.socket = socket
- self.os = os
- self.random = random
-
def _canonical_dir(self, f):
"""Return the canonical directory of the file `f`."""
return os.path.split(self.file_locator.canonical_filename(f))[0]
@@ -212,9 +201,6 @@ class coverage(object):
should not.
"""
- if os is None:
- return False
-
if filename.startswith('<'):
# Lots of non-file execution is represented with artificial
# filenames like "<string>", "<doctest readme.txt[0]>", or
@@ -281,25 +267,6 @@ class coverage(object):
self._warnings.append(msg)
sys.stderr.write("Coverage.py warning: %s\n" % msg)
- def _prep_patterns(self, patterns):
- """Prepare the file patterns for use in a `FnmatchMatcher`.
-
- If a pattern starts with a wildcard, it is used as a pattern
- as-is. If it does not start with a wildcard, then it is made
- absolute with the current directory.
-
- If `patterns` is None, an empty list is returned.
-
- """
- patterns = patterns or []
- prepped = []
- for p in patterns or []:
- if p.startswith("*") or p.startswith("?"):
- prepped.append(p)
- else:
- prepped.append(self.file_locator.abs_file(p))
- return prepped
-
def _check_for_packages(self):
"""Update the source_match matcher with latest imported packages."""
# Our self.source_pkgs attribute is a list of package names we want to
@@ -354,7 +321,15 @@ class coverage(object):
self.data.read()
def start(self):
- """Start measuring code coverage."""
+ """Start measuring code coverage.
+
+ Coverage measurement actually occurs in functions called after `start`
+ is invoked. Statements in the same scope as `start` won't be measured.
+
+ Once you invoke `start`, you must also call `stop` eventually, or your
+ process might not shut down cleanly.
+
+ """
if self.run_suffix:
# Calling start() means we're running code, so use the run_suffix
# as the data_suffix when we eventually save the data.
@@ -385,7 +360,6 @@ class coverage(object):
def stop(self):
"""Stop measuring code coverage."""
self.collector.stop()
- self._harvest_data()
def erase(self):
"""Erase previously-collected coverage data.
@@ -450,8 +424,8 @@ class coverage(object):
# `save()` at the last minute so that the pid will be correct even
# if the process forks.
data_suffix = "%s.%s.%06d" % (
- self.socket.gethostname(), self.os.getpid(),
- self.random.randint(0, 99999)
+ socket.gethostname(), os.getpid(),
+ random.randint(0, 99999)
)
self._harvest_data()
@@ -498,6 +472,7 @@ class coverage(object):
# Find files that were never executed at all.
for src in self.source:
for py_file in find_python_files(src):
+ py_file = self.file_locator.canonical_filename(py_file)
self.data.touch_file(py_file)
self._harvested = True
@@ -537,6 +512,7 @@ class coverage(object):
Returns an `Analysis` object.
"""
+ self._harvest_data()
if not isinstance(it, CodeUnit):
it = code_unit_factory(it, self.file_locator)[0]
@@ -555,13 +531,16 @@ class coverage(object):
match those patterns will be included in the report. Modules matching
`omit` will not be included in the report.
+ Returns a float, the total percentage covered.
+
"""
+ self._harvest_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
show_missing=show_missing,
)
reporter = SummaryReporter(self, self.config)
- reporter.report(morfs, outfile=file)
+ return reporter.report(morfs, outfile=file)
def annotate(self, morfs=None, directory=None, ignore_errors=None,
omit=None, include=None):
@@ -575,6 +554,7 @@ class coverage(object):
See `coverage.report()` for other arguments.
"""
+ self._harvest_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include
)
@@ -582,7 +562,7 @@ class coverage(object):
reporter.report(morfs, directory=directory)
def html_report(self, morfs=None, directory=None, ignore_errors=None,
- omit=None, include=None, extra_css=None):
+ omit=None, include=None, extra_css=None, title=None):
"""Generate an HTML report.
The HTML is written to `directory`. The file "index.html" is the
@@ -592,15 +572,21 @@ class coverage(object):
`extra_css` is a path to a file of other CSS to apply on the page.
It will be copied into the HTML directory.
+ `title` is a text string (not HTML) to use as the title of the HTML
+ report.
+
See `coverage.report()` for other arguments.
+ Returns a float, the total percentage covered.
+
"""
+ self._harvest_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
- html_dir=directory, extra_css=extra_css,
+ html_dir=directory, extra_css=extra_css, html_title=title,
)
reporter = HtmlReporter(self, self.config)
- reporter.report(morfs)
+ return reporter.report(morfs)
def xml_report(self, morfs=None, outfile=None, ignore_errors=None,
omit=None, include=None):
@@ -613,12 +599,16 @@ class coverage(object):
See `coverage.report()` for other arguments.
+ Returns a float, the total percentage covered.
+
"""
+ self._harvest_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
xml_output=outfile,
)
file_to_close = None
+ delete_file = False
if self.config.xml_output:
if self.config.xml_output == '-':
outfile = sys.stdout
@@ -627,10 +617,15 @@ class coverage(object):
file_to_close = outfile
try:
reporter = XmlReporter(self, self.config)
- reporter.report(morfs, outfile=outfile)
+ return reporter.report(morfs, outfile=outfile)
+ except CoverageException:
+ delete_file = True
+ raise
finally:
if file_to_close:
file_to_close.close()
+ if delete_file:
+ file_be_gone(self.config.xml_output)
def sysinfo(self):
"""Return a list of (key, value) pairs showing internal information."""
@@ -656,8 +651,8 @@ class coverage(object):
('cwd', os.getcwd()),
('path', sys.path),
('environment', [
- ("%s = %s" % (k, v)) for k, v in os.environ.items()
- if re.search("^COV|^PY", k)
+ ("%s = %s" % (k, v)) for k, v in iitems(os.environ)
+ if re.search(r"^COV|^PY", k)
]),
]
return info
diff --git a/coverage/data.py b/coverage/data.py
index 7a8d656f..c86a77f2 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -2,8 +2,9 @@
import os
-from coverage.backward import pickle, sorted # pylint: disable=W0622
+from coverage.backward import iitems, pickle, sorted # pylint: disable=W0622
from coverage.files import PathAliases
+from coverage.misc import file_be_gone
class CoverageData(object):
@@ -60,10 +61,6 @@ class CoverageData(object):
#
self.arcs = {}
- self.os = os
- self.sorted = sorted
- self.pickle = pickle
-
def usefile(self, use_file=True):
"""Set whether or not to use a disk file for data."""
self.use_file = use_file
@@ -93,21 +90,21 @@ class CoverageData(object):
def erase(self):
"""Erase the data, both in this object, and from its file storage."""
if self.use_file:
- if self.filename and os.path.exists(self.filename):
- os.remove(self.filename)
+ if self.filename:
+ file_be_gone(self.filename)
self.lines = {}
self.arcs = {}
def line_data(self):
"""Return the map from filenames to lists of line numbers executed."""
return dict(
- [(f, self.sorted(lmap.keys())) for f, lmap in self.lines.items()]
+ [(f, sorted(lmap.keys())) for f, lmap in iitems(self.lines)]
)
def arc_data(self):
"""Return the map from filenames to lists of line number pairs."""
return dict(
- [(f, self.sorted(amap.keys())) for f, amap in self.arcs.items()]
+ [(f, sorted(amap.keys())) for f, amap in iitems(self.arcs)]
)
def write_file(self, filename):
@@ -127,7 +124,7 @@ class CoverageData(object):
# Write the pickle to the file.
fdata = open(filename, 'wb')
try:
- self.pickle.dump(data, fdata, 2)
+ pickle.dump(data, fdata, 2)
finally:
fdata.close()
@@ -159,12 +156,12 @@ class CoverageData(object):
# Unpack the 'lines' item.
lines = dict([
(f, dict.fromkeys(linenos, None))
- for f, linenos in data.get('lines', {}).items()
+ for f, linenos in iitems(data.get('lines', {}))
])
# Unpack the 'arcs' item.
arcs = dict([
(f, dict.fromkeys(arcpairs, None))
- for f, arcpairs in data.get('arcs', {}).items()
+ for f, arcpairs in iitems(data.get('arcs', {}))
])
except Exception:
pass
@@ -187,10 +184,10 @@ class CoverageData(object):
if f.startswith(localdot):
full_path = os.path.join(data_dir, f)
new_lines, new_arcs = self._read_file(full_path)
- for filename, file_data in new_lines.items():
+ for filename, file_data in iitems(new_lines):
filename = aliases.map(filename)
self.lines.setdefault(filename, {}).update(file_data)
- for filename, file_data in new_arcs.items():
+ for filename, file_data in iitems(new_arcs):
filename = aliases.map(filename)
self.arcs.setdefault(filename, {}).update(file_data)
if f != local:
@@ -202,7 +199,7 @@ class CoverageData(object):
`line_data` is { filename: { lineno: None, ... }, ...}
"""
- for filename, linenos in line_data.items():
+ for filename, linenos in iitems(line_data):
self.lines.setdefault(filename, {}).update(linenos)
def add_arc_data(self, arc_data):
@@ -211,7 +208,7 @@ class CoverageData(object):
`arc_data` is { filename: { (l1,l2): None, ... }, ...}
"""
- for filename, arcs in arc_data.items():
+ for filename, arcs in iitems(arc_data):
self.arcs.setdefault(filename, {}).update(arcs)
def touch_file(self, filename):
@@ -252,8 +249,8 @@ class CoverageData(object):
if fullpath:
filename_fn = lambda f: f
else:
- filename_fn = self.os.path.basename
- for filename, lines in self.lines.items():
+ filename_fn = os.path.basename
+ for filename, lines in iitems(self.lines):
summ[filename_fn(filename)] = len(lines)
return summ
diff --git a/coverage/execfile.py b/coverage/execfile.py
index 3283a3f7..587c2d3c 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -110,7 +110,7 @@ def run_python_file(filename, args, package=None):
# We have the source. `compile` still needs the last line to be clean,
# so make sure it is, then compile a code object from it.
- if source[-1] != '\n':
+ if not source or source[-1] != '\n':
source += '\n'
code = compile(source, filename, "exec")
diff --git a/coverage/files.py b/coverage/files.py
index 13f43930..40af7bf7 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -2,23 +2,19 @@
from coverage.backward import to_string
from coverage.misc import CoverageException
-import fnmatch, os, re, sys
+import fnmatch, os, os.path, re, sys
class FileLocator(object):
"""Understand how filenames work."""
def __init__(self):
# The absolute path to our current directory.
- self.relative_dir = self.abs_file(os.curdir) + os.sep
+ self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep)
# Cache of results of calling the canonical_filename() method, to
# avoid duplicating work.
self.canonical_filename_cache = {}
- def abs_file(self, filename):
- """Return the absolute normalized form of `filename`."""
- return os.path.normcase(os.path.abspath(os.path.realpath(filename)))
-
def relative_filename(self, filename):
"""Return the relative form of `filename`.
@@ -26,8 +22,9 @@ class FileLocator(object):
`FileLocator` was constructed.
"""
- if filename.startswith(self.relative_dir):
- filename = filename.replace(self.relative_dir, "", 1)
+ fnorm = os.path.normcase(filename)
+ if fnorm.startswith(self.relative_dir):
+ filename = filename[len(self.relative_dir):]
return filename
def canonical_filename(self, filename):
@@ -49,7 +46,7 @@ class FileLocator(object):
if os.path.exists(g):
f = g
break
- cf = self.abs_file(f)
+ cf = abs_file(f)
self.canonical_filename_cache[filename] = cf
return self.canonical_filename_cache[filename]
@@ -78,6 +75,72 @@ class FileLocator(object):
return None
+if sys.platform == 'win32':
+
+ def actual_path(path):
+ """Get the actual path of `path`, including the correct case."""
+ if path in actual_path.cache:
+ return actual_path.cache[path]
+
+ head, tail = os.path.split(path)
+ if not tail:
+ actpath = head
+ elif not head:
+ actpath = tail
+ else:
+ head = actual_path(head)
+ if head in actual_path.list_cache:
+ files = actual_path.list_cache[head]
+ else:
+ try:
+ files = os.listdir(head)
+ except OSError:
+ files = []
+ actual_path.list_cache[head] = files
+ normtail = os.path.normcase(tail)
+ for f in files:
+ if os.path.normcase(f) == normtail:
+ tail = f
+ break
+ actpath = os.path.join(head, tail)
+ actual_path.cache[path] = actpath
+ return actpath
+
+ actual_path.cache = {}
+ actual_path.list_cache = {}
+
+else:
+ def actual_path(filename):
+ """The actual path for non-Windows platforms."""
+ return filename
+
+def abs_file(filename):
+ """Return the absolute normalized form of `filename`."""
+ path = os.path.abspath(os.path.realpath(filename))
+ path = actual_path(path)
+ return path
+
+
+def prep_patterns(patterns):
+ """Prepare the file patterns for use in a `FnmatchMatcher`.
+
+ If a pattern starts with a wildcard, it is used as a pattern
+ as-is. If it does not start with a wildcard, then it is made
+ absolute with the current directory.
+
+ If `patterns` is None, an empty list is returned.
+
+ """
+ patterns = patterns or []
+ prepped = []
+ for p in patterns or []:
+ if p.startswith("*") or p.startswith("?"):
+ prepped.append(p)
+ else:
+ prepped.append(abs_file(p))
+ return prepped
+
+
class TreeMatcher(object):
"""A matcher for files in a tree."""
def __init__(self, directories):
@@ -175,7 +238,7 @@ class PathAliases(object):
# either separator.
regex_pat = regex_pat.replace(r"\/", r"[\\/]")
# We want case-insensitive matching, so add that flag.
- regex = re.compile("(?i)" + regex_pat)
+ regex = re.compile(r"(?i)" + regex_pat)
# Normalize the result: it must end with a path separator.
result_sep = sep(result)
diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py
index ad350bc0..6a258d67 100644
--- a/coverage/fullcoverage/encodings.py
+++ b/coverage/fullcoverage/encodings.py
@@ -37,6 +37,14 @@ class FullCoverageTracer(object):
sys.settrace(FullCoverageTracer().fullcoverage_trace)
+# In coverage/files.py is actual_filename(), which uses glob.glob. I don't
+# understand why, but that use of glob borks everything if fullcoverage is in
+# effect. So here we make an ugly hail-mary pass to switch off glob.glob over
+# there. This means when using fullcoverage, Windows path names will not be
+# their actual case.
+
+#sys.fullcoverage = True
+
# Finally, remove our own directory from sys.path; remove ourselves from
# sys.modules; and re-import "encodings", which will be the real package
# this time. Note that the delete from sys.modules dictionary has to
diff --git a/coverage/html.py b/coverage/html.py
index 34bf6a61..6a6c648e 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -7,6 +7,7 @@ from coverage.backward import pickle
from coverage.misc import CoverageException, Hasher
from coverage.phystokens import source_token_lines, source_encoding
from coverage.report import Reporter
+from coverage.results import Numbers
from coverage.templite import Templite
# Disable pylint msg W0612, because a bunch of variables look unused, but
@@ -46,6 +47,7 @@ class HtmlReporter(Reporter):
self.directory = None
self.template_globals = {
'escape': escape,
+ 'title': self.config.html_title,
'__url__': coverage.__url__,
'__version__': coverage.__version__,
}
@@ -59,6 +61,7 @@ class HtmlReporter(Reporter):
self.arcs = self.coverage.data.has_arcs()
self.status = HtmlStatus()
self.extra_css = None
+ self.totals = Numbers()
def report(self, morfs):
"""Generate an HTML report for `morfs`.
@@ -94,6 +97,8 @@ class HtmlReporter(Reporter):
self.make_local_static_report_files()
+ return self.totals.pc_covered
+
def make_local_static_report_files(self):
"""Make local instances of static files for HTML report."""
# The files we provide must always be copied.
@@ -245,12 +250,15 @@ class HtmlReporter(Reporter):
files = self.files
arcs = self.arcs
- totals = sum([f['nums'] for f in files])
+ self.totals = totals = sum([f['nums'] for f in files])
extra_css = self.extra_css
+ html = index_tmpl.render(locals())
+ if sys.version_info < (3, 0):
+ html = html.decode("utf-8")
self.write_html(
os.path.join(self.directory, "index.html"),
- index_tmpl.render(locals())
+ html
)
# Write the latest hashes for next time.
@@ -358,5 +366,5 @@ def spaceless(html):
Get rid of some.
"""
- html = re.sub(">\s+<p ", ">\n<p ", html)
+ html = re.sub(r">\s+<p ", ">\n<p ", html)
return html
diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js
index 5906e653..b24006d2 100644
--- a/coverage/htmlfiles/coverage_html.js
+++ b/coverage/htmlfiles/coverage_html.js
@@ -374,4 +374,3 @@ coverage.scroll_window = function (to_pos) {
coverage.finish_scrolling = function () {
$("html,body").stop(true, true);
};
-
diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html
index c6d9eec0..5a7c8c2e 100644
--- a/coverage/htmlfiles/index.html
+++ b/coverage/htmlfiles/index.html
@@ -2,7 +2,7 @@
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
- <title>Coverage report</title>
+ <title>{{ title|escape }}</title>
<link rel='stylesheet' href='style.css' type='text/css'>
{% if extra_css %}
<link rel='stylesheet' href='{{ extra_css }}' type='text/css'>
@@ -19,7 +19,7 @@
<div id='header'>
<div class='content'>
- <h1>Coverage report:
+ <h1>{{ title|escape }}:
<span class='pc_cov'>{{totals.pc_covered_str}}%</span>
</h1>
<img id='keyboard_icon' src='keybd_closed.png'>
diff --git a/coverage/misc.py b/coverage/misc.py
index fd9be857..3ed854a7 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -1,6 +1,10 @@
"""Miscellaneous stuff for Coverage."""
+import errno
import inspect
+import os
+import sys
+
from coverage.backward import md5, sorted # pylint: disable=W0622
from coverage.backward import string_class, to_bytes
@@ -83,6 +87,16 @@ def join_regex(regexes):
return ""
+def file_be_gone(path):
+ """Remove a file, and don't get annoyed if it doesn't exist."""
+ try:
+ os.remove(path)
+ except OSError:
+ _, e, _ = sys.exc_info()
+ if e.errno != errno.ENOENT:
+ raise
+
+
class Hasher(object):
"""Hashes Python data into md5."""
def __init__(self):
diff --git a/coverage/phystokens.py b/coverage/phystokens.py
index 3beebab1..166020e1 100644
--- a/coverage/phystokens.py
+++ b/coverage/phystokens.py
@@ -119,7 +119,7 @@ def source_encoding(source):
# This is mostly code adapted from Py3.2's tokenize module.
- cookie_re = re.compile("coding[:=]\s*([-\w.]+)")
+ cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)")
# Do this so the detect_encode code we copied will work.
readline = iter(source.splitlines()).next
diff --git a/coverage/report.py b/coverage/report.py
index e351340f..34f44422 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -2,6 +2,7 @@
import fnmatch, os
from coverage.codeunit import code_unit_factory
+from coverage.files import prep_patterns
from coverage.misc import CoverageException, NoSource, NotPython
class Reporter(object):
@@ -35,7 +36,7 @@ class Reporter(object):
self.code_units = code_unit_factory(morfs, file_locator)
if self.config.include:
- patterns = [file_locator.abs_file(p) for p in self.config.include]
+ patterns = prep_patterns(self.config.include)
filtered = []
for cu in self.code_units:
for pattern in patterns:
@@ -45,7 +46,7 @@ class Reporter(object):
self.code_units = filtered
if self.config.omit:
- patterns = [file_locator.abs_file(p) for p in self.config.omit]
+ patterns = prep_patterns(self.config.omit)
filtered = []
for cu in self.code_units:
for pattern in patterns:
diff --git a/coverage/results.py b/coverage/results.py
index d7e2a9d1..b39966ca 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -2,7 +2,7 @@
import os
-from coverage.backward import set, sorted # pylint: disable=W0622
+from coverage.backward import iitems, set, sorted # pylint: disable=W0622
from coverage.misc import format_lines, join_regex, NoSource
from coverage.parser import CodeParser
@@ -42,7 +42,7 @@ class Analysis(object):
n_branches = self.total_branches()
mba = self.missing_branch_arcs()
n_missing_branches = sum(
- [len(v) for k,v in mba.items() if k not in self.missing]
+ [len(v) for k,v in iitems(mba) if k not in self.missing]
)
else:
n_branches = n_missing_branches = 0
@@ -109,7 +109,7 @@ class Analysis(object):
def branch_lines(self):
"""Returns a list of line numbers that have more than one exit."""
exit_counts = self.parser.exit_counts()
- return [l1 for l1,count in exit_counts.items() if count > 1]
+ return [l1 for l1,count in iitems(exit_counts) if count > 1]
def total_branches(self):
"""How many total branches are there?"""
diff --git a/coverage/summary.py b/coverage/summary.py
index c8fa5be4..03648e5f 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -82,3 +82,5 @@ class SummaryReporter(Reporter):
if self.config.show_missing:
args += ("",)
outfile.write(fmt_coverage % args)
+
+ return total.pc_covered
diff --git a/coverage/tracer.c b/coverage/tracer.c
index c17dc03c..97dd113b 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -235,10 +235,8 @@ CTracer_record_pair(CTracer *self, int l1, int l2)
{
int ret = RET_OK;
- PyObject * t = PyTuple_New(2);
+ PyObject * t = Py_BuildValue("(ii)", l1, l2);
if (t != NULL) {
- PyTuple_SET_ITEM(t, 0, MyInt_FromLong(l1));
- PyTuple_SET_ITEM(t, 1, MyInt_FromLong(l2));
if (PyDict_SetItem(self->cur_file_data, t, Py_None) < 0) {
STATS( self->stats.errors++; )
ret = RET_ERROR;
@@ -730,4 +728,3 @@ inittracer(void)
}
#endif /* Py3k */
-
diff --git a/coverage/version.py b/coverage/version.py
new file mode 100644
index 00000000..5a8a2d6e
--- /dev/null
+++ b/coverage/version.py
@@ -0,0 +1,9 @@
+"""The version and URL for coverage.py"""
+# This file is exec'ed in setup.py, don't import anything!
+
+__version__ = "3.5.4b1" # see detailed history in CHANGES.txt
+
+__url__ = "http://nedbatchelder.com/code/coverage"
+if max(__version__).isalpha():
+ # For pre-releases, use a version-specific URL.
+ __url__ += "/" + __version__
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index 03f910c8..e062ceee 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -84,6 +84,9 @@ class XmlReporter(Reporter):
# Use the DOM to write the output file.
outfile.write(self.xml_out.toprettyxml())
+ # Return the total percentage.
+ return 100.0 * (lhits_tot + bhits_tot) / (lnum_tot + bnum_tot)
+
def xml_file(self, cu, analysis):
"""Add to the XML report for a single file."""