summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt6
-rw-r--r--TODO.txt2
-rw-r--r--coverage/annotate.py6
-rw-r--r--coverage/cmdline.py6
-rw-r--r--coverage/control.py7
-rw-r--r--coverage/html.py3
-rw-r--r--coverage/misc.py6
-rw-r--r--coverage/parser.py58
-rw-r--r--coverage/phystokens.py9
-rw-r--r--coverage/results.py2
-rw-r--r--coverage/templite.py226
-rw-r--r--coverage/xmlreport.py2
-rw-r--r--doc/changes.rst12
-rw-r--r--doc/conf.py4
-rw-r--r--doc/index.rst3
-rw-r--r--doc/install.rst5
-rw-r--r--doc/sample_html/cogapp___init__.html2
-rw-r--r--doc/sample_html/cogapp___main__.html2
-rw-r--r--doc/sample_html/cogapp_backward.html2
-rw-r--r--doc/sample_html/cogapp_cogapp.html2
-rw-r--r--doc/sample_html/cogapp_makefiles.html2
-rw-r--r--doc/sample_html/cogapp_test_cogapp.html2
-rw-r--r--doc/sample_html/cogapp_test_makefiles.html2
-rw-r--r--doc/sample_html/cogapp_test_whiteutils.html2
-rw-r--r--doc/sample_html/cogapp_whiteutils.html2
-rw-r--r--doc/sample_html/index.html2
-rw-r--r--doc/sample_html/status.dat16
-rw-r--r--lab/branches.py20
-rw-r--r--tests/coveragetest.py7
-rw-r--r--tests/test_templite.py53
30 files changed, 311 insertions, 162 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 4b5d996..8819a98 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -8,8 +8,10 @@ Change history for Coverage.py
- Python versions supported are now 2.6, 2.7, 3.2, 3.3.
-3.7.1
------
+3.7.1 -- 13 December 2013
+-------------------------
+
+- Improved the speed of HTML report generation by about 20%.
- Fixed the mechanism for finding OS-installed static files for the HTML report
so that it will actually find OS-installed static files.
diff --git a/TODO.txt b/TODO.txt
index c43c91a..d6e0ec0 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -21,6 +21,8 @@ Key:
+ "with" statements
- .format() ?
+ try/except/finally
+ - with assertRaises
+ - exec statement can look like a function in py2 (since when?)
+ Remove code only run on <2.6
- Change data file to json
diff --git a/coverage/annotate.py b/coverage/annotate.py
index b7f32c1..19777ea 100644
--- a/coverage/annotate.py
+++ b/coverage/annotate.py
@@ -59,9 +59,9 @@ class AnnotateReporter(Reporter):
dest_file = filename + ",cover"
dest = open(dest_file, 'w')
- statements = analysis.statements
- missing = analysis.missing
- excluded = analysis.excluded
+ statements = sorted(analysis.statements)
+ missing = sorted(analysis.missing)
+ excluded = sorted(analysis.excluded)
lineno = 0
i = 0
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 9373283..c311976 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -1,6 +1,6 @@
"""Command-line support for Coverage."""
-import optparse, os, sys, traceback
+import optparse, os, sys, time, traceback
from coverage.execfile import run_python_file, run_python_module
from coverage.misc import CoverageException, ExceptionDuringRun, NoSource
@@ -715,7 +715,11 @@ def main(argv=None):
if argv is None:
argv = sys.argv[1:]
try:
+ start = time.clock()
status = CoverageScript().command_line(argv)
+ end = time.clock()
+ if 0:
+ print("time: %.3fs" % (end - start))
except ExceptionDuringRun as err:
# An exception was caught while running the product code. The
# sys.exc_info() return tuple is packed into an ExceptionDuringRun
diff --git a/coverage/control.py b/coverage/control.py
index 51ff043..fa6fec7 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -571,8 +571,11 @@ class coverage(object):
"""
analysis = self._analyze(morf)
return (
- analysis.filename, analysis.statements, analysis.excluded,
- analysis.missing, analysis.missing_formatted()
+ analysis.filename,
+ sorted(analysis.statements),
+ sorted(analysis.excluded),
+ sorted(analysis.missing),
+ analysis.missing_formatted(),
)
def _analyze(self, it):
diff --git a/coverage/html.py b/coverage/html.py
index e1966bf..d168e35 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -176,7 +176,8 @@ class HtmlReporter(Reporter):
# Get the numbers for this file.
nums = analysis.numbers
- missing_branch_arcs = analysis.missing_branch_arcs()
+ if self.arcs:
+ missing_branch_arcs = analysis.missing_branch_arcs()
# These classes determine which lines are highlighted by default.
c_run = "run hide_run"
diff --git a/coverage/misc.py b/coverage/misc.py
index c3fd9e2..c88d4ec 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -37,6 +37,8 @@ def format_lines(statements, lines):
i = 0
j = 0
start = None
+ statements = sorted(statements)
+ lines = sorted(lines)
while i < len(statements) and j < len(lines):
if statements[i] == lines[j]:
if start == None:
@@ -111,8 +113,10 @@ class Hasher(object):
self.md5.update(to_bytes(str(type(v))))
if isinstance(v, string_class):
self.md5.update(to_bytes(v))
+ elif v is None:
+ pass
elif isinstance(v, (int, float)):
- self.update(str(v))
+ self.md5.update(to_bytes(str(v)))
elif isinstance(v, (tuple, list)):
for e in v:
self.update(e)
diff --git a/coverage/parser.py b/coverage/parser.py
index f2885c0..de6590a 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -103,7 +103,7 @@ class CodeParser(object):
first_line = None
empty = True
- tokgen = tokenize.generate_tokens(StringIO(self.text).readline)
+ tokgen = generate_tokens(self.text)
for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
if self.show_tokens: # pragma: not covered
print("%10s %5s %-20r %r" % (
@@ -170,16 +170,18 @@ class CodeParser(object):
first_line = line
return first_line
- def first_lines(self, lines, ignore=None):
+ def first_lines(self, lines, *ignores):
"""Map the line numbers in `lines` to the correct first line of the
statement.
- Skip any line mentioned in `ignore`.
+ Skip any line mentioned in any of the sequences in `ignores`.
- Returns a sorted list of the first lines.
+ Returns a set of the first lines.
"""
- ignore = ignore or []
+ ignore = set()
+ for ign in ignores:
+ ignore.update(ign)
lset = set()
for l in lines:
if l in ignore:
@@ -187,13 +189,13 @@ class CodeParser(object):
new_l = self.first_line(l)
if new_l not in ignore:
lset.add(new_l)
- return sorted(lset)
+ return lset
def parse_source(self):
"""Parse source text to find executable lines, excluded lines, etc.
- Return values are 1) a sorted list of executable line numbers, and
- 2) a sorted list of excluded line numbers.
+ Return values are 1) a set of executable line numbers, and 2) a set of
+ excluded line numbers.
Reported line numbers are normalized to the first line of multi-line
statements.
@@ -209,8 +211,11 @@ class CodeParser(object):
)
excluded_lines = self.first_lines(self.excluded)
- ignore = excluded_lines + list(self.docstrings)
- lines = self.first_lines(self.statement_starts, ignore)
+ lines = self.first_lines(
+ self.statement_starts,
+ excluded_lines,
+ self.docstrings
+ )
return lines, excluded_lines
@@ -432,14 +437,15 @@ class ByteParser(object):
# Get a set of all of the jump-to points.
jump_to = set()
- for bc in ByteCodes(self.code.co_code):
+ bytecodes = list(ByteCodes(self.code.co_code))
+ for bc in bytecodes:
if bc.jump_to >= 0:
jump_to.add(bc.jump_to)
chunk_lineno = 0
# Walk the byte codes building chunks.
- for bc in ByteCodes(self.code.co_code):
+ for bc in bytecodes:
# Maybe have to start a new chunk
start_new_chunk = False
first_chunk = False
@@ -652,3 +658,31 @@ class Chunk(object):
return "<%d+%d @%d%s %r>" % (
self.byte, self.length, self.line, bang, list(self.exits)
)
+
+
+class CachedTokenizer(object):
+ """A one-element cache around tokenize.generate_tokens.
+
+ When reporting, coverage.py tokenizes files twice, once to find the
+ structure of the file, and once to syntax-color it. Tokenizing is
+ expensive, and easily cached.
+
+ This is a one-element cache so that our twice-in-a-row tokenizing doesn't
+ actually tokenize twice.
+
+ """
+ def __init__(self):
+ self.last_text = None
+ self.last_tokens = None
+
+ def generate_tokens(self, text):
+ """A stand-in for `tokenize.generate_tokens`."""
+ if text != self.last_text:
+ self.last_text = text
+ self.last_tokens = list(
+ tokenize.generate_tokens(StringIO(text).readline)
+ )
+ return self.last_tokens
+
+# Create our generate_tokens cache as a callable replacement function.
+generate_tokens = CachedTokenizer().generate_tokens
diff --git a/coverage/phystokens.py b/coverage/phystokens.py
index 5852241..e79ce01 100644
--- a/coverage/phystokens.py
+++ b/coverage/phystokens.py
@@ -1,7 +1,8 @@
"""Better tokenizing for coverage.py."""
import codecs, keyword, re, sys, token, tokenize
-from coverage.backward import StringIO # pylint: disable=W0622
+from coverage.parser import generate_tokens
+
def phys_tokens(toks):
"""Return all physical tokens, even line continuations.
@@ -18,7 +19,7 @@ def phys_tokens(toks):
last_ttype = None
for ttype, ttext, (slineno, scol), (elineno, ecol), ltext in toks:
if last_lineno != elineno:
- if last_line and last_line[-2:] == "\\\n":
+ if last_line and last_line.endswith("\\\n"):
# We are at the beginning of a new line, and the last line
# ended with a backslash. We probably have to inject a
# backslash token into the stream. Unfortunately, there's more
@@ -74,11 +75,11 @@ def source_token_lines(source):
is indistinguishable from a final line with a newline.
"""
- ws_tokens = [token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL]
+ ws_tokens = set([token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL])
line = []
col = 0
source = source.expandtabs(8).replace('\r\n', '\n')
- tokgen = tokenize.generate_tokens(StringIO(source).readline)
+ tokgen = generate_tokens(source)
for ttype, ttext, (_, scol), (_, ecol), _ in phys_tokens(tokgen):
mark_start = True
for part in re.split('(\n)', ttext):
diff --git a/coverage/results.py b/coverage/results.py
index e6475af..0576ae1 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -27,7 +27,7 @@ class Analysis(object):
# Identify missing statements.
executed = self.coverage.data.executed_lines(self.filename)
exec1 = self.parser.first_lines(executed)
- self.missing = sorted(set(self.statements) - set(exec1))
+ self.missing = self.statements - exec1
if self.coverage.data.has_arcs():
self.no_branch = self.parser.lines_matching(
diff --git a/coverage/templite.py b/coverage/templite.py
index c39e061..429a5cc 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -2,7 +2,51 @@
# Coincidentally named the same as http://code.activestate.com/recipes/496702/
-import re, sys
+import re
+
+
+class CodeBuilder(object):
+ """Build source code conveniently."""
+
+ def __init__(self, indent=0):
+ self.code = []
+ self.indent_amount = indent
+
+ def add_line(self, line):
+ """Add a line of source to the code.
+
+ Don't include indentations or newlines.
+
+ """
+ self.code.append(" " * self.indent_amount)
+ self.code.append(line)
+ self.code.append("\n")
+
+ def add_section(self):
+ """Add a section, a sub-CodeBuilder."""
+ sect = CodeBuilder(self.indent_amount)
+ self.code.append(sect)
+ return sect
+
+ def indent(self):
+ """Increase the current indent for following lines."""
+ self.indent_amount += 4
+
+ def dedent(self):
+ """Decrease the current indent for following lines."""
+ self.indent_amount -= 4
+
+ def __str__(self):
+ return "".join([str(c) for c in self.code])
+
+ def get_function(self, fn_name):
+ """Compile the code, and return the function `fn_name`."""
+ assert self.indent_amount == 0
+ g = {}
+ code_text = str(self)
+ exec(code_text, g)
+ return g[fn_name]
+
class Templite(object):
"""A simple template renderer, for a nano-subset of Django syntax.
@@ -39,53 +83,104 @@ class Templite(object):
for context in contexts:
self.context.update(context)
+ # We construct a function in source form, then compile it and hold onto
+ # it, and execute it to render the template.
+ code = CodeBuilder()
+
+ code.add_line("def render(ctx, dot):")
+ code.indent()
+ vars_code = code.add_section()
+ self.all_vars = set()
+ self.loop_vars = set()
+ code.add_line("result = []")
+ code.add_line("a = result.append")
+ code.add_line("e = result.extend")
+ code.add_line("s = str")
+
+ buffered = []
+ def flush_output():
+ """Force `buffered` to the code builder."""
+ if len(buffered) == 1:
+ code.add_line("a(%s)" % buffered[0])
+ elif len(buffered) > 1:
+ code.add_line("e([%s])" % ",".join(buffered))
+ del buffered[:]
+
# Split the text to form a list of tokens.
toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
- # Parse the tokens into a nested list of operations. Each item in the
- # list is a tuple with an opcode, and arguments. They'll be
- # interpreted by TempliteEngine.
- #
- # When parsing an action tag with nested content (if, for), the current
- # ops list is pushed onto ops_stack, and the parsing continues in a new
- # ops list that is part of the arguments to the if or for op.
- ops = []
ops_stack = []
for tok in toks:
if tok.startswith('{{'):
- # Expression: ('exp', expr)
- ops.append(('exp', tok[2:-2].strip()))
+ # An expression to evaluate.
+ buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip()))
elif tok.startswith('{#'):
# Comment: ignore it and move on.
continue
elif tok.startswith('{%'):
# Action tag: split into words and parse further.
+ flush_output()
words = tok[2:-2].strip().split()
if words[0] == 'if':
- # If: ('if', (expr, body_ops))
- if_ops = []
+ # An if statement: evaluate the expression to determine if.
assert len(words) == 2
- ops.append(('if', (words[1], if_ops)))
- ops_stack.append(ops)
- ops = if_ops
+ ops_stack.append('if')
+ code.add_line("if %s:" % self.expr_code(words[1]))
+ code.indent()
elif words[0] == 'for':
- # For: ('for', (varname, listexpr, body_ops))
+ # A loop: iterate over expression result.
assert len(words) == 4 and words[2] == 'in'
- for_ops = []
- ops.append(('for', (words[1], words[3], for_ops)))
- ops_stack.append(ops)
- ops = for_ops
+ ops_stack.append('for')
+ self.loop_vars.add(words[1])
+ code.add_line(
+ "for c_%s in %s:" % (
+ words[1],
+ self.expr_code(words[3])
+ )
+ )
+ code.indent()
elif words[0].startswith('end'):
# Endsomething. Pop the ops stack
- ops = ops_stack.pop()
- assert ops[-1][0] == words[0][3:]
+ end_what = words[0][3:]
+ if ops_stack[-1] != end_what:
+ raise SyntaxError("Mismatched end tag: %r" % end_what)
+ ops_stack.pop()
+ code.dedent()
else:
- raise SyntaxError("Don't understand tag %r" % words)
+ raise SyntaxError("Don't understand tag: %r" % words[0])
else:
- ops.append(('lit', tok))
+ # Literal content. If it isn't empty, output it.
+ if tok:
+ buffered.append("%r" % tok)
+ flush_output()
+
+ for var_name in self.all_vars - self.loop_vars:
+ vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name))
- assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0]
- self.ops = ops
+ if ops_stack:
+ raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1])
+
+ code.add_line("return ''.join(result)")
+ code.dedent()
+ self.render_function = code.get_function('render')
+
+ def expr_code(self, expr):
+ """Generate a Python expression for `expr`."""
+ if "|" in expr:
+ pipes = expr.split("|")
+ code = self.expr_code(pipes[0])
+ for func in pipes[1:]:
+ self.all_vars.add(func)
+ code = "c_%s(%s)" % (func, code)
+ elif "." in expr:
+ dots = expr.split(".")
+ code = self.expr_code(dots[0])
+ args = [repr(d) for d in dots[1:]]
+ code = "dot(%s, %s)" % (code, ", ".join(args))
+ else:
+ self.all_vars.add(expr)
+ code = "c_%s" % expr
+ return code
def render(self, context=None):
"""Render this template by applying it to `context`.
@@ -97,70 +192,15 @@ class Templite(object):
ctx = dict(self.context)
if context:
ctx.update(context)
-
- # Run it through an engine, and return the result.
- engine = _TempliteEngine(ctx)
- engine.execute(self.ops)
- return "".join(engine.result)
-
-
-class _TempliteEngine(object):
- """Executes Templite objects to produce strings."""
- def __init__(self, context):
- self.context = context
- self.result = []
-
- def execute(self, ops):
- """Execute `ops` in the engine.
-
- Called recursively for the bodies of if's and loops.
-
- """
- for op, args in ops:
- if op == 'lit':
- self.result.append(args)
- elif op == 'exp':
- try:
- self.result.append(str(self.evaluate(args)))
- except:
- exc_class, exc, _ = sys.exc_info()
- new_exc = exc_class("Couldn't evaluate {{ %s }}: %s"
- % (args, exc))
- raise new_exc
- elif op == 'if':
- expr, body = args
- if self.evaluate(expr):
- self.execute(body)
- elif op == 'for':
- var, lis, body = args
- vals = self.evaluate(lis)
- for val in vals:
- self.context[var] = val
- self.execute(body)
- else:
- raise AssertionError("TempliteEngine doesn't grok op %r" % op)
-
- def evaluate(self, expr):
- """Evaluate an expression.
-
- `expr` can have pipes and dots to indicate data access and filtering.
-
- """
- if "|" in expr:
- pipes = expr.split("|")
- value = self.evaluate(pipes[0])
- for func in pipes[1:]:
- value = self.evaluate(func)(value)
- elif "." in expr:
- dots = expr.split('.')
- value = self.evaluate(dots[0])
- for dot in dots[1:]:
- try:
- value = getattr(value, dot)
- except AttributeError:
- value = value[dot]
- if hasattr(value, '__call__'):
- value = value()
- else:
- value = self.context[expr]
+ return self.render_function(ctx, self.do_dots)
+
+ def do_dots(self, value, *dots):
+ """Evaluate dotted expressions at runtime."""
+ for dot in dots:
+ try:
+ value = getattr(value, dot)
+ except AttributeError:
+ value = value[dot]
+ if hasattr(value, '__call__'):
+ value = value()
return value
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index d4b102f..f5a4c1b 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -116,7 +116,7 @@ class XmlReporter(Reporter):
branch_stats = analysis.branch_stats()
# For each statement, create an XML 'line' element.
- for line in analysis.statements:
+ for line in sorted(analysis.statements):
xline = self.xml_out.createElement("line")
xline.setAttribute("number", str(line))
diff --git a/doc/changes.rst b/doc/changes.rst
index f87bb91..3ddf889 100644
--- a/doc/changes.rst
+++ b/doc/changes.rst
@@ -26,6 +26,7 @@ Major change history for coverage.py
:history: 20121223T180600, updated for 3.6b2.
:history: 20130105T173500, updated for 3.6
:history: 20131005T205700, updated for 3.7
+:history: 20131212T213100, updated for 3.7.1
These are the major changes for coverage.py. For a more complete change
@@ -34,6 +35,17 @@ history, see the `CHANGES.txt`_ file in the source tree.
.. _CHANGES.txt: http://bitbucket.org/ned/coveragepy/src/tip/CHANGES.txt
+.. _changes_371:
+
+Version 3.7.1 --- 13 December 2013
+----------------------------------
+
+- Improved the speed of HTML report generation by about 20%.
+
+- Fixed the mechanism for finding OS-installed static files for the HTML report
+ so that it will actually find OS-installed static files.
+
+
.. _changes_37:
Version 3.7 --- 6 October 2013
diff --git a/doc/conf.py b/doc/conf.py
index 052bd8e..41a00e3 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -47,9 +47,9 @@ copyright = u'2009\N{EN DASH}2013, Ned Batchelder'
# built documents.
#
# The short X.Y version.
-version = '3.7'
+version = '3.7.1'
# The full version, including alpha/beta/rc tags.
-release = '3.7'
+release = '3.7.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/doc/index.rst b/doc/index.rst
index 657e3d3..3a0d930 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -30,6 +30,7 @@ coverage.py
:history: 20121229T112300, Updated for 3.6b3.
:history: 20130105T174000, Updated for 3.6
:history: 20131005T210000, Updated for 3.7
+:history: 20131212T213300, Updated for 3.7.1
Coverage.py is a tool for measuring code coverage of Python programs. It
@@ -42,7 +43,7 @@ not.
.. ifconfig:: not prerelease
- The latest version is coverage.py 3.7, released 6 October 2013.
+ The latest version is coverage.py 3.7.1, released 13 December 2013.
It is supported on Python versions 2.3 through 3.4, and PyPy 2.1.
.. ifconfig:: prerelease
diff --git a/doc/install.rst b/doc/install.rst
index 2e807fa..bc8097a 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -19,6 +19,7 @@ Installation
:history: 20121229T112400, updated for 3.6b3.
:history: 20130105T174400, updated for 3.6.
:history: 20131005T210600, updated for 3.7.
+:history: 20131212T213500, updated for 3.7.1.
.. highlight:: console
@@ -74,9 +75,9 @@ If all went well, you should be able to open a command prompt, and see coverage
installed properly::
$ coverage --version
- Coverage.py, version 3.7. http://nedbatchelder.com/code/coverage
+ Coverage.py, version 3.7.1. http://nedbatchelder.com/code/coverage
You can also invoke coverage as a module::
$ python -m coverage --version
- Coverage.py, version 3.7. http://nedbatchelder.com/code/coverage
+ Coverage.py, version 3.7.1. http://nedbatchelder.com/code/coverage
diff --git a/doc/sample_html/cogapp___init__.html b/doc/sample_html/cogapp___init__.html
index 3e02038..1199d7b 100644
--- a/doc/sample_html/cogapp___init__.html
+++ b/doc/sample_html/cogapp___init__.html
@@ -93,7 +93,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp___main__.html b/doc/sample_html/cogapp___main__.html
index 3f9c06d..23c3d5e 100644
--- a/doc/sample_html/cogapp___main__.html
+++ b/doc/sample_html/cogapp___main__.html
@@ -85,7 +85,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_backward.html b/doc/sample_html/cogapp_backward.html
index 1132204..0ab8e96 100644
--- a/doc/sample_html/cogapp_backward.html
+++ b/doc/sample_html/cogapp_backward.html
@@ -139,7 +139,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_cogapp.html b/doc/sample_html/cogapp_cogapp.html
index 6167ebe..bfac946 100644
--- a/doc/sample_html/cogapp_cogapp.html
+++ b/doc/sample_html/cogapp_cogapp.html
@@ -1541,7 +1541,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_makefiles.html b/doc/sample_html/cogapp_makefiles.html
index c1211a6..62bde7a 100644
--- a/doc/sample_html/cogapp_makefiles.html
+++ b/doc/sample_html/cogapp_makefiles.html
@@ -201,7 +201,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_test_cogapp.html b/doc/sample_html/cogapp_test_cogapp.html
index e96d7a9..3c6696a 100644
--- a/doc/sample_html/cogapp_test_cogapp.html
+++ b/doc/sample_html/cogapp_test_cogapp.html
@@ -4151,7 +4151,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_test_makefiles.html b/doc/sample_html/cogapp_test_makefiles.html
index c05160f..bb9ef2e 100644
--- a/doc/sample_html/cogapp_test_makefiles.html
+++ b/doc/sample_html/cogapp_test_makefiles.html
@@ -261,7 +261,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_test_whiteutils.html b/doc/sample_html/cogapp_test_whiteutils.html
index 307bc9c..ffc7b88 100644
--- a/doc/sample_html/cogapp_test_whiteutils.html
+++ b/doc/sample_html/cogapp_test_whiteutils.html
@@ -281,7 +281,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/cogapp_whiteutils.html b/doc/sample_html/cogapp_whiteutils.html
index 210c327..c262ff9 100644
--- a/doc/sample_html/cogapp_whiteutils.html
+++ b/doc/sample_html/cogapp_whiteutils.html
@@ -217,7 +217,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html
index 76c8b65..336868b 100644
--- a/doc/sample_html/index.html
+++ b/doc/sample_html/index.html
@@ -189,7 +189,7 @@
<div id='footer'>
<div class='content'>
<p>
- <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a>
+ <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7.1</a>
</p>
</div>
</div>
diff --git a/doc/sample_html/status.dat b/doc/sample_html/status.dat
index 6928ddf..53fb652 100644
--- a/doc/sample_html/status.dat
+++ b/doc/sample_html/status.dat
@@ -52,7 +52,7 @@ p24
I2
sbssS'hash'
p25
-S'Mg\x80>V\xccq\x0c\x8d\x95.\xda\xc5F\x9f@'
+S'\xaa\xeae\xeaG{\xcf\xde\xe3\xc8;.$\x1f\xb5,'
p26
ssS'cogapp_whiteutils'
p27
@@ -86,7 +86,7 @@ I3
sg24
I3
sbssg25
-S'#)\xee\xe5\xbb\x8c\x8f\xf9g\xaf\t\xc2r\x08sp'
+S'o\xfd\x0e+s2="\xb2\x1c\xd6\xa1\xee\x85\x85\xda'
p34
ssS'cogapp_test_makefiles'
p35
@@ -154,7 +154,7 @@ I20
sg24
I14
sbssg25
-S'\x07\x13Op\x19\t\xfb)\x1b\xac\xbe\xff ~\xcb\xd0'
+S'p/\xc29Y\x0b\x02\x8a\x9eO\xbf\x89r\xa86\xf2'
p50
ssS'cogapp_backward'
p51
@@ -188,7 +188,7 @@ I12
sg24
I1
sbssg25
-S'-wq\x1br\xbd\xe0w\xce\xa3\x83a\x9a\x1d\x80^'
+S'\x91\x17\x7f\x8b\x0c\x83"6\rG\xd6gERX\xe5'
p58
ssS'cogapp___main__'
p59
@@ -290,7 +290,7 @@ I180
sg24
I104
sbssg25
-S'\xc3\xb9\xed\xa9\xe7\xf8\x87\xabv\x8a\rO\xb7nB\x0c'
+S'\xbbX\x05\x95\xff\xe5\xebOd\x9d\xc2\x8c\x98\xe16\xc3'
p82
ssS'cogapp___init__'
p83
@@ -324,15 +324,15 @@ I0
sg24
I0
sbssg25
-S't\xfe\x8e\x84\x9f\xb2\xed\x06\xb8Co\xe50\x90\x10\xd2'
+S'\x99*\x0e\\\x10\x11O\x06WG/gJ\x83\xdd\x99'
p90
sssS'version'
p91
-S'3.7'
+S'3.7.1'
p92
sS'settings'
p93
-S'\xa3rd\x80\x8f\xb5t?p5\x04R\x18\x85\xda\x82'
+S'\x98\xa8x\xfe"r\xb5K\x98\xb1N\\\x99\x9e\xf3\xe5'
p94
sS'format'
p95
diff --git a/lab/branches.py b/lab/branches.py
index 1fa705f..fbba87e 100644
--- a/lab/branches.py
+++ b/lab/branches.py
@@ -2,24 +2,24 @@
def my_function(x):
"""This isn't real code, just snippets..."""
-
+
# An infinite loop is structurally still a branch: it can next execute the
# first line of the loop, or the first line after the loop. But
# "while True" will never jump to the line after the loop, so the line
# is shown as a partial branch:
- i = 0
+ i = 0
while True:
print "In while True"
if i > 0:
break
i += 1
print "Left the True loop"
-
+
# Notice that "while 1" also has this problem. Even though the compiler
# knows there's no computation at the top of the loop, it's still expressed
# in byte code as a branch with two possibilities.
-
+
i = 0
while 1:
print "In while 1"
@@ -27,14 +27,14 @@ def my_function(x):
break
i += 1
print "Left the 1 loop"
-
- # Coverage.py lets the developer exclude lines that he knows will not be
+
+ # Coverage.py lets developers exclude lines that they know will not be
# executed. So far, the branch coverage doesn't use all that information
# when deciding which lines are partially executed.
#
# Here, even though the else line is explicitly marked as never executed,
# the if line complains that it never branched to the else:
-
+
if x < 1000:
# This branch is always taken
print "x is reasonable"
@@ -48,7 +48,7 @@ def my_function(x):
# Here we run the code twice: once with no exception, and once with a
# matching exception. The "except" line is marked as partial because we
# never executed its third case: a non-matching exception.
-
+
for y in (1, 2):
try:
if y % 2:
@@ -57,10 +57,10 @@ def my_function(x):
print "y must have been odd"
print "done with y"
print "done with 1, 2"
-
+
# Another except clause, but this time all three cases are executed. No
# partial lines are shown:
-
+
for y in (0, 1, 2):
try:
if y % 2:
diff --git a/tests/coveragetest.py b/tests/coveragetest.py
index e1c38b2..d047a47 100644
--- a/tests/coveragetest.py
+++ b/tests/coveragetest.py
@@ -350,20 +350,21 @@ class CoverageTest(TestCase):
# Get the analysis results, and check that they are right.
analysis = cov._analyze(mod)
+ statements = sorted(analysis.statements)
if lines is not None:
if type(lines[0]) == type(1):
# lines is just a list of numbers, it must match the statements
# found in the code.
- self.assertEqual(analysis.statements, lines)
+ self.assertEqual(statements, lines)
else:
# lines is a list of possible line number lists, one of them
# must match.
for line_list in lines:
- if analysis.statements == line_list:
+ if statements == line_list:
break
else:
self.fail("None of the lines choices matched %r" %
- analysis.statements
+ statements
)
if type(missing) == type(""):
diff --git a/tests/test_templite.py b/tests/test_templite.py
index 0435c54..7326d24 100644
--- a/tests/test_templite.py
+++ b/tests/test_templite.py
@@ -1,7 +1,7 @@
"""Tests for coverage.templite."""
from coverage.templite import Templite
-import unittest
+from tests.coveragetest import CoverageTest
# pylint: disable=W0612,E1101
# Disable W0612 (Unused variable) and
@@ -18,9 +18,11 @@ class AnyOldObject(object):
setattr(self, n, v)
-class TempliteTest(unittest.TestCase):
+class TempliteTest(CoverageTest):
"""Tests for Templite."""
+ run_in_temp_dir = False
+
def try_render(self, text, ctx, result):
"""Render `text` through `ctx`, and it had better be `result`."""
self.assertEqual(Templite(text).render(ctx), result)
@@ -37,6 +39,14 @@ class TempliteTest(unittest.TestCase):
# Variables use {{var}} syntax.
self.try_render("Hello, {{name}}!", {'name':'Ned'}, "Hello, Ned!")
+ def test_undefined_variables(self):
+ # Using undefined names is an error.
+ self.assertRaises(
+ Exception,
+ self.try_render,
+ "Hi, {{name}}!", {}, "xyz"
+ )
+
def test_pipes(self):
# Variables can be filtered with pipes.
data = {
@@ -165,6 +175,23 @@ class TempliteTest(unittest.TestCase):
"Hi, NEDBEN!"
)
+ def test_complex_if(self):
+ class Complex(AnyOldObject):
+ """A class to try out complex data access."""
+ def getit(self):
+ """Return it."""
+ return self.it
+ obj = Complex(it={'x':"Hello", 'y': 0})
+ self.try_render(
+ "@"
+ "{% if obj.getit.x %}X{% endif %}"
+ "{% if obj.getit.y %}Y{% endif %}"
+ "{% if obj.getit.y|str %}S{% endif %}"
+ "!",
+ { 'obj': obj, 'str': str },
+ "@XS!"
+ )
+
def test_loop_if(self):
self.try_render(
"@{% for n in nums %}{% if n %}Z{% endif %}{{n}}{% endfor %}!",
@@ -184,9 +211,11 @@ class TempliteTest(unittest.TestCase):
def test_nested_loops(self):
self.try_render(
- "@{% for n in nums %}"
+ "@"
+ "{% for n in nums %}"
"{% for a in abc %}{{a}}{{n}}{% endfor %}"
- "{% endfor %}!",
+ "{% endfor %}"
+ "!",
{'nums': [0,1,2], 'abc': ['a', 'b', 'c']},
"@a0b0c0a1b1c1a2b2c2!"
)
@@ -199,6 +228,20 @@ class TempliteTest(unittest.TestCase):
)
def test_bogus_tag_syntax(self):
- self.assertRaises(SyntaxError, self.try_render,
+ self.assertRaisesRegexp(
+ SyntaxError, "Don't understand tag: 'bogus'",
+ self.try_render,
"Huh: {% bogus %}!!{% endbogus %}??", {}, ""
)
+
+ def test_bad_nesting(self):
+ self.assertRaisesRegexp(
+ SyntaxError, "Unmatched action tag: 'if'",
+ self.try_render,
+ "{% if x %}X", {}, ""
+ )
+ self.assertRaisesRegexp(
+ SyntaxError, "Mismatched end tag: 'for'",
+ self.try_render,
+ "{% if x %}X{% endfor %}", {}, ""
+ )