diff options
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. @@ -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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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'>« index</a> <a class='nav' href='http://nedbatchelder.com/code/coverage'>coverage.py v3.7</a> + <a class='nav' href='index.html'>« index</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/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 %}", {}, "" + ) |