summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/cmdline.py40
-rw-r--r--coverage/html.py16
-rw-r--r--coverage/htmlfiles/pyfile.html20
-rw-r--r--coverage/parser.py29
-rw-r--r--coverage/plugin.py14
-rw-r--r--coverage/templite.py100
6 files changed, 118 insertions, 101 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 221c18d6..38f4b43a 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -699,32 +699,32 @@ def unglob_args(args):
HELP_TOPICS = {
'help': """\
- Coverage.py, version %(__version__)s
- Measure, collect, and report on code coverage in Python programs.
-
- usage: %(program_name)s <command> [options] [args]
-
- Commands:
- annotate Annotate source files with execution information.
- combine Combine a number of data files.
- erase Erase previously collected coverage data.
- help Get help on using coverage.py.
- html Create an HTML report.
- report Report coverage stats on modules.
- run Run a Python program and measure code execution.
- xml Create an XML report of coverage results.
-
- Use "%(program_name)s help <command>" for detailed help on any command.
- For full documentation, see %(__url__)s
+ Coverage.py, version %(__version__)s
+ Measure, collect, and report on code coverage in Python programs.
+
+ usage: %(program_name)s <command> [options] [args]
+
+ Commands:
+ annotate Annotate source files with execution information.
+ combine Combine a number of data files.
+ erase Erase previously collected coverage data.
+ help Get help on using coverage.py.
+ html Create an HTML report.
+ report Report coverage stats on modules.
+ run Run a Python program and measure code execution.
+ xml Create an XML report of coverage results.
+
+ Use "%(program_name)s help <command>" for detailed help on any command.
+ For full documentation, see %(__url__)s
""",
'minimum_help': """\
- Code coverage for Python. Use '%(program_name)s help' for help.
+ Code coverage for Python. Use '%(program_name)s help' for help.
""",
'version': """\
- Coverage.py, version %(__version__)s.
- Documentation at %(__url__)s
+ Coverage.py, version %(__version__)s.
+ Documentation at %(__url__)s
""",
}
diff --git a/coverage/html.py b/coverage/html.py
index 0d6e6f96..a7a92095 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -231,7 +231,8 @@ class HtmlReporter(Reporter):
else:
annotate_long = "%d missed branches: %s" % (
len(longs),
- ", ".join("%d) %s" % (num, ann_long) for num, ann_long in enumerate(longs, start=1)),
+ ", ".join("%d) %s" % (num, ann_long)
+ for num, ann_long in enumerate(longs, start=1)),
)
elif lineno in analysis.statements:
line_class.append(c_run)
@@ -262,7 +263,7 @@ class HtmlReporter(Reporter):
'fr': fr, 'nums': nums, 'lines': lines,
'time_stamp': self.time_stamp,
}
- html = spaceless(self.source_tmpl.render(template_values))
+ html = self.source_tmpl.render(template_values)
html_filename = rootname + ".html"
html_path = os.path.join(self.directory, html_filename)
@@ -425,17 +426,6 @@ def escape(t):
return t.replace("&", "&amp;").replace("<", "&lt;")
-def spaceless(html):
- """Squeeze out some annoying extra space from an HTML string.
-
- Nicely-formatted templates mean lots of extra space in the result.
- Get rid of some.
-
- """
- html = re.sub(r">\s+<p ", ">\n<p ", html)
- return html
-
-
def pair(ratio):
"""Format a pair of numbers so JavaScript can read them in an attribute."""
return "%s %s" % ratio
diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html
index fb8e131e..8542a467 100644
--- a/coverage/htmlfiles/pyfile.html
+++ b/coverage/htmlfiles/pyfile.html
@@ -71,14 +71,22 @@
<table>
<tr>
<td class="linenos">
- {% for line in lines %}
- <p id="n{{line.number}}" class="{{line.class}}"><a href="#n{{line.number}}">{{line.number}}</a></p>
- {% endfor %}
+{% for line in lines -%}
+ <p id="n{{line.number}}" class="{{line.class}}"><a href="#n{{line.number}}">{{line.number}}</a></p>
+{% endfor %}
</td>
<td class="text">
- {% for line in lines %}
- <p id="t{{line.number}}" class="{{line.class}}">{% if line.annotate %}<span class="annotate short">{{line.annotate}}</span><span class="annotate long">{{line.annotate_long}}</span>{% endif %}{{line.html}}<span class="strut">&nbsp;</span></p>
- {% endfor %}
+{# These are the source lines, which are very sensitive to whitespace. -#}
+{# The `{ # - # }` below are comments which slurp up the following space. -#}
+{% for line in lines -%}
+ <p id="t{{line.number}}" class="{{line.class}}">{#-#}
+ {% if line.annotate -%}
+ <span class="annotate short">{{line.annotate}}</span>{#-#}
+ <span class="annotate long">{{line.annotate_long}}</span>{#-#}
+ {% endif -%}
+ {{line.html}}<span class="strut">&nbsp;</span>{#-#}
+ </p>
+{% endfor %}
</td>
</tr>
</table>
diff --git a/coverage/parser.py b/coverage/parser.py
index f84133d2..f34a26fb 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -305,6 +305,7 @@ class PythonParser(object):
return exit_counts
def missing_arc_description(self, start, end):
+ """Provide an English sentence describing a missing arc."""
if self._missing_arc_fragments is None:
self._analyze_ast()
@@ -316,6 +317,9 @@ class PythonParser(object):
if emsg is None:
if end < 0:
+ # Hmm, maybe we have a one-line callable, let's check.
+ if (-end, end) in self._missing_arc_fragments:
+ return self.missing_arc_description(-end, end)
emsg = "didn't jump to the function exit"
else:
emsg = "didn't jump to line {lineno}"
@@ -491,6 +495,7 @@ class AstArcAnalyzer(object):
code_object_handler(node)
def add_arc(self, start, end, smsg=None, emsg=None):
+ """Add an arc, including message fragments to use if it is missing."""
if self.debug:
print("\nAdding arc: ({}, {}): {!r}, {!r}".format(start, end, smsg, emsg))
print(short_stack(limit=6))
@@ -672,7 +677,8 @@ class AstArcAnalyzer(object):
@contract(returns='ArcStarts')
def _handle__Break(self, node):
here = self.line_for_node(node)
- self.process_break_exits([ArcStart(here, cause="the break on line {lineno} wasn't executed")])
+ break_start = ArcStart(here, cause="the break on line {lineno} wasn't executed")
+ self.process_break_exits([break_start])
return set()
@contract(returns='ArcStarts')
@@ -703,7 +709,8 @@ class AstArcAnalyzer(object):
@contract(returns='ArcStarts')
def _handle__Continue(self, node):
here = self.line_for_node(node)
- self.process_continue_exits([ArcStart(here, cause="the continue on line {lineno} wasn't executed")])
+ continue_start = ArcStart(here, cause="the continue on line {lineno} wasn't executed")
+ self.process_continue_exits([continue_start])
return set()
@contract(returns='ArcStarts')
@@ -743,14 +750,16 @@ class AstArcAnalyzer(object):
@contract(returns='ArcStarts')
def _handle__Raise(self, node):
here = self.line_for_node(node)
- self.process_raise_exits([ArcStart(here, cause="the raise on line {lineno} wasn't executed")])
+ raise_start = ArcStart(here, cause="the raise on line {lineno} wasn't executed")
+ self.process_raise_exits([raise_start])
# `raise` statement jumps away, no exits from here.
return set()
@contract(returns='ArcStarts')
def _handle__Return(self, node):
here = self.line_for_node(node)
- self.process_return_exits([ArcStart(here, cause="the return on line {lineno} wasn't executed")])
+ return_start = ArcStart(here, cause="the return on line {lineno} wasn't executed")
+ self.process_return_exits([return_start])
# `return` statement jumps away, no exits from here.
return set()
@@ -794,7 +803,8 @@ class AstArcAnalyzer(object):
if last_handler_start is not None:
self.add_arc(last_handler_start, handler_start)
last_handler_start = handler_start
- from_start = ArcStart(handler_start, cause="the exception caught by line {lineno} didn't happen")
+ from_cause = "the exception caught by line {lineno} didn't happen"
+ from_start = ArcStart(handler_start, cause=from_cause)
handler_exits |= self.add_body_arcs(handler_node.body, from_start=from_start)
if node.orelse:
@@ -900,7 +910,7 @@ class AstArcAnalyzer(object):
if node.body:
exits = self.add_body_arcs(node.body, from_start=ArcStart(-1))
for xit in exits:
- self.add_arc(xit.lineno, -start, xit.cause, 'exit the module')
+ self.add_arc(xit.lineno, -start, xit.cause, "didn't exit the module")
else:
# Empty module.
self.add_arc(-1, start)
@@ -922,18 +932,19 @@ class AstArcAnalyzer(object):
for xit in exits:
self.add_arc(
xit.lineno, -start, xit.cause,
- "exit the body of class '{0}'".format(node.name),
+ "didn't exit the body of class '{0}'".format(node.name),
)
def _make_oneline_code_method(noun): # pylint: disable=no-self-argument
- def _method(self, node):
+ """A function to make methods for online callable _code_object__ methods."""
+ def _code_object__oneline_callable(self, node):
start = self.line_for_node(node)
self.add_arc(-1, start)
self.add_arc(
start, -start, None,
"didn't run the {0} on line {1}".format(noun, start),
)
- return _method
+ return _code_object__oneline_callable
_code_object__Lambda = _make_oneline_code_method("lambda")
_code_object__GeneratorExp = _make_oneline_code_method("generator expression")
diff --git a/coverage/plugin.py b/coverage/plugin.py
index 85521e34..97d9c16e 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -330,19 +330,13 @@ class FileReporter(object):
return {}
def missing_arc_description(self, start, end):
- """Provide an English phrase describing a missing arc.
+ """Provide an English sentence describing a missing arc.
- For an arc like (123, 456), it should read well to use the phrase like
- this::
-
- "Line {0} didn't {1}".format(123, missing_arc_description(123, 456))
-
- TODO: say more.
-
- By default, this simply returns the string "jump to {end}".
+ By default, this simply returns the string "Line {start} didn't jump
+ to {end}".
"""
- return "jump to line {end}".format(end=end)
+ return "Line {start} didn't jump to line {end}".format(start=start, end=end)
def source_token_lines(self):
"""Generate a series of tokenized lines, one for each line in `source`.
diff --git a/coverage/templite.py b/coverage/templite.py
index f131f748..4c09c113 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -90,6 +90,9 @@ class Templite(object):
{# This will be ignored #}
+ Any of these constructs can have a hypen at the end (`-}}`, `-%}`, `-#}`),
+ which will collapse the whitespace following the tag.
+
Construct a Templite with the template text, then use `render` against a
dictionary context to create a finished string::
@@ -151,53 +154,64 @@ class Templite(object):
# Split the text to form a list of tokens.
tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
+ squash = False
+
for token in tokens:
- if token.startswith('{#'):
- # Comment: ignore it and move on.
- continue
- elif token.startswith('{{'):
- # An expression to evaluate.
- expr = self._expr_code(token[2:-2].strip())
- buffered.append("to_str(%s)" % expr)
- elif token.startswith('{%'):
- # Action tag: split into words and parse further.
- flush_output()
- words = token[2:-2].strip().split()
- if words[0] == 'if':
- # An if statement: evaluate the expression to determine if.
- if len(words) != 2:
- self._syntax_error("Don't understand if", token)
- ops_stack.append('if')
- code.add_line("if %s:" % self._expr_code(words[1]))
- code.indent()
- elif words[0] == 'for':
- # A loop: iterate over expression result.
- if len(words) != 4 or words[2] != 'in':
- self._syntax_error("Don't understand for", token)
- ops_stack.append('for')
- self._variable(words[1], self.loop_vars)
- code.add_line(
- "for c_%s in %s:" % (
- words[1],
- self._expr_code(words[3])
+ if token.startswith('{'):
+ start, end = 2, -2
+ squash = (token[-3] == '-')
+ if squash:
+ end = -3
+
+ if token.startswith('{#'):
+ # Comment: ignore it and move on.
+ continue
+ elif token.startswith('{{'):
+ # An expression to evaluate.
+ expr = self._expr_code(token[start:end].strip())
+ buffered.append("to_str(%s)" % expr)
+ elif token.startswith('{%'):
+ # Action tag: split into words and parse further.
+ flush_output()
+
+ words = token[start:end].strip().split()
+ if words[0] == 'if':
+ # An if statement: evaluate the expression to determine if.
+ if len(words) != 2:
+ self._syntax_error("Don't understand if", token)
+ ops_stack.append('if')
+ code.add_line("if %s:" % self._expr_code(words[1]))
+ code.indent()
+ elif words[0] == 'for':
+ # A loop: iterate over expression result.
+ if len(words) != 4 or words[2] != 'in':
+ self._syntax_error("Don't understand for", token)
+ ops_stack.append('for')
+ self._variable(words[1], self.loop_vars)
+ 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.
- if len(words) != 1:
- self._syntax_error("Don't understand end", token)
- end_what = words[0][3:]
- if not ops_stack:
- self._syntax_error("Too many ends", token)
- start_what = ops_stack.pop()
- if start_what != end_what:
- self._syntax_error("Mismatched end tag", end_what)
- code.dedent()
- else:
- self._syntax_error("Don't understand tag", words[0])
+ code.indent()
+ elif words[0].startswith('end'):
+ # Endsomething. Pop the ops stack.
+ if len(words) != 1:
+ self._syntax_error("Don't understand end", token)
+ end_what = words[0][3:]
+ if not ops_stack:
+ self._syntax_error("Too many ends", token)
+ start_what = ops_stack.pop()
+ if start_what != end_what:
+ self._syntax_error("Mismatched end tag", end_what)
+ code.dedent()
+ else:
+ self._syntax_error("Don't understand tag", words[0])
else:
# Literal content. If it isn't empty, output it.
+ if squash:
+ token = token.lstrip()
if token:
buffered.append(repr(token))