summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2014-04-18 21:13:06 -0400
committerNed Batchelder <ned@nedbatchelder.com>2014-04-18 21:13:06 -0400
commit50c2193ed5fa2fde05f9d1c05e41b0af4a6d7901 (patch)
tree9b18e09c347e57267406927203852c981b78211b
parentf775236abfbb689ed99b313c070036693fe100bf (diff)
downloadpython-coveragepy-50c2193ed5fa2fde05f9d1c05e41b0af4a6d7901.tar.gz
More refactoring of hacked Mako support
-rw-r--r--coverage/backward.py2
-rw-r--r--coverage/codeunit.py87
-rw-r--r--coverage/control.py6
-rw-r--r--coverage/parser.py79
-rw-r--r--coverage/results.py42
-rw-r--r--lab/parser.py4
-rw-r--r--tests/test_parser.py54
7 files changed, 134 insertions, 140 deletions
diff --git a/coverage/backward.py b/coverage/backward.py
index a0dc902..03fa651 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -8,7 +8,7 @@
import os, re, sys
-# Pythons 2 and 3 differ on where to get StringIO
+# Pythons 2 and 3 differ on where to get StringIO.
try:
from cStringIO import StringIO
BytesIO = StringIO
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index f19a179..d9cd5e4 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -3,9 +3,8 @@
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.misc import CoverageException, NoSource
+from coverage.parser import CodeParser, PythonParser
from coverage.phystokens import source_token_lines, source_encoding
@@ -67,7 +66,7 @@ class CodeUnit(object):
f = morf.__file__
else:
f = morf
- f = self.adjust_filename(f)
+ f = self._adjust_filename(f)
self.filename = self.file_locator.canonical_filename(f)
if hasattr(morf, '__name__'):
@@ -88,9 +87,6 @@ class CodeUnit(object):
def __repr__(self):
return "<CodeUnit name=%r filename=%r>" % (self.name, self.filename)
- def adjust_filename(self, fname):
- return fname
-
# Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
# of them defined.
@@ -114,7 +110,7 @@ class CodeUnit(object):
the same directory, but need to differentiate same-named files from
different directories.
- For example, the file a/b/c.py might return 'a_b_c'
+ For example, the file a/b/c.py will return 'a_b_c'
"""
if self.modname:
@@ -152,10 +148,9 @@ class CodeUnit(object):
class PythonCodeUnit(CodeUnit):
"""Represents a Python file."""
- analysis_class = Analysis
- parser_class = CodeParser
+ parser_class = PythonParser
- def adjust_filename(self, fname):
+ def _adjust_filename(self, fname):
# .pyc files should always refer to a .py instead.
if fname.endswith(('.pyc', '.pyo')):
fname = fname[:-1]
@@ -163,6 +158,44 @@ class PythonCodeUnit(CodeUnit):
fname = fname[:-9] + ".py"
return fname
+ def find_source(self, filename):
+ """Find the source for `filename`.
+
+ Returns two values: the actual filename, and the source.
+
+ The source returned depends on which of these cases holds:
+
+ * The filename seems to be a non-source file: returns None
+
+ * The filename is a source file, and actually exists: returns None.
+
+ * The filename is a source file, and is in a zip file or egg:
+ returns the source.
+
+ * The filename is a source file, but couldn't be found: raises
+ `NoSource`.
+
+ """
+ source = None
+
+ base, ext = os.path.splitext(filename)
+ TRY_EXTS = {
+ '.py': ['.py', '.pyw'],
+ '.pyw': ['.pyw'],
+ }
+ try_exts = TRY_EXTS.get(ext)
+ if not try_exts:
+ return filename, None
+
+ for try_ext in try_exts:
+ try_filename = base + try_ext
+ if os.path.exists(try_filename):
+ return try_filename, None
+ source = self.file_locator.get_zip_data(try_filename)
+ if source:
+ return try_filename, source
+ raise NoSource("No source for code: '%s'" % filename)
+
def should_be_python(self):
"""Does it seem like this file should contain Python?
@@ -202,7 +235,7 @@ def mako_template_name(py_filename):
return template_filename
-class MakoParser(object):
+class MakoParser(CodeParser):
def __init__(self, cu, text, filename, exclude):
self.cu = cu
self.text = text
@@ -235,20 +268,16 @@ class MakoParser(object):
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 MakoCodeUnit(CodeUnit):
+ parser_class = MakoParser
+ def __init__(self, *args, **kwargs):
+ super(MakoCodeUnit, self).__init__(*args, **kwargs)
+ self.mako_filename = mako_template_name(self.filename)
-class MakoAnalysis(Analysis):
+ def source_file(self):
+ return open(self.mako_filename)
def find_source(self, filename):
"""Find the source for `filename`.
@@ -262,18 +291,6 @@ class MakoAnalysis(Analysis):
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():
diff --git a/coverage/control.py b/coverage/control.py
index e71547a..07551ff 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -4,7 +4,7 @@ import atexit, os, random, socket, sys
from coverage.annotate import AnnotateReporter
from coverage.backward import string_class, iitems
-from coverage.codeunit import code_unit_factory, CodeUnit
+from coverage.codeunit import code_unit_factory, CodeUnit, PythonCodeUnit
from coverage.collector import Collector
from coverage.config import CoverageConfig
from coverage.data import CoverageData
@@ -214,7 +214,7 @@ class coverage(object):
def _canonical_dir(self, morf):
"""Return the canonical directory of the module or file `morf`."""
- return os.path.split(CodeUnit(morf, self.file_locator).filename)[0]
+ return os.path.split(PythonCodeUnit(morf, self.file_locator).filename)[0]
def _source_for_file(self, filename):
"""Return the source file for `filename`."""
@@ -595,7 +595,7 @@ class coverage(object):
if not isinstance(it, CodeUnit):
it = code_unit_factory(it, self.file_locator)[0]
- return it.analysis_class(self, it)
+ return Analysis(self, it)
def report(self, morfs=None, show_missing=True, ignore_errors=None,
file=None, # pylint: disable=W0622
diff --git a/coverage/parser.py b/coverage/parser.py
index 88f6f29..f569de2 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -11,6 +11,47 @@ from coverage.misc import CoverageException, NoSource, NotPython
class CodeParser(object):
+ """
+ Base class for any code parser.
+ """
+ def _adjust_filename(self, fname):
+ return fname
+
+ 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 any of the sequences in `ignores`.
+
+ Returns a set of the first lines.
+
+ """
+ ignore = set()
+ for ign in ignores:
+ ignore.update(ign)
+ lset = set()
+ for l in lines:
+ if l in ignore:
+ continue
+ new_l = self.first_line(l)
+ if new_l not in ignore:
+ lset.add(new_l)
+ return lset
+
+ def first_line(self, line):
+ return line
+
+ def translate_lines(self, lines):
+ return lines
+
+ def exit_counts(self):
+ return {}
+
+ def arcs(self):
+ return []
+
+
+class PythonParser(CodeParser):
"""Parse code to find executable lines, excluded lines, etc."""
def __init__(self, cu, text=None, filename=None, exclude=None):
@@ -20,7 +61,7 @@ class CodeParser(object):
`exclude`, a regex.
"""
- assert text or filename, "CodeParser needs either text or filename"
+ assert text or filename, "PythonParser needs either text or filename"
self.filename = filename or "<code>"
self.text = text
if not self.text:
@@ -137,9 +178,8 @@ class CodeParser(object):
# We're at the end of a line, and we've ended on a
# different line than the first line of the statement,
# so record a multi-line range.
- rng = (first_line, elineno)
for l in range(first_line, elineno+1):
- self.multiline[l] = rng
+ self.multiline[l] = first_line
first_line = None
if ttext.strip() and toktype != tokenize.COMMENT:
@@ -161,38 +201,13 @@ 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)
- if rng:
- first_line = rng[0]
+ first_line = self.multiline.get(line)
+ if first_line:
+ return first_line
else:
- first_line = line
- return first_line
-
- 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 any of the sequences in `ignores`.
-
- Returns a set of the first lines.
-
- """
- ignore = set()
- for ign in ignores:
- ignore.update(ign)
- lset = set()
- for l in lines:
- if l in ignore:
- continue
- new_l = self.first_line(l)
- if new_l not in ignore:
- lset.add(new_l)
- return lset
+ return line
def parse_source(self):
"""Parse source text to find executable lines, excluded lines, etc.
diff --git a/coverage/results.py b/coverage/results.py
index 8cac147..79615c7 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -4,7 +4,7 @@ import collections
import os
from coverage.backward import iitems
-from coverage.misc import format_lines, join_regex, NoSource
+from coverage.misc import format_lines, join_regex
class Analysis(object):
@@ -15,7 +15,7 @@ class Analysis(object):
self.code_unit = code_unit
self.filename = self.code_unit.filename
- actual_filename, source = self.find_source(self.filename)
+ actual_filename, source = self.code_unit.find_source(self.filename)
self.parser = code_unit.parser_class(
code_unit,
@@ -54,44 +54,6 @@ class Analysis(object):
n_missing_branches=n_missing_branches,
)
- def find_source(self, filename):
- """Find the source for `filename`.
-
- Returns two values: the actual filename, and the source.
-
- The source returned depends on which of these cases holds:
-
- * The filename seems to be a non-source file: returns None
-
- * The filename is a source file, and actually exists: returns None.
-
- * The filename is a source file, and is in a zip file or egg:
- returns the source.
-
- * The filename is a source file, but couldn't be found: raises
- `NoSource`.
-
- """
- source = None
-
- base, ext = os.path.splitext(filename)
- TRY_EXTS = {
- '.py': ['.py', '.pyw'],
- '.pyw': ['.pyw'],
- }
- try_exts = TRY_EXTS.get(ext)
- if not try_exts:
- return filename, None
-
- for try_ext in try_exts:
- try_filename = base + try_ext
- if os.path.exists(try_filename):
- return try_filename, None
- source = self.coverage.file_locator.get_zip_data(try_filename)
- if source:
- return try_filename, source
- raise NoSource("No source for code: '%s'" % filename)
-
def missing_formatted(self):
"""The missing line numbers, formatted nicely.
diff --git a/lab/parser.py b/lab/parser.py
index cc8266a..932480d 100644
--- a/lab/parser.py
+++ b/lab/parser.py
@@ -9,7 +9,7 @@ from optparse import OptionParser
import disgen
from coverage.misc import CoverageException
-from coverage.parser import ByteParser, CodeParser
+from coverage.parser import ByteParser, PythonParser
opcode_counts = collections.Counter()
@@ -89,7 +89,7 @@ class ParserMain(object):
print("Arcs: %r" % sorted(arcs))
if options.source or options.tokens:
- cp = CodeParser(filename=filename, exclude=r"no\s*cover")
+ cp = PythonParser(filename=filename, exclude=r"no\s*cover")
cp.show_tokens = options.tokens
cp._raw_parse()
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 6c7c8d9..5b90f34 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -2,23 +2,23 @@
import textwrap
from tests.coveragetest import CoverageTest
-from coverage.parser import CodeParser
+from coverage.parser import PythonParser
-class ParserTest(CoverageTest):
- """Tests for Coverage.py's code parsing."""
+class PythonParserTest(CoverageTest):
+ """Tests for Coverage.py's Python code parsing."""
run_in_temp_dir = False
def parse_source(self, text):
- """Parse `text` as source, and return the `CodeParser` used."""
+ """Parse `text` as source, and return the `PythonParser` used."""
text = textwrap.dedent(text)
- cp = CodeParser(None, text=text, exclude="nocover")
- cp.parse_source()
- return cp
+ parser = PythonParser(None, text=text, exclude="nocover")
+ parser.parse_source()
+ return parser
def test_exit_counts(self):
- cp = self.parse_source("""\
+ parser = self.parse_source("""\
# check some basic branch counting
class Foo:
def foo(self, a):
@@ -30,12 +30,12 @@ class ParserTest(CoverageTest):
class Bar:
pass
""")
- self.assertEqual(cp.exit_counts(), {
+ self.assertEqual(parser.exit_counts(), {
2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1
})
def test_try_except(self):
- cp = self.parse_source("""\
+ parser = self.parse_source("""\
try:
a = 2
except ValueError:
@@ -46,12 +46,12 @@ class ParserTest(CoverageTest):
a = 8
b = 9
""")
- self.assertEqual(cp.exit_counts(), {
+ self.assertEqual(parser.exit_counts(), {
1: 1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1
})
def test_excluded_classes(self):
- cp = self.parse_source("""\
+ parser = self.parse_source("""\
class Foo:
def __init__(self):
pass
@@ -60,20 +60,20 @@ class ParserTest(CoverageTest):
class Bar:
pass
""")
- self.assertEqual(cp.exit_counts(), {
+ self.assertEqual(parser.exit_counts(), {
1:0, 2:1, 3:1
})
def test_missing_branch_to_excluded_code(self):
- cp = self.parse_source("""\
+ parser = self.parse_source("""\
if fooey:
a = 2
else: # nocover
a = 4
b = 5
""")
- self.assertEqual(cp.exit_counts(), { 1:1, 2:1, 5:1 })
- cp = self.parse_source("""\
+ self.assertEqual(parser.exit_counts(), { 1:1, 2:1, 5:1 })
+ parser = self.parse_source("""\
def foo():
if fooey:
a = 3
@@ -81,8 +81,8 @@ class ParserTest(CoverageTest):
a = 5
b = 6
""")
- self.assertEqual(cp.exit_counts(), { 1:1, 2:2, 3:1, 5:1, 6:1 })
- cp = self.parse_source("""\
+ self.assertEqual(parser.exit_counts(), { 1:1, 2:2, 3:1, 5:1, 6:1 })
+ parser = self.parse_source("""\
def foo():
if fooey:
a = 3
@@ -90,17 +90,17 @@ class ParserTest(CoverageTest):
a = 5
b = 6
""")
- self.assertEqual(cp.exit_counts(), { 1:1, 2:1, 3:1, 6:1 })
+ self.assertEqual(parser.exit_counts(), { 1:1, 2:1, 3:1, 6:1 })
class ParserFileTest(CoverageTest):
"""Tests for Coverage.py's code parsing from files."""
def parse_file(self, filename):
- """Parse `text` as source, and return the `CodeParser` used."""
- cp = CodeParser(None, filename=filename, exclude="nocover")
- cp.parse_source()
- return cp
+ """Parse `text` as source, and return the `PythonParser` used."""
+ parser = PythonParser(None, filename=filename, exclude="nocover")
+ parser.parse_source()
+ return parser
def test_line_endings(self):
text = """\
@@ -120,12 +120,12 @@ class ParserFileTest(CoverageTest):
for fname, newline in name_endings:
fname = fname + ".py"
self.make_file(fname, text, newline=newline)
- cp = self.parse_file(fname)
- self.assertEqual(cp.exit_counts(), counts)
+ parser = self.parse_file(fname)
+ self.assertEqual(parser.exit_counts(), counts)
def test_encoding(self):
self.make_file("encoded.py", """\
coverage = "\xe7\xf6v\xear\xe3g\xe9"
""")
- cp = self.parse_file("encoded.py")
- cp.exit_counts()
+ parser = self.parse_file("encoded.py")
+ parser.exit_counts() # TODO: This value should be tested!