diff options
-rw-r--r-- | coverage/backward.py | 2 | ||||
-rw-r--r-- | coverage/codeunit.py | 87 | ||||
-rw-r--r-- | coverage/control.py | 6 | ||||
-rw-r--r-- | coverage/parser.py | 79 | ||||
-rw-r--r-- | coverage/results.py | 42 | ||||
-rw-r--r-- | lab/parser.py | 4 | ||||
-rw-r--r-- | tests/test_parser.py | 54 |
7 files changed, 134 insertions, 140 deletions
diff --git a/coverage/backward.py b/coverage/backward.py index a0dc9027..03fa6512 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 f19a1799..d9cd5e44 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 e71547a8..07551ff1 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 88f6f29e..f569de25 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 8cac1476..79615c77 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 cc8266a9..932480df 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 6c7c8d99..5b90f342 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! |