summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/codeunit.py117
-rw-r--r--coverage/control.py2
-rw-r--r--coverage/html.py4
-rw-r--r--coverage/parser.py5
-rw-r--r--coverage/results.py3
-rw-r--r--tests/test_parser.py4
6 files changed, 126 insertions, 9 deletions
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index 4c834117..fac43ed7 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -1,11 +1,12 @@
"""Code unit (module) handling for Coverage."""
-import glob, os
+import glob, os, re
from coverage.backward import open_source, string_class, StringIO
from coverage.misc import CoverageException
from coverage.parser import CodeParser
from coverage.results import Analysis
+from coverage.phystokens import source_token_lines, source_encoding
def code_unit_factory(morfs, file_locator):
@@ -31,7 +32,19 @@ def code_unit_factory(morfs, file_locator):
globbed.append(morf)
morfs = globbed
- code_units = [PythonCodeUnit(morf, file_locator) for morf in morfs]
+ code_units = []
+ for morf in morfs:
+ # Hacked-in Mako support. Disabled for going onto trunk.
+ if 0 and isinstance(morf, string_class) and "/mako/" in morf:
+ # Super hack! Do mako both ways!
+ if 0:
+ cu = PythonCodeUnit(morf, file_locator)
+ cu.name += '_fako'
+ code_units.append(cu)
+ klass = MakoCodeUnit
+ else:
+ klass = PythonCodeUnit
+ code_units.append(klass(morf, file_locator))
return code_units
@@ -139,6 +152,7 @@ class CodeUnit(object):
class PythonCodeUnit(CodeUnit):
"""Represents a Python file."""
+ analysis_class = Analysis
parser_class = CodeParser
def adjust_filename(self, fname):
@@ -168,3 +182,102 @@ class PythonCodeUnit(CodeUnit):
return True
# Everything else is probably not Python.
return False
+
+ def source_token_lines(self, source):
+ return source_token_lines(source)
+
+ def source_encoding(self, source):
+ return source_encoding(source)
+
+
+def mako_template_name(py_filename):
+ with open(py_filename) as f:
+ py_source = f.read()
+
+ # Find the template filename. TODO: string escapes in the string.
+ m = re.search(r"^_template_filename = u?'([^']+)'", py_source, flags=re.MULTILINE)
+ if not m:
+ raise Exception("Couldn't find template filename in Mako file %r" % py_filename)
+ template_filename = m.group(1)
+ return template_filename
+
+
+class MakoParser(object):
+ def __init__(self, cu, text, filename, exclude):
+ self.cu = cu
+ self.text = text
+ self.filename = filename
+ self.exclude = exclude
+
+ def parse_source(self):
+ """Returns executable_line_numbers, excluded_line_numbers"""
+ with open(self.cu.filename) as f:
+ py_source = f.read()
+
+ # Get the line numbers.
+ self.py_to_html = {}
+ html_linenum = None
+ for linenum, line in enumerate(py_source.splitlines(), start=1):
+ m_source_line = re.search(r"^\s+# SOURCE LINE (\d+)$", line)
+ if m_source_line:
+ html_linenum = int(m_source_line.group(1))
+ else:
+ m_boilerplate_line = re.search(r"^\s+# BOILERPLATE ", line)
+ if m_boilerplate_line:
+ html_linenum = None
+ elif html_linenum:
+ self.py_to_html[linenum] = html_linenum
+
+ return set(self.py_to_html.values()), set()
+
+ def translate_lines(self, lines):
+ tlines = set(self.py_to_html.get(l, -1) for l in lines)
+ tlines.remove(-1)
+ return tlines
+
+ def first_lines(self, lines, *ignores):
+ return set(lines)
+
+ def first_line(self, line):
+ return line
+
+ def exit_counts(self):
+ return {}
+
+ def arcs(self):
+ return []
+
+
+class MakoAnalysis(Analysis):
+
+ def find_source(self, filename):
+ """Find the source for `filename`.
+
+ Returns two values: the actual filename, and the source.
+
+ """
+ mako_filename = mako_template_name(filename)
+ with open(mako_filename) as f:
+ source = f.read()
+
+ return mako_filename, source
+
+
+class MakoCodeUnit(CodeUnit):
+ analysis_class = MakoAnalysis
+ parser_class = MakoParser
+
+ def __init__(self, *args, **kwargs):
+ super(MakoCodeUnit, self).__init__(*args, **kwargs)
+ self.mako_filename = mako_template_name(self.filename)
+
+ def source_file(self):
+ return open(self.mako_filename)
+
+ def source_token_lines(self, source):
+ """Return the 'tokenized' text for the code."""
+ for line in source.splitlines():
+ yield [('txt', line)]
+
+ def source_encoding(self, source):
+ return "utf-8"
diff --git a/coverage/control.py b/coverage/control.py
index 38c6cb4f..e71547a8 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -595,7 +595,7 @@ class coverage(object):
if not isinstance(it, CodeUnit):
it = code_unit_factory(it, self.file_locator)[0]
- return Analysis(self, it)
+ return it.analysis_class(self, it)
def report(self, morfs=None, show_missing=True, ignore_errors=None,
file=None, # pylint: disable=W0622
diff --git a/coverage/html.py b/coverage/html.py
index d168e351..d890436c 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -167,7 +167,7 @@ class HtmlReporter(Reporter):
# If need be, determine the encoding of the source file. We use it
# later to properly write the HTML.
if sys.version_info < (3, 0):
- encoding = source_encoding(source)
+ encoding = cu.source_encoding(source)
# Some UTF8 files have the dreaded UTF8 BOM. If so, junk it.
if encoding.startswith("utf-8") and source[:3] == "\xef\xbb\xbf":
source = source[3:]
@@ -187,7 +187,7 @@ class HtmlReporter(Reporter):
lines = []
- for lineno, line in enumerate(source_token_lines(source), start=1):
+ for lineno, line in enumerate(cu.source_token_lines(source), start=1):
# Figure out how to mark this line.
line_class = []
annotate_html = ""
diff --git a/coverage/parser.py b/coverage/parser.py
index de6590aa..88f6f29e 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -13,7 +13,7 @@ from coverage.misc import CoverageException, NoSource, NotPython
class CodeParser(object):
"""Parse code to find executable lines, excluded lines, etc."""
- def __init__(self, text=None, filename=None, exclude=None):
+ def __init__(self, cu, text=None, filename=None, exclude=None):
"""
Source can be provided as `text`, the text itself, or `filename`, from
which the text will be read. Excluded lines are those that match
@@ -161,6 +161,9 @@ class CodeParser(object):
if not empty:
self.statement_starts.update(self.byte_parser._find_statements())
+ def translate_lines(self, lines):
+ return lines
+
def first_line(self, line):
"""Return the first line number of the statement including `line`."""
rng = self.multiline.get(line)
diff --git a/coverage/results.py b/coverage/results.py
index e63db0f5..8cac1476 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -18,13 +18,14 @@ class Analysis(object):
actual_filename, source = self.find_source(self.filename)
self.parser = code_unit.parser_class(
+ code_unit,
text=source, filename=actual_filename,
exclude=self.coverage._exclude_regex('exclude')
)
self.statements, self.excluded = self.parser.parse_source()
# Identify missing statements.
- executed = self.coverage.data.executed_lines(self.filename)
+ executed = self.parser.translate_lines(self.coverage.data.executed_lines(self.filename))
exec1 = self.parser.first_lines(executed)
self.missing = self.statements - exec1
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 80773c74..6c7c8d99 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -13,7 +13,7 @@ class ParserTest(CoverageTest):
def parse_source(self, text):
"""Parse `text` as source, and return the `CodeParser` used."""
text = textwrap.dedent(text)
- cp = CodeParser(text=text, exclude="nocover")
+ cp = CodeParser(None, text=text, exclude="nocover")
cp.parse_source()
return cp
@@ -98,7 +98,7 @@ class ParserFileTest(CoverageTest):
def parse_file(self, filename):
"""Parse `text` as source, and return the `CodeParser` used."""
- cp = CodeParser(filename=filename, exclude="nocover")
+ cp = CodeParser(None, filename=filename, exclude="nocover")
cp.parse_source()
return cp