diff options
Diffstat (limited to 'coverage/python.py')
-rw-r--r-- | coverage/python.py | 44 |
1 files changed, 34 insertions, 10 deletions
diff --git a/coverage/python.py b/coverage/python.py index 22ca214e..c3ca0e1e 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -1,14 +1,22 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + """Python source expertise for coverage.py""" import os.path +import types import zipimport from coverage import env, files -from coverage.misc import contract, NoSource, join_regex +from coverage.misc import ( + contract, CoverageException, expensive, NoSource, join_regex, isolate_module, +) from coverage.parser import PythonParser from coverage.phystokens import source_token_lines, source_encoding from coverage.plugin import FileReporter +os = isolate_module(os) + @contract(returns='bytes') def read_python_source(filename): @@ -43,8 +51,12 @@ def get_python_source(filename): break else: # Couldn't find source. - raise NoSource("No source for code: '%s'." % filename) + exc_msg = "No source for code: '%s'.\n" % (filename,) + exc_msg += "Aborting report output, consider using -i." + raise NoSource(exc_msg) + # Replace \f because of http://bugs.python.org/issue19035 + source = source.replace(b'\f', b' ') source = source.decode(source_encoding(source), "replace") # Python code should always end with a line with a newline. @@ -87,9 +99,15 @@ class PythonFileReporter(FileReporter): if hasattr(morf, '__file__'): filename = morf.__file__ + elif isinstance(morf, types.ModuleType): + # A module should have had .__file__, otherwise we can't use it. + # This could be a PEP-420 namespace package. + raise CoverageException("Module {0} has no file".format(morf)) else: filename = morf + filename = files.unicode_filename(filename) + # .pyc files should always refer to a .py instead. if filename.endswith(('.pyc', '.pyo')): filename = filename[:-1] @@ -101,6 +119,7 @@ class PythonFileReporter(FileReporter): if hasattr(morf, '__name__'): name = morf.__name__ name = name.replace(".", os.sep) + ".py" + name = files.unicode_filename(name) else: name = files.relative_filename(filename) self.relname = name @@ -110,29 +129,28 @@ class PythonFileReporter(FileReporter): self._statements = None self._excluded = None + @contract(returns='unicode') def relative_filename(self): return self.relname @property def parser(self): + """Lazily create a :class:`PythonParser`.""" if self._parser is None: self._parser = PythonParser( filename=self.filename, exclude=self.coverage._exclude_regex('exclude'), ) + self._parser.parse_source() return self._parser - def statements(self): + def lines(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 + return self.parser.statements - def excluded_statements(self): + def excluded_lines(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 + return self.parser.excluded def translate_lines(self, lines): return self.parser.translate_lines(lines) @@ -140,6 +158,7 @@ class PythonFileReporter(FileReporter): def translate_arcs(self, arcs): return self.parser.translate_arcs(arcs) + @expensive def no_branch_lines(self): no_branch = self.parser.lines_matching( join_regex(self.coverage.config.partial_list), @@ -147,12 +166,17 @@ class PythonFileReporter(FileReporter): ) return no_branch + @expensive def arcs(self): return self.parser.arcs() + @expensive def exit_counts(self): return self.parser.exit_counts() + def missing_arc_description(self, start, end, executed_arcs=None): + return self.parser.missing_arc_description(start, end, executed_arcs) + @contract(returns='unicode') def source(self): if self._source is None: |