summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/__init__.py6
-rw-r--r--coverage/backward.py31
-rw-r--r--coverage/bytecode.py2
-rw-r--r--coverage/cmdline.py101
-rw-r--r--coverage/collector.py2
-rw-r--r--coverage/config.py95
-rw-r--r--coverage/control.py189
-rw-r--r--coverage/data.py19
-rw-r--r--coverage/execfile.py28
-rw-r--r--coverage/files.py7
-rw-r--r--coverage/html.py32
-rw-r--r--coverage/htmlfiles/coverage_html.js21
-rw-r--r--coverage/htmlfiles/index.html161
-rw-r--r--coverage/htmlfiles/pyfile.html106
-rw-r--r--coverage/htmlfiles/style.css4
-rw-r--r--coverage/misc.py16
-rw-r--r--coverage/parser.py17
-rw-r--r--coverage/phystokens.py3
-rw-r--r--coverage/summary.py2
-rw-r--r--coverage/templite.py8
20 files changed, 567 insertions, 283 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py
index 4db12408..9fd4a8d2 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -5,11 +5,11 @@ http://nedbatchelder.com/code/coverage
"""
-__version__ = "3.2b4" # see detailed history in CHANGES.txt
+__version__ = "3.3.2a1" # see detailed history in CHANGES.txt
__url__ = "http://nedbatchelder.com/code/coverage"
-from coverage.control import coverage
+from coverage.control import coverage, process_startup
from coverage.data import CoverageData
from coverage.cmdline import main, CoverageScript
from coverage.misc import CoverageException
@@ -55,7 +55,7 @@ annotate = _singleton_method('annotate')
# COPYRIGHT AND LICENSE
#
# Copyright 2001 Gareth Rees. All rights reserved.
-# Copyright 2004-2009 Ned Batchelder. All rights reserved.
+# Copyright 2004-2010 Ned Batchelder. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
diff --git a/coverage/backward.py b/coverage/backward.py
index 66cfbb96..425bcc6e 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -14,7 +14,6 @@ try:
except NameError:
from sets import Set as set
-
# Python 2.3 doesn't have `sorted`.
try:
sorted = sorted
@@ -26,7 +25,6 @@ except NameError:
return lst
# Pythons 2 and 3 differ on where to get StringIO
-
try:
from cStringIO import StringIO
BytesIO = StringIO
@@ -34,39 +32,42 @@ except ImportError:
from io import StringIO, BytesIO
# What's a string called?
-
try:
string_class = basestring
except NameError:
string_class = str
# Where do pickles come from?
-
try:
import cPickle as pickle
except ImportError:
import pickle
# range or xrange?
-
try:
range = xrange
except NameError:
range = range
# Exec is a statement in Py2, a function in Py3
-
-if sys.hexversion > 0x03000000:
- def exec_function(source, filename, global_map):
+if sys.version_info >= (3, 0):
+ def exec_code_object(code, global_map):
"""A wrapper around exec()."""
- exec(compile(source, filename, "exec"), global_map)
+ exec(code, global_map)
else:
# OK, this is pretty gross. In Py2, exec was a statement, but that will
# be a syntax error if we try to put it in a Py3 file, even if it is never
# executed. So hide it inside an evaluated string literal instead.
- eval(compile("""\
-def exec_function(source, filename, global_map):
- exec compile(source, filename, "exec") in global_map
-""",
- "<exec_function>", "exec"
- ))
+ eval(
+ compile(
+ "def exec_code_object(code, global_map):\n"
+ " exec code in global_map\n",
+ "<exec_function>", "exec"
+ )
+ )
+
+# ConfigParser was renamed to the more-standard configparser
+try:
+ import configparser
+except ImportError:
+ import ConfigParser as configparser
diff --git a/coverage/bytecode.py b/coverage/bytecode.py
index ac280342..ab522d6c 100644
--- a/coverage/bytecode.py
+++ b/coverage/bytecode.py
@@ -22,7 +22,7 @@ class ByteCodes(object):
self.code = code
self.offset = 0
- if sys.hexversion > 0x03000000:
+ if sys.version_info >= (3, 0):
def __getitem__(self, i):
return self.code[i]
else:
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 27c28d48..9e15074b 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -1,9 +1,10 @@
"""Command-line support for Coverage."""
-import optparse, re, sys
+import optparse, re, sys, traceback
+from coverage.backward import sorted # pylint: disable-msg=W0622
from coverage.execfile import run_python_file
-from coverage.misc import CoverageException
+from coverage.misc import CoverageException, ExceptionDuringRun
class Opts(object):
@@ -60,8 +61,13 @@ class Opts(object):
)
parallel_mode = optparse.Option(
'-p', '--parallel-mode', action='store_true',
- help="Include the machine name and process id in the .coverage "
- "data file name."
+ help="Append the machine name, process id and random number to the "
+ ".coverage data file name to simplify collecting data from "
+ "many processes."
+ )
+ rcfile = optparse.Option(
+ '', '--rcfile', action='store',
+ help="Specify configuration file. Defaults to '.coveragerc'"
)
timid = optparse.Option(
'', '--timid', action='store_true',
@@ -95,6 +101,7 @@ class CoverageOptionParser(optparse.OptionParser, object):
omit=None,
parallel_mode=None,
pylib=None,
+ rcfile=True,
show_missing=None,
timid=None,
erase_first=None,
@@ -102,7 +109,11 @@ class CoverageOptionParser(optparse.OptionParser, object):
)
self.disable_interspersed_args()
- self.help_fn = lambda: None
+ self.help_fn = self.help_noop
+
+ def help_noop(self, error=None, topic=None, parser=None):
+ """No-op help function."""
+ pass
class OptionParserError(Exception):
"""Used to stop the optparse error handler ending the process."""
@@ -197,6 +208,10 @@ class CmdOptionParser(CoverageOptionParser):
# results, and they will compare equal to objects.
return (other == "<CmdOptionParser:%s>" % self.cmd)
+GLOBAL_ARGS = [
+ Opts.rcfile,
+ Opts.help,
+ ]
CMDS = {
'annotate': CmdOptionParser("annotate",
@@ -204,40 +219,21 @@ CMDS = {
Opts.directory,
Opts.ignore_errors,
Opts.omit,
- Opts.help,
- ],
+ ] + GLOBAL_ARGS,
usage = "[options] [modules]",
description = "Make annotated copies of the given files, marking "
"statements that are executed with > and statements that are "
"missed with !."
),
- 'help': CmdOptionParser("help", [Opts.help],
- usage = "[command]",
- description = "Describe how to use coverage.py"
- ),
-
- 'html': CmdOptionParser("html",
- [
- Opts.directory,
- Opts.ignore_errors,
- Opts.omit,
- Opts.help,
- ],
- usage = "[options] [modules]",
- description = "Create an HTML report of the coverage of the files. "
- "Each file gets its own page, with the source decorated to show "
- "executed, excluded, and missed lines."
- ),
-
- 'combine': CmdOptionParser("combine", [Opts.help],
+ 'combine': CmdOptionParser("combine", GLOBAL_ARGS,
usage = " ",
description = "Combine data from multiple coverage files collected "
"with 'run -p'. The combined results are written to a single "
"file representing the union of the data."
),
- 'debug': CmdOptionParser("debug", [Opts.help],
+ 'debug': CmdOptionParser("debug", GLOBAL_ARGS,
usage = "<topic>",
description = "Display information on the internals of coverage.py, "
"for diagnosing problems. "
@@ -245,18 +241,34 @@ CMDS = {
"or 'sys' to show installation information."
),
- 'erase': CmdOptionParser("erase", [Opts.help],
+ 'erase': CmdOptionParser("erase", GLOBAL_ARGS,
usage = " ",
description = "Erase previously collected coverage data."
),
+ 'help': CmdOptionParser("help", GLOBAL_ARGS,
+ usage = "[command]",
+ description = "Describe how to use coverage.py"
+ ),
+
+ 'html': CmdOptionParser("html",
+ [
+ Opts.directory,
+ Opts.ignore_errors,
+ Opts.omit,
+ ] + GLOBAL_ARGS,
+ usage = "[options] [modules]",
+ description = "Create an HTML report of the coverage of the files. "
+ "Each file gets its own page, with the source decorated to show "
+ "executed, excluded, and missed lines."
+ ),
+
'report': CmdOptionParser("report",
[
Opts.ignore_errors,
Opts.omit,
Opts.show_missing,
- Opts.help,
- ],
+ ] + GLOBAL_ARGS,
usage = "[options] [modules]",
description = "Report coverage statistics on modules."
),
@@ -268,8 +280,7 @@ CMDS = {
Opts.pylib,
Opts.parallel_mode,
Opts.timid,
- Opts.help,
- ],
+ ] + GLOBAL_ARGS,
defaults = {'erase_first': True},
cmd = "run",
usage = "[options] <pyfile> [program options]",
@@ -281,8 +292,7 @@ CMDS = {
Opts.ignore_errors,
Opts.omit,
Opts.output_xml,
- Opts.help,
- ],
+ ] + GLOBAL_ARGS,
cmd = "xml",
defaults = {'outfile': 'coverage.xml'},
usage = "[options] [modules]",
@@ -418,10 +428,11 @@ class CoverageScript(object):
# Do something.
self.coverage = self.covpkg.coverage(
- data_suffix = bool(options.parallel_mode),
+ data_suffix = options.parallel_mode,
cover_pylib = options.pylib,
timid = options.timid,
branch = options.branch,
+ config_file = options.rcfile,
)
if 'debug' in options.actions:
@@ -496,8 +507,6 @@ class CoverageScript(object):
directory=options.directory, **report_args)
if 'xml' in options.actions:
outfile = options.outfile
- if outfile == '-':
- outfile = None
self.coverage.xml_report(outfile=outfile, **report_args)
return OK
@@ -580,16 +589,30 @@ Coverage.py, version %(__version__)s. %(__url__)s
"""
-def main():
+def main(argv=None):
"""The main entrypoint to Coverage.
This is installed as the script entrypoint.
"""
+ if argv is None:
+ argv = sys.argv[1:]
try:
- status = CoverageScript().command_line(sys.argv[1:])
+ status = CoverageScript().command_line(argv)
+ except ExceptionDuringRun:
+ # An exception was caught while running the product code. The
+ # sys.exc_info() return tuple is packed into an ExceptionDuringRun
+ # exception.
+ _, err, _ = sys.exc_info()
+ traceback.print_exception(*err.args)
+ status = ERR
except CoverageException:
+ # A controlled error inside coverage.py: print the message to the user.
_, err, _ = sys.exc_info()
print(err)
status = ERR
+ except SystemExit:
+ # The user called `sys.exit()`. Exit with their status code.
+ _, err, _ = sys.exc_info()
+ status = err.args[0]
return status
diff --git a/coverage/collector.py b/coverage/collector.py
index 29dddf6b..1837aae2 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -251,7 +251,7 @@ class Collector(object):
line_data = {}
for f, arcs in self.data.items():
line_data[f] = ldf = {}
- for l1, _ in arcs:
+ for l1, _ in list(arcs.keys()):
if l1:
ldf[l1] = None
return line_data
diff --git a/coverage/config.py b/coverage/config.py
new file mode 100644
index 00000000..8f508f28
--- /dev/null
+++ b/coverage/config.py
@@ -0,0 +1,95 @@
+"""Config file for coverage.py"""
+
+import os
+from coverage.backward import configparser # pylint: disable-msg=W0622
+
+
+class CoverageConfig(object):
+ """Coverage.py configuration.
+
+ The attributes of this class are the various settings that control the
+ operation of coverage.py.
+
+ """
+
+ def __init__(self):
+ """Initialize the configuration attributes to their defaults."""
+ # Defaults for [run]
+ self.branch = False
+ self.cover_pylib = False
+ self.data_file = ".coverage"
+ self.parallel = False
+ self.timid = False
+
+ # Defaults for [report]
+ self.exclude_list = ['(?i)# *pragma[: ]*no *cover']
+ self.ignore_errors = False
+ self.omit_prefixes = None
+
+ # Defaults for [html]
+ self.html_dir = "htmlcov"
+
+ # Defaults for [xml]
+ self.xml_output = "coverage.xml"
+
+ def from_environment(self, env_var):
+ """Read configuration from the `env_var` environment variable."""
+ # Timidity: for nose users, read an environment variable. This is a
+ # cheap hack, since the rest of the command line arguments aren't
+ # recognized, but it solves some users' problems.
+ env = os.environ.get(env_var, '')
+ if env:
+ self.timid = ('--timid' in env)
+
+ def from_args(self, **kwargs):
+ """Read config values from `kwargs`."""
+ for k, v in kwargs.items():
+ if v is not None:
+ setattr(self, k, v)
+
+ def from_file(self, *files):
+ """Read configuration from .rc files.
+
+ Each argument in `files` is a file name to read.
+
+ """
+ cp = configparser.RawConfigParser()
+ cp.read(files)
+
+ # [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', 'parallel'):
+ self.parallel = cp.getboolean('run', 'parallel')
+ if cp.has_option('run', 'timid'):
+ self.timid = cp.getboolean('run', 'timid')
+
+ # [report]
+ if cp.has_option('report', 'exclude_lines'):
+ # exclude_lines is a list of lines, leave out the blank ones.
+ exclude_list = cp.get('report', 'exclude_lines')
+ self.exclude_list = list(filter(None, exclude_list.split('\n')))
+ if cp.has_option('report', 'ignore_errors'):
+ self.ignore_errors = cp.getboolean('report', 'ignore_errors')
+ if cp.has_option('report', 'omit'):
+ # omit is a list of prefixes, on separate lines, or separated by
+ # commas.
+ omit_list = cp.get('report', 'omit')
+ self.omit_prefixes = []
+ for omit_line in omit_list.split('\n'):
+ for omit in omit_line.split(','):
+ omit = omit.strip()
+ if omit:
+ self.omit_prefixes.append(omit)
+
+ # [html]
+ if cp.has_option('html', 'directory'):
+ self.html_dir = cp.get('html', 'directory')
+
+ # [xml]
+ if cp.has_option('xml', 'output'):
+ self.xml_output = cp.get('xml', 'output')
diff --git a/coverage/control.py b/coverage/control.py
index 15bbe982..1456178a 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -1,14 +1,16 @@
"""Core control stuff for Coverage."""
-import atexit, os, socket
+import atexit, os, random, socket, sys
from coverage.annotate import AnnotateReporter
-from coverage.backward import string_class # pylint: disable-msg=W0622
+from coverage.backward import string_class
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
from coverage.html import HtmlReporter
+from coverage.misc import bool_or_none
from coverage.results import Analysis
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
@@ -28,13 +30,13 @@ class coverage(object):
"""
- def __init__(self, data_file=None, data_suffix=False, cover_pylib=False,
- auto_data=False, timid=False, branch=False):
+ def __init__(self, data_file=None, data_suffix=None, cover_pylib=None,
+ auto_data=False, timid=None, branch=None, config_file=True):
"""
`data_file` is the base name of the data file to use, defaulting to
- ".coverage". `data_suffix` is appended to `data_file` to create the
- final file name. If `data_suffix` is simply True, then a suffix is
- created with the machine and process identity included.
+ ".coverage". `data_suffix` is appended (with a dot) to `data_file` to
+ create the final file name. If `data_suffix` is simply True, then a
+ suffix is created with the machine and process identity included.
`cover_pylib` is a boolean determining whether Python code installed
with the Python interpreter is measured. This includes the Python
@@ -51,44 +53,67 @@ class coverage(object):
If `branch` is true, then branch coverage will be measured in addition
to the usual statement coverage.
+ `config_file` determines what config file to read. If it is a string,
+ it is the name of the config file to read. If it is True, then a
+ standard file is read (".coveragerc"). If it is False, then no file is
+ read.
+
"""
from coverage import __version__
- self.cover_pylib = cover_pylib
+ # Build our configuration from a number of sources:
+ # 1: defaults:
+ self.config = CoverageConfig()
+
+ # 2: from the coveragerc file:
+ if config_file:
+ if config_file is True:
+ config_file = ".coveragerc"
+ self.config.from_file(config_file)
+
+ # 3: from environment variables:
+ self.config.from_environment('COVERAGE_OPTIONS')
+ env_data_file = os.environ.get('COVERAGE_FILE')
+ if env_data_file:
+ self.config.data_file = env_data_file
+
+ # 4: from constructor arguments:
+ self.config.from_args(
+ data_file=data_file, cover_pylib=cover_pylib, timid=timid,
+ branch=branch, parallel=bool_or_none(data_suffix)
+ )
+
self.auto_data = auto_data
self.atexit_registered = False
self.exclude_re = ""
- self.exclude_list = []
+ self._compile_exclude()
self.file_locator = FileLocator()
- # Timidity: for nose users, read an environment variable. This is a
- # cheap hack, since the rest of the command line arguments aren't
- # recognized, but it solves some users' problems.
- timid = timid or ('--timid' in os.environ.get('COVERAGE_OPTIONS', ''))
self.collector = Collector(
- self._should_trace, timid=timid, branch=branch
+ self._should_trace, timid=self.config.timid,
+ branch=self.config.branch
)
# Create the data file.
- if data_suffix:
+ if data_suffix or self.config.parallel:
if not isinstance(data_suffix, string_class):
- # if data_suffix=True, use .machinename.pid
- data_suffix = ".%s.%s" % (socket.gethostname(), os.getpid())
+ # if data_suffix=True, use .machinename.pid.random
+ data_suffix = "%s.%s.%06d" % (
+ socket.gethostname(), os.getpid(), random.randint(0, 99999)
+ )
else:
data_suffix = None
+ self.run_suffix = data_suffix
self.data = CoverageData(
- basename=data_file, suffix=data_suffix,
+ basename=self.config.data_file,
collector="coverage v%s" % __version__
)
- # The default exclude pattern.
- self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]')
-
# The prefix for files considered "installed with the interpreter".
- if not self.cover_pylib:
+ if not self.config.cover_pylib:
# Look at where the "os" module is located. That's the indication
# for "installed with the interpreter".
os_file = self.file_locator.canonical_filename(os.__file__)
@@ -131,7 +156,7 @@ class coverage(object):
# If we aren't supposed to trace installed code, then check if this is
# near the Python standard library and skip it if so.
- if not self.cover_pylib:
+ if not self.config.cover_pylib:
if canonical.startswith(self.pylib_prefix):
return False
@@ -166,6 +191,14 @@ class coverage(object):
def start(self):
"""Start measuring code coverage."""
+ if self.run_suffix:
+ # If the .coveragerc file specifies parallel=True, then we need to
+ # remake the data file for collection, with a suffix.
+ from coverage import __version__
+ self.data = CoverageData(
+ basename=self.config.data_file, suffix=self.run_suffix,
+ collector="coverage v%s" % __version__
+ )
if self.auto_data:
self.load()
# Save coverage data when Python exits.
@@ -191,7 +224,7 @@ class coverage(object):
def clear_exclude(self):
"""Clear the exclude list."""
- self.exclude_list = []
+ self.config.exclude_list = []
self.exclude_re = ""
def exclude(self, regex):
@@ -203,12 +236,16 @@ class coverage(object):
Matching any of the regexes excludes a source line.
"""
- self.exclude_list.append(regex)
- self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")"
+ self.config.exclude_list.append(regex)
+ self._compile_exclude()
+
+ def _compile_exclude(self):
+ """Build the internal usable form of the exclude list."""
+ self.exclude_re = "(" + ")|(".join(self.config.exclude_list) + ")"
def get_exclude_list(self):
"""Return the list of excluded regex patterns."""
- return self.exclude_list
+ return self.config.exclude_list
def save(self):
"""Save the collected coverage data to the data file."""
@@ -271,7 +308,7 @@ class coverage(object):
return Analysis(self, it)
- def report(self, morfs=None, show_missing=True, ignore_errors=False,
+ def report(self, morfs=None, show_missing=True, ignore_errors=None,
file=None, omit_prefixes=None): # pylint: disable-msg=W0622
"""Write a summary report to `file`.
@@ -279,10 +316,18 @@ class coverage(object):
statements, missing statements, and a list of lines missed.
"""
- reporter = SummaryReporter(self, show_missing, ignore_errors)
- reporter.report(morfs, outfile=file, omit_prefixes=omit_prefixes)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes
+ )
+ reporter = SummaryReporter(
+ self, show_missing, self.config.ignore_errors
+ )
+ reporter.report(
+ morfs, outfile=file, omit_prefixes=self.config.omit_prefixes
+ )
- def annotate(self, morfs=None, directory=None, ignore_errors=False,
+ def annotate(self, morfs=None, directory=None, ignore_errors=None,
omit_prefixes=None):
"""Annotate a list of modules.
@@ -292,40 +337,67 @@ class coverage(object):
excluded lines have "-", and missing lines have "!".
"""
- reporter = AnnotateReporter(self, ignore_errors)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes
+ )
+ reporter = AnnotateReporter(self, self.config.ignore_errors)
reporter.report(
- morfs, directory=directory, omit_prefixes=omit_prefixes)
+ morfs, directory=directory, omit_prefixes=self.config.omit_prefixes
+ )
- def html_report(self, morfs=None, directory=None, ignore_errors=False,
+ def html_report(self, morfs=None, directory=None, ignore_errors=None,
omit_prefixes=None):
"""Generate an HTML report.
"""
- reporter = HtmlReporter(self, ignore_errors)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ html_dir=directory,
+ )
+ reporter = HtmlReporter(self, self.config.ignore_errors)
reporter.report(
- morfs, directory=directory, omit_prefixes=omit_prefixes)
+ morfs, directory=self.config.html_dir,
+ omit_prefixes=self.config.omit_prefixes
+ )
- def xml_report(self, morfs=None, outfile=None, ignore_errors=False,
+ def xml_report(self, morfs=None, outfile=None, ignore_errors=None,
omit_prefixes=None):
"""Generate an XML report of coverage results.
The report is compatible with Cobertura reports.
+ Each module in `morfs` is included in the report. `outfile` is the
+ path to write the file to, "-" will write to stdout.
+
"""
- if outfile:
- outfile = open(outfile, "w")
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ xml_output=outfile,
+ )
+ file_to_close = None
+ if self.config.xml_output:
+ if self.config.xml_output == '-':
+ outfile = sys.stdout
+ else:
+ outfile = open(self.config.xml_output, "w")
+ file_to_close = outfile
try:
- reporter = XmlReporter(self, ignore_errors)
+ reporter = XmlReporter(self, self.config.ignore_errors)
reporter.report(
- morfs, omit_prefixes=omit_prefixes, outfile=outfile)
+ morfs, omit_prefixes=self.config.omit_prefixes, outfile=outfile
+ )
finally:
- outfile.close()
+ if file_to_close:
+ file_to_close.close()
def sysinfo(self):
- """Return a list of key,value pairs showing internal information."""
+ """Return a list of (key, value) pairs showing internal information."""
import coverage as covmod
- import platform, re, sys
+ import platform, re
info = [
('version', covmod.__version__),
@@ -344,3 +416,32 @@ class coverage(object):
]),
]
return info
+
+
+def process_startup():
+ """Call this at Python startup to perhaps measure coverage.
+
+ If the environment variable COVERAGE_PROCESS_START is defined, coverage
+ measurement is started. The value of the variable is the config file
+ to use.
+
+ There are two ways to configure your Python installation to invoke this
+ function when Python starts:
+
+ #. Create or append to sitecustomize.py to add these lines::
+
+ import coverage
+ coverage.process_startup()
+
+ #. Create a .pth file in your Python installation containing::
+
+ import coverage; coverage.process_startup()
+
+ """
+ cps = os.environ.get("COVERAGE_PROCESS_START")
+ if cps:
+ cov = coverage(config_file=cps, auto_data=True)
+ if os.environ.get("COVERAGE_COVERAGE"):
+ # Measuring coverage within coverage.py takes yet more trickery.
+ cov.cover_prefix = "Please measure coverage.py!"
+ cov.start()
diff --git a/coverage/data.py b/coverage/data.py
index 11c7c01d..9359af12 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -2,7 +2,7 @@
import os
-from coverage.backward import pickle, sorted # pylint: disable-msg=W0622
+from coverage.backward import pickle, sorted # pylint: disable-msg=W0622
class CoverageData(object):
@@ -34,21 +34,21 @@ class CoverageData(object):
`suffix` is a suffix to append to the base file name. This can be used
for multiple or parallel execution, so that many coverage data files
- can exist simultaneously.
+ can exist simultaneously. A dot will be used to join the base name and
+ the suffix.
`collector` is a string describing the coverage measurement software.
"""
- self.collector = collector
+ self.collector = collector or 'unknown'
self.use_file = True
# Construct the filename that will be used for data file storage, if we
# ever do any file storage.
- self.filename = (basename or
- os.environ.get(self.filename_env, self.filename_default))
+ self.filename = basename or ".coverage"
if suffix:
- self.filename += suffix
+ self.filename += "." + suffix
self.filename = os.path.abspath(self.filename)
# A map from canonical Python source file name to a dictionary in
@@ -169,18 +169,21 @@ class CoverageData(object):
"""Combine a number of data files together.
Treat `self.filename` as a file prefix, and combine the data from all
- of the data files starting with that prefix.
+ of the data files starting with that prefix plus a dot.
"""
data_dir, local = os.path.split(self.filename)
+ localdot = local + '.'
for f in os.listdir(data_dir or '.'):
- if f.startswith(local):
+ 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():
self.lines.setdefault(filename, {}).update(file_data)
for filename, file_data in new_arcs.items():
self.arcs.setdefault(filename, {}).update(file_data)
+ if f != local:
+ os.remove(full_path)
def add_line_data(self, line_data):
"""Add executed line data.
diff --git a/coverage/execfile.py b/coverage/execfile.py
index 15f0a5f8..333163f8 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -2,8 +2,8 @@
import imp, os, sys
-from coverage.backward import exec_function
-from coverage.misc import NoSource
+from coverage.backward import exec_code_object
+from coverage.misc import NoSource, ExceptionDuringRun
try:
@@ -36,11 +36,33 @@ def run_python_file(filename, args):
sys.path[0] = os.path.dirname(filename)
try:
+ # Open the source file.
try:
source = open(filename, 'rU').read()
except IOError:
raise NoSource("No file to run: %r" % filename)
- exec_function(source, filename, main_mod.__dict__)
+
+ # 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':
+ source += '\n'
+ code = compile(source, filename, "exec")
+
+ # Execute the source file.
+ try:
+ exec_code_object(code, main_mod.__dict__)
+ except SystemExit:
+ # The user called sys.exit(). Just pass it along to the upper
+ # layers, where it will be handled.
+ raise
+ except:
+ # Something went wrong while executing the user code.
+ # Get the exc_info, and pack them into an exception that we can
+ # throw up to the outer loop. We peel two layers off the traceback
+ # so that the coverage.py code doesn't appear in the final printed
+ # traceback.
+ typ, err, tb = sys.exc_info()
+ raise ExceptionDuringRun(typ, err, tb.tb_next.tb_next)
finally:
# Restore the old __main__
sys.modules['__main__'] = old_main_mod
diff --git a/coverage/files.py b/coverage/files.py
index fb597329..28331a18 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -24,7 +24,8 @@ class FileLocator(object):
"""
common_prefix = os.path.commonprefix(
- [filename, self.relative_dir + os.sep])
+ [filename, self.relative_dir + os.sep]
+ )
return filename[len(common_prefix):]
def canonical_filename(self, filename):
@@ -40,6 +41,8 @@ class FileLocator(object):
f = os.path.basename(f)
if not os.path.isabs(f):
for path in [os.curdir] + sys.path:
+ if path is None:
+ continue
g = os.path.join(path, f)
if os.path.exists(g):
f = g
@@ -69,7 +72,7 @@ class FileLocator(object):
data = zi.get_data(parts[1])
except IOError:
continue
- if sys.hexversion > 0x03000000:
+ if sys.version_info >= (3, 0):
data = data.decode('utf8') # TODO: How to do this properly?
return data
return None
diff --git a/coverage/html.py b/coverage/html.py
index 4d51eb34..f8de2e4c 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -69,27 +69,27 @@ class HtmlReporter(Reporter):
arcs = self.arcs
# These classes determine which lines are highlighted by default.
- c_run = " run hide_run"
- c_exc = " exc"
- c_mis = " mis"
- c_par = " par" + c_run
+ c_run = "run hide_run"
+ c_exc = "exc"
+ c_mis = "mis"
+ c_par = "par " + c_run
lines = []
for lineno, line in enumerate(source_token_lines(source)):
lineno += 1 # 1-based line numbers.
# Figure out how to mark this line.
- line_class = ""
+ line_class = []
annotate_html = ""
annotate_title = ""
if lineno in analysis.statements:
- line_class += " stm"
+ line_class.append("stm")
if lineno in analysis.excluded:
- line_class += c_exc
+ line_class.append(c_exc)
elif lineno in analysis.missing:
- line_class += c_mis
+ line_class.append(c_mis)
elif self.arcs and lineno in missing_branch_arcs:
- line_class += c_par
+ line_class.append(c_par)
n_par += 1
annlines = []
for b in missing_branch_arcs[lineno]:
@@ -103,21 +103,23 @@ class HtmlReporter(Reporter):
elif len(annlines) == 1:
annotate_title = "no jump to this line number"
elif lineno in analysis.statements:
- line_class += c_run
+ line_class.append(c_run)
# Build the HTML for the line
- html = ""
+ html = []
for tok_type, tok_text in line:
if tok_type == "ws":
- html += escape(tok_text)
+ html.append(escape(tok_text))
else:
tok_html = escape(tok_text) or '&nbsp;'
- html += "<span class='%s'>%s</span>" % (tok_type, tok_html)
+ html.append(
+ "<span class='%s'>%s</span>" % (tok_type, tok_html)
+ )
lines.append({
- 'html': html,
+ 'html': ''.join(html),
'number': lineno,
- 'class': line_class.strip() or "pln",
+ 'class': ' '.join(line_class) or "pln",
'annotate': annotate_html,
'annotate_title': annotate_title,
})
diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js
index c0abab81..b70712c4 100644
--- a/coverage/htmlfiles/coverage_html.js
+++ b/coverage/htmlfiles/coverage_html.js
@@ -33,7 +33,7 @@ function index_page_ready($) {
}
else {
// This is not the first load - something has
- // already defined sorting so we'll just update
+ // already defined sorting so we'll just update
// our stored value to match:
sort_list = table.config.sortList;
}
@@ -58,7 +58,22 @@ function index_page_ready($) {
});
// Watch for page unload events so we can save the final sort settings:
- $(window).unload(function() {
- document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"
+ $(window).unload(function() {
+ document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"
});
}
+
+// -- pyfile stuff --
+
+function toggle_lines(btn, cls) {
+ var btn = $(btn);
+ var hide = "hide_"+cls;
+ if (btn.hasClass(hide)) {
+ $("#source ."+cls).removeClass(hide);
+ btn.removeClass(hide);
+ }
+ else {
+ $("#source ."+cls).addClass(hide);
+ btn.addClass(hide);
+ }
+}
diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html
index 0f387644..700ff8bb 100644
--- a/coverage/htmlfiles/index.html
+++ b/coverage/htmlfiles/index.html
@@ -1,80 +1,81 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
- <head>
- <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
- <title>Coverage report</title>
- <link rel='stylesheet' href='style.css' type='text/css'>
- <script type='text/javascript' src='jquery-1.3.2.min.js'></script>
- <script type='text/javascript' src='jquery.tablesorter.min.js'></script>
- <script type='text/javascript' src='coverage_html.js'></script>
- <script type="text/javascript" charset="utf-8">
- jQuery(document).ready(index_page_ready);
- </script>
- </head>
- <body>
-
- <div id='header'>
- <div class='content'>
- <h1>Coverage report:
- <span class='pc_cov'>{{totals.pc_covered|format_pct}}%</span>
- </h1>
- </div>
- </div>
-
- <div id='index'>
- <table class='index'>
- <thead>
- {# The title='' attr doesn't work in Safari. #}
- <tr class='tablehead' title='Click to sort'>
- <th class='name left headerSortDown'>Module</th>
- <th>statements</th>
- <th>run</th>
- <th>excluded</th>
- {% if arcs %}
- <th>branches</th>
- <th>br exec</th>
- {% endif %}
- <th class='right'>coverage</th>
- </tr>
- </thead>
- {# HTML syntax requires thead, tfoot, tbody #}
- <tfoot>
- <tr class='total'>
- <td class='name left'>Total</td>
- <td>{{totals.n_statements}}</td>
- <td>{{totals.n_executed}}</td>
- <td>{{totals.n_excluded}}</td>
- {% if arcs %}
- <td>{{totals.n_branches}}</td>
- <td>{{totals.n_executed_branches}}</td>
- {% endif %}
- <td class='right'>{{totals.pc_covered|format_pct}}%</td>
- </tr>
- </tfoot>
- <tbody>
- {% for file in files %}
- <tr class='file'>
- <td class='name left'><a href='{{file.html_filename}}'>{{file.cu.name}}</a></td>
- <td>{{file.nums.n_statements}}</td>
- <td>{{file.nums.n_executed}}</td>
- <td>{{file.nums.n_excluded}}</td>
- {% if arcs %}
- <td>{{file.nums.n_branches}}</td>
- <td>{{file.nums.n_executed_branches}}</td>
- {% endif %}
- <td class='right'>{{file.nums.pc_covered|format_pct}}%</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </div>
-
- <div id='footer'>
- <div class='content'>
- <p>
- <a class='nav' href='{{__url__}}'>coverage.py v{{__version__}}</a>
- </p>
- </div>
- </div>
- </body>
-</html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
+ <title>Coverage report</title>
+ <link rel='stylesheet' href='style.css' type='text/css'>
+ <script type='text/javascript' src='jquery-1.3.2.min.js'></script>
+ <script type='text/javascript' src='jquery.tablesorter.min.js'></script>
+ <script type='text/javascript' src='coverage_html.js'></script>
+ <script type='text/javascript' charset='utf-8'>
+ jQuery(document).ready(index_page_ready);
+ </script>
+</head>
+<body>
+
+<div id='header'>
+ <div class='content'>
+ <h1>Coverage report:
+ <span class='pc_cov'>{{totals.pc_covered|format_pct}}%</span>
+ </h1>
+ </div>
+</div>
+
+<div id='index'>
+ <table class='index'>
+ <thead>
+ {# The title='' attr doesn't work in Safari. #}
+ <tr class='tablehead' title='Click to sort'>
+ <th class='name left headerSortDown'>Module</th>
+ <th>statements</th>
+ <th>run</th>
+ <th>excluded</th>
+ {% if arcs %}
+ <th>branches</th>
+ <th>br exec</th>
+ {% endif %}
+ <th class='right'>coverage</th>
+ </tr>
+ </thead>
+ {# HTML syntax requires thead, tfoot, tbody #}
+ <tfoot>
+ <tr class='total'>
+ <td class='name left'>Total</td>
+ <td>{{totals.n_statements}}</td>
+ <td>{{totals.n_executed}}</td>
+ <td>{{totals.n_excluded}}</td>
+ {% if arcs %}
+ <td>{{totals.n_branches}}</td>
+ <td>{{totals.n_executed_branches}}</td>
+ {% endif %}
+ <td class='right'>{{totals.pc_covered|format_pct}}%</td>
+ </tr>
+ </tfoot>
+ <tbody>
+ {% for file in files %}
+ <tr class='file'>
+ <td class='name left'><a href='{{file.html_filename}}'>{{file.cu.name}}</a></td>
+ <td>{{file.nums.n_statements}}</td>
+ <td>{{file.nums.n_executed}}</td>
+ <td>{{file.nums.n_excluded}}</td>
+ {% if arcs %}
+ <td>{{file.nums.n_branches}}</td>
+ <td>{{file.nums.n_executed_branches}}</td>
+ {% endif %}
+ <td class='right'>{{file.nums.pc_covered|format_pct}}%</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
+
+<div id='footer'>
+ <div class='content'>
+ <p>
+ <a class='nav' href='{{__url__}}'>coverage.py v{{__version__}}</a>
+ </p>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html
index 62518ba0..ec69556d 100644
--- a/coverage/htmlfiles/pyfile.html
+++ b/coverage/htmlfiles/pyfile.html
@@ -1,59 +1,47 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
-<head>
-<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
-<title>Coverage for {{cu.name|escape}}</title>
-<link rel='stylesheet' href='style.css' type='text/css'>
-<script type='text/javascript' src='jquery-1.3.2.min.js'></script>
-<script type='text/javascript'>
-function toggle_lines(btn, cls) {
- var btn = $(btn);
- var hide = "hide_"+cls;
- if (btn.hasClass(hide)) {
- $("#source ."+cls).removeClass(hide);
- btn.removeClass(hide);
- }
- else {
- $("#source ."+cls).addClass(hide);
- btn.addClass(hide);
- }
-}
-</script>
-</head>
-<body>
-<div id='header'>
- <div class='content'>
- <h1>Coverage for <b>{{cu.name|escape}}</b> :
- <span class='pc_cov'>{{nums.pc_covered|format_pct}}%</span>
- </h1>
- <h2 class='stats'>
- {{nums.n_statements}} statements
- <span class='{{c_run.strip}}' onclick='toggle_lines(this, "run")'>{{nums.n_executed}} run</span>
- <span class='{{c_exc.strip}}' onclick='toggle_lines(this, "exc")'>{{nums.n_excluded}} excluded</span>
- <span class='{{c_mis.strip}}' onclick='toggle_lines(this, "mis")'>{{nums.n_missing}} missing</span>
- {% if arcs %}
- <span class='{{c_par.strip}}' onclick='toggle_lines(this, "par")'>{{n_par}} partial</span>
- {% endif %}
- </h2>
- </div>
-</div>
-
-<div id='source'>
-<table cellspacing='0' cellpadding='0'>
-<tr>
-<td class='linenos' valign='top'>
- {% for line in lines %}
- <p class='{{line.class}}'>{{line.number}}</p>
- {% endfor %}
-</td>
-<td class='text' valign='top'>
- {% for line in lines %}
- <p class='{{line.class}}'>{% if line.annotate %}<span class='annotate' title='{{line.annotate_title}}'>{{line.annotate}}</span>{% endif %}{{line.html}}<span class='strut'>&nbsp;</span></p>
- {% endfor %}
-</td>
-</tr>
-</table>
-</div>
-
-</body>
-</html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
+ <title>Coverage for {{cu.name|escape}}: {{nums.pc_covered|format_pct}}%</title>
+ <link rel='stylesheet' href='style.css' type='text/css'>
+ <script type='text/javascript' src='jquery-1.3.2.min.js'></script>
+ <script type='text/javascript' src='coverage_html.js'></script>
+</head>
+<body>
+
+<div id='header'>
+ <div class='content'>
+ <h1>Coverage for <b>{{cu.name|escape}}</b> :
+ <span class='pc_cov'>{{nums.pc_covered|format_pct}}%</span>
+ </h1>
+ <h2 class='stats'>
+ {{nums.n_statements}} statements
+ <span class='{{c_run}}' onclick='toggle_lines(this, "run")'>{{nums.n_executed}} run</span>
+ <span class='{{c_exc}}' onclick='toggle_lines(this, "exc")'>{{nums.n_excluded}} excluded</span>
+ <span class='{{c_mis}}' onclick='toggle_lines(this, "mis")'>{{nums.n_missing}} missing</span>
+ {% if arcs %}
+ <span class='{{c_par}}' onclick='toggle_lines(this, "par")'>{{n_par}} partial</span>
+ {% endif %}
+ </h2>
+ </div>
+</div>
+
+<div id='source'>
+ <table cellspacing='0' cellpadding='0'>
+ <tr>
+ <td class='linenos' valign='top'>
+ {% for line in lines %}
+ <p id='n{{line.number}}' class='{{line.class}}'>{{line.number}}</p>
+ {% endfor %}
+ </td>
+ <td class='text' valign='top'>
+ {% for line in lines %}
+ <p id='t{{line.number}}' class='{{line.class}}'>{% if line.annotate %}<span class='annotate' title='{{line.annotate_title}}'>{{line.annotate}}</span>{% endif %}{{line.html}}<span class='strut'>&nbsp;</span></p>
+ {% endfor %}
+ </td>
+ </tr>
+ </table>
+</div>
+
+</body>
+</html>
diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css
index b2987ea3..25e7d115 100644
--- a/coverage/htmlfiles/style.css
+++ b/coverage/htmlfiles/style.css
@@ -20,7 +20,7 @@ body {
html>body {
font-size: 16px;
- }
+ }
/* Set base font size to 12/16 */
p {
@@ -115,7 +115,7 @@ td.text {
margin: 0;
padding: 0 0 0 .5em;
border-left: 2px solid #ffffff;
- white-space: nowrap;
+ white-space: nowrap;
}
.text p.mis {
diff --git a/coverage/misc.py b/coverage/misc.py
index 0e6bcf99..4218536d 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -60,6 +60,14 @@ def expensive(fn):
return _wrapped
+def bool_or_none(b):
+ """Return bool(b), but preserve None."""
+ if b is None:
+ return None
+ else:
+ return bool(b)
+
+
class CoverageException(Exception):
"""An exception specific to Coverage."""
pass
@@ -67,3 +75,11 @@ class CoverageException(Exception):
class NoSource(CoverageException):
"""Used to indicate we couldn't find the source for a module."""
pass
+
+class ExceptionDuringRun(CoverageException):
+ """An exception happened while running customer code.
+
+ Construct it with three arguments, the values from `sys.exc_info`.
+
+ """
+ pass
diff --git a/coverage/parser.py b/coverage/parser.py
index 43f691f5..aea05f3d 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -256,6 +256,11 @@ OPS_CHUNK_END = _opcode_set(
'BREAK_LOOP', 'CONTINUE_LOOP',
)
+# Opcodes that unconditionally begin a new code chunk. By starting new chunks
+# with unconditional jump instructions, we neatly deal with jumps to jumps
+# properly.
+OPS_CHUNK_BEGIN = _opcode_set('JUMP_ABSOLUTE', 'JUMP_FORWARD')
+
# Opcodes that push a block on the block stack.
OPS_PUSH_BLOCK = _opcode_set('SETUP_LOOP', 'SETUP_EXCEPT', 'SETUP_FINALLY')
@@ -310,7 +315,7 @@ class ByteParser(object):
return map(lambda c: ByteParser(code=c), CodeObjects(self.code))
# Getting numbers from the lnotab value changed in Py3.0.
- if sys.hexversion >= 0x03000000:
+ if sys.version_info >= (3, 0):
def _lnotab_increments(self, lnotab):
"""Return a list of ints from the lnotab bytes in 3.x"""
return list(lnotab)
@@ -399,12 +404,20 @@ class ByteParser(object):
ult = penult = None
for bc in ByteCodes(self.code.co_code):
- # Maybe have to start a new block
+ # Maybe have to start a new chunk
if bc.offset in bytes_lines_map:
+ # Start a new chunk for each source line number.
if chunk:
chunk.exits.add(bc.offset)
chunk = Chunk(bc.offset, bytes_lines_map[bc.offset])
chunks.append(chunk)
+ elif bc.op in OPS_CHUNK_BEGIN:
+ # Jumps deserve their own unnumbered chunk. This fixes
+ # problems with jumps to jumps getting confused.
+ if chunk:
+ chunk.exits.add(bc.offset)
+ chunk = Chunk(bc.offset)
+ chunks.append(chunk)
if not chunk:
chunk = Chunk(bc.offset)
diff --git a/coverage/phystokens.py b/coverage/phystokens.py
index 5824b9b9..60b87932 100644
--- a/coverage/phystokens.py
+++ b/coverage/phystokens.py
@@ -77,7 +77,8 @@ def source_token_lines(source):
ws_tokens = [token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL]
line = []
col = 0
- tokgen = tokenize.generate_tokens(StringIO(source.expandtabs(8)).readline)
+ source = source.expandtabs(8).replace('\r\n', '\n')
+ tokgen = tokenize.generate_tokens(StringIO(source).readline)
for ttype, ttext, (_, scol), (_, ecol), _ in phys_tokens(tokgen):
mark_start = True
for part in re.split('(\n)', ttext):
diff --git a/coverage/summary.py b/coverage/summary.py
index f4d3c2c6..3db7b767 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -58,7 +58,7 @@ class SummaryReporter(Reporter):
args += (analysis.missing_formatted(),)
outfile.write(fmt_coverage % args)
total += nums
- except KeyboardInterrupt: #pragma: no cover
+ except KeyboardInterrupt: # pragma: no cover
raise
except:
if not self.ignore_errors:
diff --git a/coverage/templite.py b/coverage/templite.py
index d3c673c6..c39e061e 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -101,14 +101,14 @@ class Templite(object):
# Run it through an engine, and return the result.
engine = _TempliteEngine(ctx)
engine.execute(self.ops)
- return engine.result
+ return "".join(engine.result)
class _TempliteEngine(object):
"""Executes Templite objects to produce strings."""
def __init__(self, context):
self.context = context
- self.result = ""
+ self.result = []
def execute(self, ops):
"""Execute `ops` in the engine.
@@ -118,10 +118,10 @@ class _TempliteEngine(object):
"""
for op, args in ops:
if op == 'lit':
- self.result += args
+ self.result.append(args)
elif op == 'exp':
try:
- self.result += str(self.evaluate(args))
+ self.result.append(str(self.evaluate(args)))
except:
exc_class, exc, _ = sys.exc_info()
new_exc = exc_class("Couldn't evaluate {{ %s }}: %s"