Switch to upstream source from tarball ‘python-coverage_3.7.1+dfsg.1.orig.tar.gz’.upstream/3.7.1+dfsg.1
authorBen Finney <[email protected]>
Sat, 12 Mar 2016 06:59:00 +0000 (12 17:59 +1100)
committerBen Finney <[email protected]>
Sat, 12 Mar 2016 07:00:06 +0000 (12 18:00 +1100)
21 files changed:
CHANGES.txt
PKG-INFO
coverage.egg-info/PKG-INFO
coverage/annotate.py
coverage/cmdline.py
coverage/control.py
coverage/html.py
coverage/misc.py
coverage/parser.py
coverage/phystokens.py
coverage/results.py
coverage/templite.py
coverage/version.py
coverage/xmlreport.py
doc/changes.rst
doc/cmd.rst
doc/index.rst
doc/install.rst
tests/coveragetest.py
tests/test_html.py
tests/test_templite.py

index 7abcca6..1e4b888 100644 (file)
@@ -2,6 +2,15 @@
 Change history for Coverage.py
 ------------------------------
 
+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.
+
+
 3.7 --- 6 October 2013
 ----------------------
 
index df1730d..6079215 100644 (file)
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
-Metadata-Version: 1.1
+Metadata-Version: 1.0
 Name: coverage
-Version: 3.7
+Version: 3.7.1
 Summary: Code coverage measurement for Python
 Home-page: http://nedbatchelder.com/code/coverage
 Author: Ned Batchelder and others
index df1730d..6079215 100644 (file)
@@ -1,6 +1,6 @@
-Metadata-Version: 1.1
+Metadata-Version: 1.0
 Name: coverage
-Version: 3.7
+Version: 3.7.1
 Summary: Code coverage measurement for Python
 Home-page: http://nedbatchelder.com/code/coverage
 Author: Ned Batchelder and others
index b7f32c1..5c39678 100644 (file)
@@ -2,6 +2,7 @@
 
 import os, re
 
+from coverage.backward import sorted                    # pylint: disable=W0622
 from coverage.report import Reporter
 
 class AnnotateReporter(Reporter):
@@ -59,9 +60,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
index 0881313..ea112a8 100644 (file)
@@ -1,6 +1,6 @@
 """Command-line support for Coverage."""
 
-import optparse, os, sys, traceback
+import optparse, os, sys, time, traceback
 
 from coverage.backward import sorted                # pylint: disable=W0622
 from coverage.execfile import run_python_file, run_python_module
@@ -717,7 +717,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:
         # An exception was caught while running the product code.  The
         # sys.exc_info() return tuple is packed into an ExceptionDuringRun
index 4b76121..f75a3dd 100644 (file)
@@ -572,8 +572,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):
index b5cef11..5242236 100644 (file)
@@ -20,19 +20,27 @@ STATIC_PATH = [
     os.path.join(os.path.dirname(__file__), "htmlfiles"),
 ]
 
-def data_filename(fname):
+def data_filename(fname, pkgdir=""):
     """Return the path to a data file of ours.
 
     The file is searched for on `STATIC_PATH`, and the first place it's found,
     is returned.
 
+    Each directory in `STATIC_PATH` is searched as-is, and also, if `pkgdir`
+    is provided, at that subdirectory.
+
     """
     for static_dir in STATIC_PATH:
         static_filename = os.path.join(static_dir, fname)
         if os.path.exists(static_filename):
             return static_filename
+        if pkgdir:
+            static_filename = os.path.join(static_dir, pkgdir, fname)
+            if os.path.exists(static_filename):
+                return static_filename
     raise CoverageException("Couldn't find static file %r" % fname)
 
+
 def data(fname):
     """Return the contents of a data file of ours."""
     data_file = open(data_filename(fname))
@@ -47,14 +55,14 @@ class HtmlReporter(Reporter):
 
     # These files will be copied from the htmlfiles dir to the output dir.
     STATIC_FILES = [
-            "style.css",
-            "jquery.min.js",
-            "jquery.hotkeys.js",
-            "jquery.isonscreen.js",
-            "jquery.tablesorter.min.js",
-            "coverage_html.js",
-            "keybd_closed.png",
-            "keybd_open.png",
+            ("style.css", ""),
+            ("jquery.min.js", "jquery"),
+            ("jquery.hotkeys.js", "jquery-hotkeys"),
+            ("jquery.isonscreen.js", "jquery-isonscreen"),
+            ("jquery.tablesorter.min.js", "jquery-tablesorter"),
+            ("coverage_html.js", ""),
+            ("keybd_closed.png", ""),
+            ("keybd_open.png", ""),
             ]
 
     def __init__(self, cov, config):
@@ -117,9 +125,9 @@ class HtmlReporter(Reporter):
     def make_local_static_report_files(self):
         """Make local instances of static files for HTML report."""
         # The files we provide must always be copied.
