summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2015-01-02 13:41:13 -0500
committerNed Batchelder <ned@nedbatchelder.com>2015-01-02 13:41:13 -0500
commit02cecb43c2cf5693eace5c60204e0831a0ec49b7 (patch)
tree58e2a424f001ac0c6b2e68959bb0bf84f675fa91
parent7170c660826939a0f31137300bc29c5843095e47 (diff)
downloadpython-coveragepy-02cecb43c2cf5693eace5c60204e0831a0ec49b7.tar.gz
Start formalizing the FileReporter interface to simplify things
-rw-r--r--coverage/codeunit.py3
-rw-r--r--coverage/control.py4
-rw-r--r--coverage/plugin.py42
-rw-r--r--coverage/python.py52
-rw-r--r--coverage/results.py30
-rw-r--r--tests/plugin1.py11
-rw-r--r--tests/plugin2.py13
7 files changed, 105 insertions, 50 deletions
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index 998aa09..b2c9a71 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -108,6 +108,3 @@ class CodeUnit(object):
place.
"""
return False
-
- def get_parser(self, exclude=None):
- raise NotImplementedError
diff --git a/coverage/control.py b/coverage/control.py
index 4aaf1af..59d9e89 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -303,7 +303,7 @@ class Coverage(object):
def _canonical_dir(self, morf):
"""Return the canonical directory of the module or file `morf`."""
- morf_filename = PythonCodeUnit(morf, self.file_locator).filename
+ morf_filename = PythonCodeUnit(morf, self).filename
return os.path.split(morf_filename)[0]
def _source_for_file(self, filename):
@@ -775,7 +775,7 @@ class Coverage(object):
)
)
else:
- file_reporter = PythonCodeUnit(morf, self.file_locator)
+ file_reporter = PythonCodeUnit(morf, self)
return file_reporter
diff --git a/coverage/plugin.py b/coverage/plugin.py
index 362e561..dd4ebfb 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -1,6 +1,24 @@
"""Plugin management for coverage.py"""
+# TODO: abc?
+def _needs_to_implement(that, func_name):
+ """Helper to raise NotImplementedError in interface stubs."""
+ if hasattr(that, "plugin_name"):
+ thing = "Plugin"
+ name = that.plugin_name
+ else:
+ thing = "Class"
+ klass = that.__class__
+ name = "{klass.__module__}.{klass.__name__}".format(klass=klass)
+
+ raise NotImplementedError(
+ "{thing} {name!r} needs to implement {func_name}()".format(
+ thing=thing, name=name, func_name=func_name
+ )
+ )
+
+
class CoveragePlugin(object):
"""Base class for coverage.py plugins."""
def __init__(self, options):
@@ -37,9 +55,7 @@ class CoveragePlugin(object):
`file_tracer`. It's an error to return None.
"""
- raise NotImplementedError(
- "Plugin %r needs to implement file_reporter" % self.plugin_name
- )
+ _needs_to_implement(self, "file_reporter")
class FileTracer(object):
@@ -61,7 +77,7 @@ class FileTracer(object):
The filename to credit with this execution.
"""
- return None
+ _needs_to_implement(self, "source_filename")
def has_dynamic_source_filename(self):
"""Does this FileTracer have dynamic source filenames?
@@ -126,3 +142,21 @@ class FileReporter(object):
"""Support needed for files during the reporting phase."""
def __init__(self, filename):
self.filename = filename
+
+ def statements(self):
+ _needs_to_implement(self, "statements")
+
+ def excluded_statements(self):
+ return set([])
+
+ def translate_lines(self, lines):
+ return set(lines)
+
+ def translate_arcs(self, arcs):
+ return arcs
+
+ def exit_counts(self):
+ return {}
+
+ def arcs(self):
+ return []
diff --git a/coverage/python.py b/coverage/python.py
index 6237692..977497a 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -7,7 +7,7 @@ import zipimport
from coverage.backward import unicode_class
from coverage.codeunit import CodeUnit
-from coverage.misc import NoSource
+from coverage.misc import NoSource, join_regex
from coverage.parser import PythonParser
from coverage.phystokens import source_token_lines, source_encoding
@@ -88,9 +88,54 @@ def get_zip_bytes(filename):
class PythonCodeUnit(CodeUnit):
"""Represents a Python file."""
- def __init__(self, morf, file_locator=None):
+ def __init__(self, morf, coverage=None):
+ self.coverage = coverage
+ file_locator = coverage.file_locator if coverage else None
super(PythonCodeUnit, self).__init__(morf, file_locator)
self._source = None
+ self._parser = None
+ self._statements = None
+ self._excluded = None
+
+ @property
+ def parser(self):
+ if self._parser is None:
+ self._parser = PythonParser(
+ filename=self.filename,
+ exclude=self.coverage._exclude_regex('exclude'),
+ )
+ return self._parser
+
+ def statements(self):
+ """Return the line numbers of statements in the file."""
+ if self._statements is None:
+ self._statements, self._excluded = self.parser.parse_source()
+ return self._statements
+
+ def excluded_statements(self):
+ """Return the line numbers of statements in the file."""
+ if self._excluded is None:
+ self._statements, self._excluded = self.parser.parse_source()
+ return self._excluded
+
+ def translate_lines(self, lines):
+ return self.parser.translate_lines(lines)
+
+ def translate_arcs(self, arcs):
+ return self.parser.translate_arcs(arcs)
+
+ def no_branch_lines(self):
+ no_branch = self.parser.lines_matching(
+ join_regex(self.coverage.config.partial_list),
+ join_regex(self.coverage.config.partial_always_list)
+ )
+ return no_branch
+
+ def arcs(self):
+ return self.parser.arcs()
+
+ def exit_counts(self):
+ return self.parser.exit_counts()
def _adjust_filename(self, fname):
# .pyc files should always refer to a .py instead.
@@ -109,9 +154,6 @@ class PythonCodeUnit(CodeUnit):
assert isinstance(self._source, unicode_class)
return self._source
- def get_parser(self, exclude=None):
- return PythonParser(filename=self.filename, exclude=exclude)
-
def should_be_python(self):
"""Does it seem like this file should contain Python?
diff --git a/coverage/results.py b/coverage/results.py
index f1c63bb..0b27971 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -3,7 +3,7 @@
import collections
from coverage.backward import iitems
-from coverage.misc import format_lines, join_regex
+from coverage.misc import format_lines
class Analysis(object):
@@ -11,23 +11,18 @@ class Analysis(object):
def __init__(self, cov, code_unit):
self.coverage = cov
-
- self.filename = code_unit.filename
- self.parser = code_unit.get_parser(
- exclude=self.coverage._exclude_regex('exclude')
- )
- self.statements, self.excluded = self.parser.parse_source()
+ self.file_reporter = code_unit
+ self.filename = self.file_reporter.filename
+ self.statements = self.file_reporter.statements()
+ self.excluded = self.file_reporter.excluded_statements()
# Identify missing statements.
executed = self.coverage.data.executed_lines(self.filename)
- executed = self.parser.translate_lines(executed)
+ executed = self.file_reporter.translate_lines(executed)
self.missing = self.statements - executed
if self.coverage.data.has_arcs():
- self.no_branch = self.parser.lines_matching(
- join_regex(self.coverage.config.partial_list),
- join_regex(self.coverage.config.partial_always_list)
- )
+ self.no_branch = self.file_reporter.no_branch_lines()
n_branches = self.total_branches()
mba = self.missing_branch_arcs()
n_partial_branches = sum(
@@ -62,13 +57,12 @@ class Analysis(object):
def arc_possibilities(self):
"""Returns a sorted list of the arcs in the code."""
- arcs = self.parser.arcs()
- return arcs
+ return self.file_reporter.arcs()
def arcs_executed(self):
"""Returns a sorted list of the arcs actually executed in the code."""
executed = self.coverage.data.executed_arcs(self.filename)
- executed = self.parser.translate_arcs(executed)
+ executed = self.file_reporter.translate_arcs(executed)
return sorted(executed)
def arcs_missing(self):
@@ -116,12 +110,12 @@ class Analysis(object):
def branch_lines(self):
"""Returns a list of line numbers that have more than one exit."""
- exit_counts = self.parser.exit_counts()
+ exit_counts = self.file_reporter.exit_counts()
return [l1 for l1,count in iitems(exit_counts) if count > 1]
def total_branches(self):
"""How many total branches are there?"""
- exit_counts = self.parser.exit_counts()
+ exit_counts = self.file_reporter.exit_counts()
return sum(count for count in exit_counts.values() if count > 1)
def missing_branch_arcs(self):
@@ -145,7 +139,7 @@ class Analysis(object):
(total_exits, taken_exits).
"""
- exit_counts = self.parser.exit_counts()
+ exit_counts = self.file_reporter.exit_counts()
missing_arcs = self.missing_branch_arcs()
stats = {}
for lnum in self.branch_lines():
diff --git a/tests/plugin1.py b/tests/plugin1.py
index c766002..f9da35c 100644
--- a/tests/plugin1.py
+++ b/tests/plugin1.py
@@ -3,7 +3,6 @@
import os.path
import coverage
-from coverage.parser import CodeParser
class Plugin(coverage.CoveragePlugin):
@@ -40,10 +39,8 @@ class FileTracer(coverage.plugin.FileTracer):
class FileReporter(coverage.plugin.FileReporter):
"""Dead-simple FileReporter."""
- def get_parser(self, exclude=None):
- return PluginParser()
+ def statements(self):
+ return set([105, 106, 107, 205, 206, 207])
-class PluginParser(CodeParser):
- """CodeParser hard-coded for a test in test_plugins.py."""
- def parse_source(self):
- return set([105, 106, 107, 205, 206, 207]), set([])
+ def excluded_statements(self):
+ return set([])
diff --git a/tests/plugin2.py b/tests/plugin2.py
index 4fb3d05..7d2ac7c 100644
--- a/tests/plugin2.py
+++ b/tests/plugin2.py
@@ -1,7 +1,6 @@
"""A plugin for test_plugins.py to import."""
import coverage
-from coverage.parser import CodeParser
class Plugin(coverage.CoveragePlugin):
@@ -30,19 +29,11 @@ class RenderFileTracer(coverage.plugin.FileTracer):
class FileReporter(coverage.plugin.FileReporter):
- # TODO: Why do I have to make a FileReporter just to make a CodeParser??
def __init__(self, filename):
self.filename = filename
- def get_parser(self, exclude=None):
+ def statements(self):
# Goofy test arrangement: claim that the file has as many lines as the
# number in its name.
num = self.filename.split(".")[0].split("_")[1]
- return PluginParser(int(num))
-
-class PluginParser(CodeParser):
- def __init__(self, num_lines):
- self.num_lines = num_lines
-
- def parse_source(self):
- return set(range(1, self.num_lines+1)), set([])
+ return set(range(1, int(num)+1))