diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2014-03-08 06:20:27 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2014-03-08 06:20:27 -0500 |
commit | 8cc4ffa19dd13d8433878aab3458f6c7572e74b5 (patch) | |
tree | 91bcc5715a213e333674f0d0543ffaa8e757c7d2 | |
parent | 95c3003ddc3c3e11203c8b8b54dace4f197b1c4c (diff) | |
download | python-coveragepy-git-8cc4ffa19dd13d8433878aab3458f6c7572e74b5.tar.gz |
Hacked-in Mako support.
-rw-r--r-- | coverage/codeunit.py | 117 | ||||
-rw-r--r-- | coverage/control.py | 2 | ||||
-rw-r--r-- | coverage/html.py | 4 | ||||
-rw-r--r-- | coverage/parser.py | 5 | ||||
-rw-r--r-- | coverage/results.py | 3 | ||||
-rw-r--r-- | tests/test_parser.py | 4 |
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 |