-        for static in self.STATIC_FILES:
+        for static, pkgdir in self.STATIC_FILES:
             shutil.copyfile(
-                data_filename(static),
+                data_filename(static, pkgdir),
                 os.path.join(self.directory, static)
                 )
 
@@ -176,7 +184,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"
index 2d2662d..0378173 100644 (file)
@@ -38,6 +38,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:
@@ -113,8 +115,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)
index 581c851..7a145a2 100644 (file)
@@ -108,7 +108,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" % (
@@ -175,16 +175,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:
@@ -192,13 +194,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.
@@ -215,8 +217,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
 
@@ -444,14 +449,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
@@ -664,3 +670,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
index 2a91882..99b1d5b 100644 (file)
@@ -1,7 +1,9 @@
 """Better tokenizing for coverage.py."""
 
 import codecs, keyword, re, sys, token, tokenize
-from coverage.backward import StringIO              # pylint: disable=W0622
+from coverage.backward import set                       # pylint: disable=W0622
+from coverage.parser import generate_tokens
+
 
 def phys_tokens(toks):
     """Return all physical tokens, even line continuations.
@@ -18,7 +20,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 +76,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):
index 2d13e81..db6df0d 100644 (file)
@@ -26,7 +26,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(
index c39e061..e5c0baf 100644 (file)
@@ -2,7 +2,53 @@
 
 # Coincidentally named the same as http://code.activestate.com/recipes/496702/
 
-import re, sys
+import re
+
+from coverage.backward import set                       # pylint: disable=W0622
+
+
+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 +85,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()
 
-        assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0]
-        self.ops = ops
+        for var_name in self.all_vars - self.loop_vars:
+            vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name))
+
+        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 +194,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
index eb42c5d..a43bde8 100644 (file)
@@ -1,7 +1,7 @@
 """The version and URL for coverage.py"""
 # This file is exec'ed in setup.py, don't import anything!
 
-__version__ = "3.7"                     # see detailed history in CHANGES.txt
+__version__ = "3.7.1"                   # see detailed history in CHANGES.txt
 
 __url__ = "http://nedbatchelder.com/code/coverage"
 if max(__version__).isalpha():
index 7837524..26ac02a 100644 (file)
@@ -117,7 +117,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))
 
index a263d1b..3ddf889 100644 (file)
@@ -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,11 +35,25 @@ 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
 ------------------------------
 
 - Added the ``--debug`` switch to ``coverage run``.  It accepts a list of
-  options indicating the type of internal activity to log to stderr.
+  options indicating the type of internal activity to log to stderr. For
+  details, see :ref:`the run --debug options <cmd_run_debug>`.
 
 - Improved the branch coverage facility, fixing `issue 92`_ and `issue 175`_.
 
index 3572fff..49062b3 100644 (file)
@@ -152,9 +152,9 @@ of options, each indicating a facet of operation to log to stderr:
 
 * ``sys``: before starting, dump all the system and environment information,
   as with :ref:`coverage debug sys <cmd_debug>`.
-  
+
 * ``dataio``: log when reading or writing any data file.
-  
+
 * ``pid``: annotate all debug output with the process id.
 
 
index 657e3d3..3a0d930 100644 (file)
@@ -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
index 2e807fa..bc8097a 100644 (file)
@@ -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
index 9f7c79c..f6680cc 100644 (file)
@@ -355,20 +355,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(""):
index e1d41d9..06132fb 100644 (file)
@@ -317,9 +317,32 @@ class HtmlStaticFileTest(CoverageTest):
         cov = coverage.coverage()
         self.start_import_stop(cov, "main")
         cov.html_report()
+
         jquery = open("htmlcov/jquery.min.js").read()
         self.assertEqual(jquery, "Not Really JQuery!")
 
+    def test_copying_static_files_from_system_in_dir(self):
+        # Make a new place for static files.
+        INSTALLED = [
+            "jquery/jquery.min.js",
+            "jquery-hotkeys/jquery.hotkeys.js",
+            "jquery-isonscreen/jquery.isonscreen.js",
+            "jquery-tablesorter/jquery.tablesorter.min.js",
+        ]
+        for fpath in INSTALLED:
+            self.make_file(os.path.join("static_here", fpath), "Not real.")
+        coverage.html.STATIC_PATH.insert(0, "static_here")
+
+        self.make_file("main.py", "print(17)")
+        cov = coverage.coverage()
+        self.start_import_stop(cov, "main")
+        cov.html_report()
+
+        for fpath in INSTALLED:
+            the_file = os.path.basename(fpath)
+            contents = open(os.path.join("htmlcov", the_file)).read()
+            self.assertEqual(contents, "Not real.")
+
     def test_cant_find_static_files(self):
         # Make the path point to useless places.
         coverage.html.STATIC_PATH = ["/xyzzy"]
index 0435c54..7326d24 100644 (file)
@@ -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 %}", {}, ""
+            )