diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2015-01-02 13:41:13 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2015-01-02 13:41:13 -0500 |
commit | 02cecb43c2cf5693eace5c60204e0831a0ec49b7 (patch) | |
tree | 58e2a424f001ac0c6b2e68959bb0bf84f675fa91 | |
parent | 7170c660826939a0f31137300bc29c5843095e47 (diff) | |
download | python-coveragepy-02cecb43c2cf5693eace5c60204e0831a0ec49b7.tar.gz |
Start formalizing the FileReporter interface to simplify things
-rw-r--r-- | coverage/codeunit.py | 3 | ||||
-rw-r--r-- | coverage/control.py | 4 | ||||
-rw-r--r-- | coverage/plugin.py | 42 | ||||
-rw-r--r-- | coverage/python.py | 52 | ||||
-rw-r--r-- | coverage/results.py | 30 | ||||
-rw-r--r-- | tests/plugin1.py | 11 | ||||
-rw-r--r-- | tests/plugin2.py | 13 |
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)) |