summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-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
5 files changed, 99 insertions, 32 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():